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,