feat: add useForm
parent
b6688c4419
commit
cdbe8eea6b
|
@ -1,4 +1,5 @@
|
|||
import { BaseTransitionProps, CSSProperties, getCurrentInstance, onUpdated, Ref } from 'vue';
|
||||
import type { BaseTransitionProps, CSSProperties, Ref } from 'vue';
|
||||
import { getCurrentInstance, onUpdated } from 'vue';
|
||||
import { defineComponent, nextTick, Transition as T, TransitionGroup as TG } from 'vue';
|
||||
|
||||
export const getTransitionProps = (transitionName: string, opt: object = {}) => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { App, Plugin } from 'vue';
|
||||
import Form, { formProps } from './Form';
|
||||
import FormItem, { formItemProps } from './FormItem';
|
||||
import useForm from './useForm';
|
||||
|
||||
export type { FormProps } from './Form';
|
||||
export type { FormItemProps } from './FormItem';
|
||||
|
@ -12,8 +13,11 @@ Form.install = function (app: App) {
|
|||
return app;
|
||||
};
|
||||
|
||||
export { FormItem, formItemProps, formProps };
|
||||
export { FormItem, formItemProps, formProps, useForm };
|
||||
|
||||
Form.useForm = useForm;
|
||||
export default Form as typeof Form &
|
||||
Plugin & {
|
||||
readonly Item: typeof Form.Item;
|
||||
readonly useForm: typeof useForm;
|
||||
};
|
||||
|
|
|
@ -90,7 +90,7 @@ export interface ValidateErrorEntity<Values = any> {
|
|||
}
|
||||
|
||||
export interface FieldError {
|
||||
name: InternalNamePath;
|
||||
name: InternalNamePath | string;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,360 @@
|
|||
import type { Ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { reactive, watch, nextTick, unref } from 'vue';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import intersection from 'lodash-es/intersection';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import omit from 'lodash-es/omit';
|
||||
import { validateRules } from './utils/validateUtil';
|
||||
import { defaultValidateMessages } from './utils/messages';
|
||||
import { allPromiseFinish } from './utils/asyncUtil';
|
||||
import type { RuleError, ValidateMessages } from './interface';
|
||||
import type { ValidateStatus } from './FormItem';
|
||||
|
||||
interface DebounceSettings {
|
||||
leading?: boolean;
|
||||
|
||||
wait?: number;
|
||||
|
||||
trailing?: boolean;
|
||||
}
|
||||
|
||||
function isRequired(rules: any[]) {
|
||||
let isRequired = false;
|
||||
if (rules && rules.length) {
|
||||
rules.every((rule: { required: any }) => {
|
||||
if (rule.required) {
|
||||
isRequired = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return isRequired;
|
||||
}
|
||||
|
||||
function toArray(value: string | string[]) {
|
||||
if (value === undefined || value === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.isArray(value) ? value : [value];
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface validateOptions {
|
||||
validateFirst?: boolean;
|
||||
validateMessages?: ValidateMessages;
|
||||
trigger?: 'change' | 'blur' | string | string[];
|
||||
}
|
||||
|
||||
type namesType = string | string[];
|
||||
export interface ValidateInfo {
|
||||
autoLink?: boolean;
|
||||
required?: boolean;
|
||||
validateStatus?: ValidateStatus;
|
||||
help?: any;
|
||||
}
|
||||
|
||||
export interface validateInfos {
|
||||
[key: string]: ValidateInfo;
|
||||
}
|
||||
|
||||
function getPropByPath(obj: Props, path: string, strict: boolean) {
|
||||
let tempObj = obj;
|
||||
path = path.replace(/\[(\w+)\]/g, '.$1');
|
||||
path = path.replace(/^\./, '');
|
||||
|
||||
const keyArr = path.split('.');
|
||||
let i = 0;
|
||||
for (let len = keyArr.length; i < len - 1; ++i) {
|
||||
if (!tempObj && !strict) break;
|
||||
const key = keyArr[i];
|
||||
if (key in tempObj) {
|
||||
tempObj = tempObj[key];
|
||||
} else {
|
||||
if (strict) {
|
||||
throw new Error('please transfer a valid name path to validate!');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
o: tempObj,
|
||||
k: keyArr[i],
|
||||
v: tempObj ? tempObj[keyArr[i]] : null,
|
||||
isValid: tempObj && keyArr[i] in tempObj,
|
||||
};
|
||||
}
|
||||
|
||||
function useForm(
|
||||
modelRef: Props | Ref<Props>,
|
||||
rulesRef?: Props | Ref<Props>,
|
||||
options?: {
|
||||
immediate?: boolean;
|
||||
deep?: boolean;
|
||||
validateOnRuleChange?: boolean;
|
||||
debounce?: DebounceSettings;
|
||||
},
|
||||
): {
|
||||
modelRef: Props | Ref<Props>;
|
||||
rulesRef: Props | Ref<Props>;
|
||||
initialModel: Props;
|
||||
validateInfos: validateInfos;
|
||||
resetFields: (newValues?: Props) => void;
|
||||
validate: <T = any>(names?: namesType, option?: validateOptions) => Promise<T>;
|
||||
validateField: (
|
||||
name?: string,
|
||||
value?: any,
|
||||
rules?: [Record<string, unknown>],
|
||||
option?: validateOptions,
|
||||
) => Promise<RuleError[]>;
|
||||
mergeValidateInfo: (items: ValidateInfo | ValidateInfo[]) => ValidateInfo;
|
||||
clearValidate: (names?: namesType) => void;
|
||||
} {
|
||||
const initialModel = cloneDeep(unref(modelRef));
|
||||
let validateInfos: validateInfos = {};
|
||||
|
||||
const rulesKeys = computed(() => {
|
||||
return Object.keys(unref(rulesRef));
|
||||
});
|
||||
|
||||
rulesKeys.value.forEach(key => {
|
||||
validateInfos[key] = {
|
||||
autoLink: false,
|
||||
required: isRequired(unref(rulesRef)[key]),
|
||||
};
|
||||
});
|
||||
validateInfos = reactive(validateInfos);
|
||||
const resetFields = (newValues: Props) => {
|
||||
Object.assign(unref(modelRef), {
|
||||
...cloneDeep(initialModel),
|
||||
...newValues,
|
||||
});
|
||||
nextTick(() => {
|
||||
Object.keys(validateInfos).forEach(key => {
|
||||
validateInfos[key] = {
|
||||
autoLink: false,
|
||||
required: isRequired(unref(rulesRef)[key]),
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
const filterRules = (rules = [], trigger: string[]) => {
|
||||
if (!trigger.length) {
|
||||
return rules;
|
||||
} else {
|
||||
return rules.filter(rule => {
|
||||
const triggerList = toArray(rule.trigger || 'change');
|
||||
return intersection(triggerList, trigger).length;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let lastValidatePromise = null;
|
||||
const validateFields = (names: string[], option: validateOptions = {}, strict: boolean) => {
|
||||
// Collect result in promise list
|
||||
const promiseList: Promise<{
|
||||
name: string;
|
||||
errors: string[];
|
||||
}>[] = [];
|
||||
const values = {};
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
const name = names[i];
|
||||
const prop = getPropByPath(unref(modelRef), name, strict);
|
||||
if (!prop.isValid) continue;
|
||||
values[name] = prop.v;
|
||||
const rules = filterRules(unref(rulesRef)[name], toArray(option && option.trigger));
|
||||
if (rules.length) {
|
||||
promiseList.push(
|
||||
validateField(name, prop.v, rules, option || {})
|
||||
.then(() => ({
|
||||
name,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
}))
|
||||
.catch((ruleErrors: RuleError[]) => {
|
||||
const mergedErrors: string[] = [];
|
||||
const mergedWarnings: string[] = [];
|
||||
|
||||
ruleErrors.forEach(({ rule: { warningOnly }, errors }) => {
|
||||
if (warningOnly) {
|
||||
mergedWarnings.push(...errors);
|
||||
} else {
|
||||
mergedErrors.push(...errors);
|
||||
}
|
||||
});
|
||||
|
||||
if (mergedErrors.length) {
|
||||
return Promise.reject({
|
||||
name,
|
||||
errors: mergedErrors,
|
||||
warnings: mergedWarnings,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
errors: mergedErrors,
|
||||
warnings: mergedWarnings,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const summaryPromise = allPromiseFinish(promiseList);
|
||||
lastValidatePromise = summaryPromise;
|
||||
|
||||
const returnPromise = summaryPromise
|
||||
.then(() => {
|
||||
if (lastValidatePromise === summaryPromise) {
|
||||
return Promise.resolve(values);
|
||||
}
|
||||
return Promise.reject([]);
|
||||
})
|
||||
.catch((results: any[]) => {
|
||||
const errorList = results.filter(
|
||||
(result: { errors: string | any[] }) => result && result.errors.length,
|
||||
);
|
||||
return Promise.reject({
|
||||
values,
|
||||
errorFields: errorList,
|
||||
outOfDate: lastValidatePromise !== summaryPromise,
|
||||
});
|
||||
});
|
||||
|
||||
// Do not throw in console
|
||||
returnPromise.catch((e: any) => e);
|
||||
|
||||
return returnPromise;
|
||||
};
|
||||
const validateField = (
|
||||
name: string,
|
||||
value: any,
|
||||
rules: any,
|
||||
option: validateOptions,
|
||||
): Promise<RuleError[]> => {
|
||||
const promise = validateRules(
|
||||
[name],
|
||||
value,
|
||||
rules,
|
||||
{
|
||||
validateMessages: defaultValidateMessages,
|
||||
...option,
|
||||
},
|
||||
!!option.validateFirst,
|
||||
);
|
||||
validateInfos[name].validateStatus = 'validating';
|
||||
promise
|
||||
.catch((e: any) => e)
|
||||
.then((results: RuleError[] = []) => {
|
||||
if (validateInfos[name].validateStatus === 'validating') {
|
||||
const res = results.filter(result => result && result.errors.length);
|
||||
validateInfos[name].validateStatus = res.length ? 'error' : 'success';
|
||||
validateInfos[name].help = res.length ? res.map(r => r.errors) : '';
|
||||
}
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
|
||||
const validate = (names?: namesType, option?: validateOptions): Promise<any> => {
|
||||
let keys = [];
|
||||
let strict = true;
|
||||
if (!names) {
|
||||
strict = false;
|
||||
keys = rulesKeys.value;
|
||||
} else if (Array.isArray(names)) {
|
||||
keys = names;
|
||||
} else {
|
||||
keys = [names];
|
||||
}
|
||||
const promises = validateFields(keys, option || {}, strict);
|
||||
// Do not throw in console
|
||||
promises.catch((e: any) => e);
|
||||
return promises;
|
||||
};
|
||||
|
||||
const clearValidate = (names?: namesType) => {
|
||||
let keys = [];
|
||||
if (!names) {
|
||||
keys = rulesKeys.value;
|
||||
} else if (Array.isArray(names)) {
|
||||
keys = names;
|
||||
} else {
|
||||
keys = [names];
|
||||
}
|
||||
keys.forEach(key => {
|
||||
validateInfos[key] &&
|
||||
Object.assign(validateInfos[key], {
|
||||
validateStatus: '',
|
||||
help: '',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const mergeValidateInfo = (items: ValidateInfo[] | ValidateInfo) => {
|
||||
const info = { autoLink: false } as ValidateInfo;
|
||||
const help = [];
|
||||
const infos = Array.isArray(items) ? items : [items];
|
||||
for (let i = 0; i < infos.length; i++) {
|
||||
const arg = infos[i] as ValidateInfo;
|
||||
if (arg?.validateStatus === 'error') {
|
||||
info.validateStatus = 'error';
|
||||
arg.help && help.push(arg.help);
|
||||
}
|
||||
info.required = info.required || arg?.required;
|
||||
}
|
||||
info.help = help;
|
||||
return info;
|
||||
};
|
||||
let oldModel = initialModel;
|
||||
const modelFn = (model: { [x: string]: any }) => {
|
||||
const names = [];
|
||||
rulesKeys.value.forEach(key => {
|
||||
const prop = getPropByPath(model, key, false);
|
||||
const oldProp = getPropByPath(oldModel, key, false);
|
||||
if (!isEqual(prop.v, oldProp.v)) {
|
||||
names.push(key);
|
||||
}
|
||||
});
|
||||
validate(names, { trigger: 'change' });
|
||||
oldModel = cloneDeep(model);
|
||||
};
|
||||
const debounceOptions = options?.debounce;
|
||||
watch(
|
||||
modelRef,
|
||||
debounceOptions && debounceOptions.wait
|
||||
? debounce(modelFn, debounceOptions.wait, omit(debounceOptions, ['wait']))
|
||||
: modelFn,
|
||||
{ immediate: options && !!options.immediate, deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
rulesRef,
|
||||
() => {
|
||||
if (options && options.validateOnRuleChange) {
|
||||
validate();
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
return {
|
||||
modelRef,
|
||||
rulesRef,
|
||||
initialModel,
|
||||
validateInfos,
|
||||
resetFields,
|
||||
validate,
|
||||
validateField,
|
||||
mergeValidateInfo,
|
||||
clearValidate,
|
||||
};
|
||||
}
|
||||
|
||||
export default useForm;
|
2
v2-doc
2
v2-doc
|
@ -1 +1 @@
|
|||
Subproject commit 4c298275518d5790a58d26f2ed9b83ee5ba1dba4
|
||||
Subproject commit 7fd620c1f8c9f063af8048472d90cd7ed6c5bdd6
|
Loading…
Reference in New Issue