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