Fixes #6939 and #7075pull/7077/head
parent
d3fbb4170f
commit
536d21cef1
|
@ -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'));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue