From 536d21cef153416c665f728ee04012a0c492ae69 Mon Sep 17 00:00:00 2001
From: Mert Sincan <sincan.mert@gmail.com>
Date: Sun, 12 Jan 2025 18:26:03 +0000
Subject: [PATCH] Fixed #6939 and #7075: Forms/FormField: The form does not
 seem to support nested data.

Fixes #6939 and #7075
---
 .../baseeditableholder/BaseEditableHolder.vue |   6 +-
 packages/forms/src/form/Form.d.ts             |   6 ++
 packages/forms/src/useform/index.d.ts         |   1 +
 packages/forms/src/useform/index.js           | 101 +++++++++++++-----
 4 files changed, 82 insertions(+), 32 deletions(-)

diff --git a/packages/core/src/baseeditableholder/BaseEditableHolder.vue b/packages/core/src/baseeditableholder/BaseEditableHolder.vue
index 9d9790c45..ef00cf41a 100644
--- a/packages/core/src/baseeditableholder/BaseEditableHolder.vue
+++ b/packages/core/src/baseeditableholder/BaseEditableHolder.vue
@@ -76,7 +76,7 @@ export default {
         $formValue: {
             immediate: false,
             handler(newValue) {
-                if (this.$pcForm?.states?.[this.$formName] && newValue !== this.d_value) {
+                if (this.$pcForm?.getFieldState(this.$formName) && newValue !== this.d_value) {
                     this.d_value = newValue;
                 }
             }
@@ -104,7 +104,7 @@ export default {
             return isNotEmpty(this.d_value);
         },
         $invalid() {
-            return this.findNonEmpty(this.invalid, this.$pcFormField?.$field?.invalid, this.$pcForm?.states?.[this.$formName]?.invalid);
+            return this.findNonEmpty(this.invalid, this.$pcFormField?.$field?.invalid, this.$pcForm?.getFieldState(this.$formName)?.invalid);
         },
         $formName() {
             return this.name || this.$formControl?.name;
@@ -116,7 +116,7 @@ export default {
             return this.findNonEmpty(this.d_value, this.$pcFormField?.initialValue, this.$pcForm?.initialValues?.[this.$formName]);
         },
         $formValue() {
-            return this.findNonEmpty(this.$pcFormField?.$field?.value, this.$pcForm?.states?.[this.$formName]?.value);
+            return this.findNonEmpty(this.$pcFormField?.$field?.value, this.$pcForm?.getFieldState(this.$formName)?.value);
         },
         controlled() {
             return this.$inProps.hasOwnProperty('modelValue') || (!this.$inProps.hasOwnProperty('modelValue') && !this.$inProps.hasOwnProperty('defaultValue'));
diff --git a/packages/forms/src/form/Form.d.ts b/packages/forms/src/form/Form.d.ts
index 0eedcf220..389449a1e 100644
--- a/packages/forms/src/form/Form.d.ts
+++ b/packages/forms/src/form/Form.d.ts
@@ -277,6 +277,12 @@ export interface FormInstance {
      * @param value field value
      */
     setFieldValue: (field: string, value: any) => void;
+    /**
+     * Get the state of a form field.
+     * @param field field name
+     * @returns field state
+     */
+    getFieldState: (field: string) => FormFieldState | undefined;
     /**
      * Validates the form or a specific field.
      * @param field
diff --git a/packages/forms/src/useform/index.d.ts b/packages/forms/src/useform/index.d.ts
index 6aaf9504f..702d6ff0b 100644
--- a/packages/forms/src/useform/index.d.ts
+++ b/packages/forms/src/useform/index.d.ts
@@ -12,6 +12,7 @@ export interface useFormFieldState {
 export interface useFormReturn {
     defineField: (field: string, options?: any) => any;
     setFieldValue: (field: string, value: any) => void;
+    getFieldState: (field: string) => useFormFieldState | undefined;
     handleSubmit: (event: any) => any;
     handleReset: (event: any) => any;
     validate: (field: string) => any;
diff --git a/packages/forms/src/useform/index.js b/packages/forms/src/useform/index.js
index 72c9ccf85..b2b992aec 100644
--- a/packages/forms/src/useform/index.js
+++ b/packages/forms/src/useform/index.js
@@ -30,14 +30,45 @@ function watchPausable(source, callback, options) {
     };
 }
 
+// @todo: move to utils
+function groupKeys(obj) {
+    return Object.entries(obj).reduce((result, [key, value]) => {
+        key.split(/[\.\[\]]+/)
+            .filter(Boolean)
+            .reduce((acc, curr, idx, arr) => (acc[curr] ??= isNaN(arr[idx + 1]) ? (idx === arr.length - 1 ? value : {}) : []), result);
+
+        return result;
+    }, {});
+}
+
+function getValueByPath(obj, path) {
+    if (!obj || !path) {
+        // short circuit if there is nothing to resolve
+        return null;
+    }
+
+    try {
+        const value = obj[path];
+
+        if (isNotEmpty(value)) return value;
+    } catch {
+        // do nothing and continue to other methods to resolve path data
+    }
+
+    const keys = path.split(/[\.\[\]]+/).filter(Boolean);
+
+    return keys.reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), obj);
+}
+
 export const useForm = (options = {}) => {
-    const states = reactive({});
+    const _states = reactive({});
     const fields = reactive({});
-    const valid = computed(() => Object.values(states).every((field) => !field.invalid));
+    const valid = computed(() => Object.values(_states).every((field) => !field.invalid));
+    const states = computed(() => groupKeys(_states));
 
     const getInitialState = (field, initialValue) => {
         return {
-            value: initialValue ?? options.initialValues?.[field],
+            value: initialValue ?? getValueByPath(options.initialValues, field),
             touched: false,
             dirty: false,
             pristine: true,
@@ -78,45 +109,45 @@ export const useForm = (options = {}) => {
 
         fields[field]?._watcher.stop();
 
-        states[field] ||= getInitialState(field, fieldOptions?.initialValue);
+        _states[field] ||= getInitialState(field, fieldOptions?.initialValue);
 
-        const props = mergeProps(resolve(fieldOptions, states[field])?.props, resolve(fieldOptions?.props, states[field]), {
+        const props = mergeProps(resolve(fieldOptions, _states[field])?.props, resolve(fieldOptions?.props, _states[field]), {
             name: field,
             onBlur: () => {
-                states[field].touched = true;
+                _states[field].touched = true;
                 validateFieldOn(field, fieldOptions, 'validateOnBlur');
             },
             onInput: (event) => {
-                states[field].value = event.hasOwnProperty('value') ? event.value : event.target.value;
+                _states[field].value = event.hasOwnProperty('value') ? event.value : event.target.value;
             },
             onChange: (event) => {
-                states[field].value = event.hasOwnProperty('value') ? event.value : event.target.type === 'checkbox' || event.target.type === 'radio' ? event.target.checked : event.target.value;
+                _states[field].value = event.hasOwnProperty('value') ? event.value : event.target.type === 'checkbox' || event.target.type === 'radio' ? event.target.checked : event.target.value;
             },
             onInvalid: (errors) => {
-                states[field].invalid = true;
-                states[field].errors = errors;
-                states[field].error = errors?.[0] ?? null;
+                _states[field].invalid = true;
+                _states[field].errors = errors;
+                _states[field].error = errors?.[0] ?? null;
             }
         });
 
         const _watcher = watchPausable(
-            () => states[field].value,
+            () => _states[field].value,
             (newValue, oldValue) => {
-                if (states[field].pristine) {
-                    states[field].pristine = false;
+                if (_states[field].pristine) {
+                    _states[field].pristine = false;
                 }
 
                 if (newValue !== oldValue) {
-                    states[field].dirty = true;
+                    _states[field].dirty = true;
                 }
 
                 validateFieldOn(field, fieldOptions, 'validateOnValueUpdate', true);
             }
         );
 
-        fields[field] = { props, states: states[field], options: fieldOptions, _watcher };
+        fields[field] = { props, states: _states[field], options: fieldOptions, _watcher };
 
-        return [states[field], props];
+        return [_states[field], props];
     };
 
     const handleSubmit = (callback) => {
@@ -144,7 +175,7 @@ export const useForm = (options = {}) => {
     };
 
     const validate = async (field) => {
-        const resolverOptions = Object.entries(states).reduce(
+        const resolverOptions = Object.entries(_states).reduce(
             (acc, [key, val]) => {
                 acc.names.push(key);
                 acc.values[key] = val.value;
@@ -154,7 +185,11 @@ export const useForm = (options = {}) => {
             { names: [], values: {} }
         );
 
-        let result = (await options.resolver?.(resolverOptions)) ?? {};
+        let result =
+            (await options.resolver?.({
+                names: resolverOptions.names,
+                values: groupKeys(resolverOptions.values)
+            })) ?? {};
 
         result.errors ??= {};
 
@@ -173,33 +208,40 @@ export const useForm = (options = {}) => {
                     result = mergeKeys(result, fieldResult);
                 }
 
-                const errors = result.errors[fieldName] ?? [];
-                //const value = result.values?.[fieldName] ?? states[sField].value;
+                const errors = getValueByPath(result.errors, fieldName) ?? [];
 
-                states[fieldName].invalid = errors.length > 0;
-                states[fieldName].valid = !states[fieldName].invalid;
-                states[fieldName].errors = errors;
-                states[fieldName].error = errors?.[0] ?? null;
+                //const value = result.values?.[fieldName] ?? _states[sField].value;
+                _states[fieldName].invalid = errors.length > 0;
+                _states[fieldName].valid = !_states[fieldName].invalid;
+                _states[fieldName].errors = errors;
+                _states[fieldName].error = errors?.[0] ?? null;
                 //states[fieldName].value = value;
             }
         }
 
-        return result;
+        return {
+            ...result,
+            errors: groupKeys(result.errors)
+        };
     };
 
     const reset = () => {
-        Object.keys(states).forEach(async (field) => {
+        Object.keys(_states).forEach(async (field) => {
             const watcher = fields[field]._watcher;
 
             watcher.pause();
-            fields[field].states = states[field] = getInitialState(field, fields[field]?.options?.initialValue);
+            fields[field].states = _states[field] = getInitialState(field, fields[field]?.options?.initialValue);
             await nextTick();
             watcher.resume();
         });
     };
 
     const setFieldValue = (field, value) => {
-        states[field].value = value;
+        _states[field].value = value;
+    };
+
+    const getFieldState = (field) => {
+        return fields[field]?.states;
     };
 
     const setValues = (values) => {
@@ -215,6 +257,7 @@ export const useForm = (options = {}) => {
     return {
         defineField,
         setFieldValue,
+        getFieldState,
         handleSubmit,
         handleReset,
         validate,