From 103663b66bb4600fa65db7ed31f1789665d55c40 Mon Sep 17 00:00:00 2001 From: Md Afiq Iskandar Date: Tue, 15 Apr 2025 10:21:18 +0800 Subject: [PATCH] Add unsaved changes warning modal in Form Builder - Implemented a modal to warn users about unsaved changes when navigating away from the form builder. - Added computed properties and methods to manage unsaved changes state and navigation confirmation. - Updated the form management page to include unsaved changes handling when creating new forms. - Enhanced the form store to track unsaved changes status, ensuring better user experience and data integrity. --- pages/form-builder/index.vue | 107 +++++++++++++- pages/form-builder/manage.vue | 255 ++++++++++++++++++++++------------ stores/formBuilder.js | 28 +++- 3 files changed, 300 insertions(+), 90 deletions(-) diff --git a/pages/form-builder/index.vue b/pages/form-builder/index.vue index 441ede6..5db680f 100644 --- a/pages/form-builder/index.vue +++ b/pages/form-builder/index.vue @@ -22,7 +22,7 @@ type="text" name="formName" placeholder="Form Name" - v-model="formStore.formName" + v-model="formName" validation="required" validation-visibility="live" :validation-messages="{ required: 'Please enter a form name' }" @@ -151,6 +151,29 @@ + + + +
+
+ +
+

You have unsaved changes

+

Are you sure you want to leave? Your changes will be lost.

+
+
+
+ +
@@ -169,12 +192,80 @@ const router = useRouter(); const formStore = useFormBuilderStore(); const toast = useToast(); +const showPreview = ref(false); +const showUnsavedChangesModal = ref(false); +const pendingNavigation = ref(null); +const navigationTarget = ref(null); +const navigationConfirmed = ref(false); + +// Computed property for form name with getter and setter +const formName = computed({ + get: () => formStore.formName, + set: (value) => { + if (value !== formStore.formName) { + formStore.setFormName(value); + } + } +}); + // Initialize the form builder onMounted(() => { formStore.loadSavedForms(); + + // Add the beforeunload event listener + window.addEventListener('beforeunload', handleBeforeUnload); }); -const showPreview = ref(false); +onUnmounted(() => { + // Remove the beforeunload event listener + window.removeEventListener('beforeunload', handleBeforeUnload); +}); + +// Show warning if there are unsaved changes +const handleBeforeUnload = (event) => { + if (formStore.hasUnsavedChanges) { + event.preventDefault(); + event.returnValue = ''; + return ''; + } +}; + +// Navigation guards +// Add navigation guard +onBeforeRouteLeave((to, from, next) => { + // If navigation was already confirmed or there are no unsaved changes, proceed + if (navigationConfirmed.value || !formStore.hasUnsavedChanges) { + next(); + return; + } + + // Otherwise show the confirmation modal + showUnsavedChangesModal.value = true; + pendingNavigation.value = () => { + navigationConfirmed.value = true; + next(); + }; + next(false); +}); + +// Navigation handlers +const cancelNavigation = () => { + showUnsavedChangesModal.value = false; + pendingNavigation.value = null; + navigationTarget.value = null; + navigationConfirmed.value = false; +}; + +const confirmNavigation = () => { + showUnsavedChangesModal.value = false; + + if (pendingNavigation.value) { + pendingNavigation.value(); + } else if (navigationTarget.value) { + navigationConfirmed.value = true; // Mark as confirmed before navigating + router.push(navigationTarget.value); + } +}; // Handler methods const handleAddComponent = (component) => { @@ -242,11 +333,19 @@ const handlePreviewSubmit = (formData) => { }; const navigateToManage = () => { - router.push("/form-builder/manage"); + // If already confirmed or no unsaved changes, navigate directly + if (navigationConfirmed.value || !formStore.hasUnsavedChanges) { + router.push("/form-builder/manage"); + return; + } + + // Otherwise show confirmation modal + showUnsavedChangesModal.value = true; + navigationTarget.value = "/form-builder/manage"; }; const handleOptimizeLayout = () => { - // Implementation of handleOptimizeLayout method + formStore.optimizeGridLayout(); }; diff --git a/pages/form-builder/manage.vue b/pages/form-builder/manage.vue index c6a7359..edbe593 100644 --- a/pages/form-builder/manage.vue +++ b/pages/form-builder/manage.vue @@ -1,85 +1,154 @@ \ No newline at end of file + diff --git a/stores/formBuilder.js b/stores/formBuilder.js index e92de0d..c833576 100644 --- a/stores/formBuilder.js +++ b/stores/formBuilder.js @@ -8,7 +8,8 @@ export const useFormBuilderStore = defineStore('formBuilder', { formName: 'New Form', formDescription: '', isDraggingOver: false, - savedForms: [] + savedForms: [], + hasUnsavedChanges: false }), getters: { @@ -51,6 +52,7 @@ export const useFormBuilderStore = defineStore('formBuilder', { this.formComponents.push(newComponent); this.selectComponent(newComponent.id); + this.hasUnsavedChanges = true; }, // Find optimal placement for a new component in the grid @@ -142,6 +144,7 @@ export const useFormBuilderStore = defineStore('formBuilder', { const index = this.formComponents.findIndex(c => c.id === updatedComponent.id); if (index !== -1) { this.formComponents[index] = JSON.parse(JSON.stringify(updatedComponent)); + this.hasUnsavedChanges = true; } }, @@ -152,6 +155,7 @@ export const useFormBuilderStore = defineStore('formBuilder', { // Optimize layout after reordering this.optimizeGridLayout(); + this.hasUnsavedChanges = true; } }, @@ -172,6 +176,7 @@ export const useFormBuilderStore = defineStore('formBuilder', { // Optimize layout after deletion this.optimizeGridLayout(); + this.hasUnsavedChanges = true; } }, @@ -193,6 +198,8 @@ export const useFormBuilderStore = defineStore('formBuilder', { // Save to localStorage for persistence localStorage.setItem('savedForms', JSON.stringify(this.savedForms)); + this.hasUnsavedChanges = false; + return formData; }, @@ -211,11 +218,30 @@ export const useFormBuilderStore = defineStore('formBuilder', { } }, + setFormName(name) { + if (this.formName !== name) { + this.formName = name; + this.hasUnsavedChanges = true; + } + }, + + setFormDescription(description) { + if (this.formDescription !== description) { + this.formDescription = description; + this.hasUnsavedChanges = true; + } + }, + + resetUnsavedChanges() { + this.hasUnsavedChanges = false; + }, + clearForm() { this.formComponents = []; this.selectedComponentId = null; this.formName = 'New Form'; this.formDescription = ''; + this.hasUnsavedChanges = false; }, loadSavedForms() {