feat(unsaved-changes): implement utility for detecting and handling unsaved changes

This commit is contained in:
Sam Chau
2025-11-05 21:40:14 +10:30
parent f274c9900f
commit ee35e335d7
2 changed files with 109 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import { beforeNavigate, goto } from '$app/navigation';
import Modal from './Modal.svelte';
import { useUnsavedChanges } from '$lib/client/utils/unsavedChanges.svelte';
const unsavedChanges = useUnsavedChanges();
let pendingNavigationUrl: string | null = null;
beforeNavigate(async (navigation) => {
if (unsavedChanges.isDirty) {
navigation.cancel();
pendingNavigationUrl = navigation.to?.url.pathname || null;
const shouldNavigate = await unsavedChanges.confirmNavigation();
if (shouldNavigate && pendingNavigationUrl) {
goto(pendingNavigationUrl);
}
pendingNavigationUrl = null;
}
});
</script>
<Modal
open={unsavedChanges.showModal}
header="Unsaved Changes"
bodyMessage="You have unsaved changes. Are you sure you want to leave this page? Your changes will be lost."
confirmText="Discard Changes"
cancelText="Stay on Page"
confirmDanger={true}
on:confirm={() => unsavedChanges.confirmDiscard()}
on:cancel={() => unsavedChanges.cancelDiscard()}
/>

View File

@@ -0,0 +1,78 @@
/**
* Utility for detecting and handling unsaved changes
*/
let hasUnsavedChanges = $state(false);
let showWarningModal = $state(false);
let resolveNavigation: ((value: boolean) => void) | null = null;
export function useUnsavedChanges() {
return {
/**
* Mark the page as having unsaved changes
*/
markDirty() {
hasUnsavedChanges = true;
},
/**
* Mark the page as clean (changes saved)
*/
markClean() {
hasUnsavedChanges = false;
},
/**
* Check if there are unsaved changes
*/
get isDirty() {
return hasUnsavedChanges;
},
/**
* Get modal state
*/
get showModal() {
return showWarningModal;
},
/**
* Request navigation confirmation
* Returns a promise that resolves to true if navigation should proceed
*/
confirmNavigation(): Promise<boolean> {
if (!hasUnsavedChanges) {
return Promise.resolve(true);
}
showWarningModal = true;
return new Promise((resolve) => {
resolveNavigation = resolve;
});
},
/**
* User confirmed navigation (discard changes)
*/
confirmDiscard() {
showWarningModal = false;
hasUnsavedChanges = false;
if (resolveNavigation) {
resolveNavigation(true);
resolveNavigation = null;
}
},
/**
* User cancelled navigation (stay on page)
*/
cancelDiscard() {
showWarningModal = false;
if (resolveNavigation) {
resolveNavigation(false);
resolveNavigation = null;
}
}
};
}