Fixed #6939 and #7075: Forms/FormField: The form does not seem to support nested data.

Fixes #6939 and #7075
pull/7077/head
Mert Sincan 2025-01-12 18:26:03 +00:00
parent d3fbb4170f
commit 536d21cef1
4 changed files with 82 additions and 32 deletions

View File

@ -76,7 +76,7 @@ export default {
$formValue: { $formValue: {
immediate: false, immediate: false,
handler(newValue) { 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; this.d_value = newValue;
} }
} }
@ -104,7 +104,7 @@ export default {
return isNotEmpty(this.d_value); return isNotEmpty(this.d_value);
}, },
$invalid() { $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() { $formName() {
return this.name || this.$formControl?.name; 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]); return this.findNonEmpty(this.d_value, this.$pcFormField?.initialValue, this.$pcForm?.initialValues?.[this.$formName]);
}, },
$formValue() { $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() { controlled() {
return this.$inProps.hasOwnProperty('modelValue') || (!this.$inProps.hasOwnProperty('modelValue') && !this.$inProps.hasOwnProperty('defaultValue')); return this.$inProps.hasOwnProperty('modelValue') || (!this.$inProps.hasOwnProperty('modelValue') && !this.$inProps.hasOwnProperty('defaultValue'));

View File

@ -277,6 +277,12 @@ export interface FormInstance {
* @param value field value * @param value field value
*/ */
setFieldValue: (field: string, value: any) => void; 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. * Validates the form or a specific field.
* @param field * @param field

View File

@ -12,6 +12,7 @@ export interface useFormFieldState {
export interface useFormReturn { export interface useFormReturn {
defineField: (field: string, options?: any) => any; defineField: (field: string, options?: any) => any;
setFieldValue: (field: string, value: any) => void; setFieldValue: (field: string, value: any) => void;
getFieldState: (field: string) => useFormFieldState | undefined;
handleSubmit: (event: any) => any; handleSubmit: (event: any) => any;
handleReset: (event: any) => any; handleReset: (event: any) => any;
validate: (field: string) => any; validate: (field: string) => any;

View File

@ -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 = {}) => { export const useForm = (options = {}) => {
const states = reactive({}); const _states = reactive({});
const fields = 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) => { const getInitialState = (field, initialValue) => {
return { return {
value: initialValue ?? options.initialValues?.[field], value: initialValue ?? getValueByPath(options.initialValues, field),
touched: false, touched: false,
dirty: false, dirty: false,
pristine: true, pristine: true,
@ -78,45 +109,45 @@ export const useForm = (options = {}) => {
fields[field]?._watcher.stop(); 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, name: field,
onBlur: () => { onBlur: () => {
states[field].touched = true; _states[field].touched = true;
validateFieldOn(field, fieldOptions, 'validateOnBlur'); validateFieldOn(field, fieldOptions, 'validateOnBlur');
}, },
onInput: (event) => { 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) => { 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) => { onInvalid: (errors) => {
states[field].invalid = true; _states[field].invalid = true;
states[field].errors = errors; _states[field].errors = errors;
states[field].error = errors?.[0] ?? null; _states[field].error = errors?.[0] ?? null;
} }
}); });
const _watcher = watchPausable( const _watcher = watchPausable(
() => states[field].value, () => _states[field].value,
(newValue, oldValue) => { (newValue, oldValue) => {
if (states[field].pristine) { if (_states[field].pristine) {
states[field].pristine = false; _states[field].pristine = false;
} }
if (newValue !== oldValue) { if (newValue !== oldValue) {
states[field].dirty = true; _states[field].dirty = true;
} }
validateFieldOn(field, fieldOptions, 'validateOnValueUpdate', 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) => { const handleSubmit = (callback) => {
@ -144,7 +175,7 @@ export const useForm = (options = {}) => {
}; };
const validate = async (field) => { const validate = async (field) => {
const resolverOptions = Object.entries(states).reduce( const resolverOptions = Object.entries(_states).reduce(
(acc, [key, val]) => { (acc, [key, val]) => {
acc.names.push(key); acc.names.push(key);
acc.values[key] = val.value; acc.values[key] = val.value;
@ -154,7 +185,11 @@ export const useForm = (options = {}) => {
{ names: [], values: {} } { names: [], values: {} }
); );
let result = (await options.resolver?.(resolverOptions)) ?? {}; let result =
(await options.resolver?.({
names: resolverOptions.names,
values: groupKeys(resolverOptions.values)
})) ?? {};
result.errors ??= {}; result.errors ??= {};
@ -173,33 +208,40 @@ export const useForm = (options = {}) => {
result = mergeKeys(result, fieldResult); result = mergeKeys(result, fieldResult);
} }
const errors = result.errors[fieldName] ?? []; const errors = getValueByPath(result.errors, fieldName) ?? [];
//const value = result.values?.[fieldName] ?? states[sField].value;
states[fieldName].invalid = errors.length > 0; //const value = result.values?.[fieldName] ?? _states[sField].value;
states[fieldName].valid = !states[fieldName].invalid; _states[fieldName].invalid = errors.length > 0;
states[fieldName].errors = errors; _states[fieldName].valid = !_states[fieldName].invalid;
states[fieldName].error = errors?.[0] ?? null; _states[fieldName].errors = errors;
_states[fieldName].error = errors?.[0] ?? null;
//states[fieldName].value = value; //states[fieldName].value = value;
} }
} }
return result; return {
...result,
errors: groupKeys(result.errors)
};
}; };
const reset = () => { const reset = () => {
Object.keys(states).forEach(async (field) => { Object.keys(_states).forEach(async (field) => {
const watcher = fields[field]._watcher; const watcher = fields[field]._watcher;
watcher.pause(); 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(); await nextTick();
watcher.resume(); watcher.resume();
}); });
}; };
const setFieldValue = (field, value) => { const setFieldValue = (field, value) => {
states[field].value = value; _states[field].value = value;
};
const getFieldState = (field) => {
return fields[field]?.states;
}; };
const setValues = (values) => { const setValues = (values) => {
@ -215,6 +257,7 @@ export const useForm = (options = {}) => {
return { return {
defineField, defineField,
setFieldValue, setFieldValue,
getFieldState,
handleSubmit, handleSubmit,
handleReset, handleReset,
validate, validate,