import { computed, isReactive, reactive, toRaw, watch } from "vue"; import isEqual from "fast-deep-equal"; export const useForm = (initialFormValue) => { const form = reactive(deepClone(initialFormValue)); let originalForm = deepClone(initialFormValue); const updatedFields = reactive({}); const getUpdatedFields = computed(() => { return updatedFields; }); const hasUpdatedFields = computed(() => { return Object.keys(updatedFields).length > 0; }); const getFormAsFormData = () => { const formData = new FormData(); buildFormData(formData, form); return formData; }; const getUpdatedFieldsAsFormData = (spoofMethod = null) => { const formData = new FormData(); buildFormData(formData, updatedFields); if (spoofMethod) { formData.append("_method", spoofMethod.toUpperCase()); } return formData; }; const resetUpdateForm = () => { const newFormState = deepClone(originalForm); Object.keys(form).forEach((key) => delete form[key]); Object.assign(form, newFormState); }; const setUpdateFormAsOriginal = () => { originalForm = deepClone(toRaw(form)); }; watch( form, (newValue) => { const changes = diff(toRaw(newValue), originalForm); Object.keys(updatedFields).forEach((key) => delete updatedFields[key]); Object.assign(updatedFields, changes); }, { deep: true }, ); return { form, getUpdatedFields, hasUpdatedFields, getFormAsFormData, getUpdatedFieldsAsFormData, resetUpdateForm, setUpdateFormAsOriginal, }; }; const buildFormData = (formData, data, parentKey = "") => { if (data === undefined) { return; } if (data == null) { formData.append(parentKey, null); return; } if (Array.isArray(data)) { data.forEach((value, index) => { buildFormData(formData, value, `${parentKey}[${index}]`); }); return; } if ( typeof data === "object" && !(data instanceof File) && !(data instanceof Date) ) { Object.keys(data).forEach((key) => { const propName = parentKey ? `${parentKey}[${key}]` : key; buildFormData(formData, data[key], propName); }); return; } let valueToAppend = data; if (data instanceof Date) { valueToAppend = data.toISOString().slice(0, 19).replace("T", " "); } formData.append(parentKey, valueToAppend); } const deepClone = (obj) => { if (obj && isReactive(obj)) { obj = toRaw(obj); } if (typeof structuredClone === "function") { try { return structuredClone(obj); } catch (e) { console.warn( "structuredClone not supported, using JSON methods instead: " + e, ); } } return JSON.parse(JSON.stringify(obj)); } const diff = (currentObj, baseObj) => { const changes = {}; const currentKeys = Object.keys(currentObj); const baseKeys = Object.keys(baseObj); for (const key of currentKeys) { const currentValue = currentObj[key]; const baseValue = baseObj[key]; if (!isEqual(currentValue, baseValue)) { if ( currentValue && typeof currentValue === "object" && !Array.isArray(currentValue) && baseValue && typeof baseValue === "object" && !Array.isArray(baseValue) ) { const nestedChanges = diff(currentValue, baseValue); if (Object.keys(nestedChanges).length > 0) { changes[key] = nestedChanges; } } else { changes[key] = deepClone(currentValue); } } } for (const key of baseKeys) { if (!Object.prototype.hasOwnProperty.call(currentObj, key)) { changes[key] = null; } } return changes; }