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: {
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'));

View File

@ -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

View File

@ -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;

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 = {}) => {
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,