| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- import { reactive, computed, toRaw, isReactive, watch } from "vue";
- import isEqual from "fast-deep-equal";
- export const useFormUpdateTracker = (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;
- });
- watch(
- form,
- (newValue) => {
- const changes = diff(toRaw(newValue), originalForm);
- Object.keys(updatedFields).forEach((key) => delete updatedFields[key]);
- Object.assign(updatedFields, changes);
- },
- { deep: true },
- );
- const resetUpdateForm = () => {
- const newFormState = deepClone(originalForm);
- Object.keys(form).forEach((key) => delete form[key]);
- Object.assign(form, newFormState);
- };
- const setUpdateFormAsOriginal = () => {
- originalForm = deepClone(toRaw(form));
- };
- 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;
- };
- return {
- form,
- getUpdatedFields,
- hasUpdatedFields,
- resetUpdateForm,
- setUpdateFormAsOriginal,
- getFormAsFormData,
- getUpdatedFieldsAsFormData,
- };
- };
- /**
- * A recursive function to find the differences between two objects.
- * It returns a new object containing only the keys that have been added,
- * changed, or removed (set to null).
- * @param {object} currentObj The current state of the object.
- * @param {object} baseObj The original object to compare against.
- * @returns {object} An object with only the changed, new, or deleted keys.
- */
- function 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;
- }
- /**
- * Recursively builds a FormData object from a nested plain object using bracket notation
- * for keys, which is compatible with PHP/Laravel backends.
- * @param {FormData} formData The FormData instance.
- * @param {object} data The plain object to serialize.
- * @param {string} parentKey The base key for nested properties.
- */
- function 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);
- }
- /** * Deep clones an object using structuredClone if available,
- * otherwise falls back to JSON methods.
- * If the object is reactive, it converts it to a raw object first.
- * @param {object} obj The object to clone.
- * @returns {object} A deep clone of the input object.
- */
- function 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));
- }
|