|
|
@@ -0,0 +1,195 @@
|
|
|
+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));
|
|
|
+}
|