UNPKG

element-plus-useform

Version:

element-plus useForm hook,使表单验证脱离组件实例

532 lines (461 loc) 17.7 kB
import AsyncValidator, { type RuleItem, type ValidateError, type ValidateFieldsError } from 'async-validator' import { computed, effectScope, nextTick, reactive, ref, toRaw, unref, watch, type ComputedGetter, type ComputedRef, type Ref, type WatchHandle, type WritableComputedRef, } from 'vue' export type Arrayable<T> = T | T[] export type UseFormPropertyKey = string | number export type UseFormItemPropertyKey = Arrayable<UseFormPropertyKey> export interface UseFormModel { [K: UseFormPropertyKey]: any } export type UseFormRuleItemTrigger = 'blur' | 'change' export interface UseFormRuleItem extends RuleItem { // TODO: trigger trigger?: Arrayable<UseFormRuleItemTrigger> } export type UseFormRules<T extends UseFormModel = UseFormModel> = { [K in keyof T]?: UseFormRuleItem | UseFormRuleItem[] } export interface UseFormOptions { /** * 开启 deep rule 监听 * @default false * @see https://github.com/yiminghe/async-validator?tab=readme-ov-file#deep-rules */ deepRule?: boolean /** * 严格模式。false: 键不存在时不进行验证;true: 键不存在时仍然进行验证 * @default false */ strick?: boolean /** * @default false */ validateOnRuleChange?: boolean /** * 深度克隆函数 * @default structuredClone * @see https://developer.mozilla.org/zh-CN/docs/Web/API/structuredClone */ cloneDeep?: <T>(value: T) => T } export type UseFormValidationResult = Promise<boolean> export type UseFormValidateCallback = (isValid: boolean, invalidFields?: ValidateFieldsError) => Promise<void> | void export interface UseFormValidateFailure { errors: ValidateError[] | null fields: ValidateFieldsError } export type UseFormValidateStatus = 'error' | 'validating' | 'success' | '' export interface UseFormValidateInfo { /** * 必填项 */ required?: boolean /** * 验证状态 */ validateStatus?: UseFormValidateStatus /** * 错误信息 */ error?: string } export type UseFormValidateInfos<T extends UseFormModel = UseFormModel> = { [key in keyof T]?: UseFormValidateInfo } & { [key: UseFormPropertyKey]: UseFormValidateInfo } export interface UseFormResult< T extends UseFormModel, M extends T | Ref<T>, R extends UseFormRules<T> | Ref<UseFormRules<T>> | ComputedRef<UseFormRules<T>> | WritableComputedRef<UseFormRules<T>> | (() => UseFormRules<T>) > { model: M rules: R extends ComputedGetter<any> ? ComputedRef<UseFormRules<T>> : R initialModel: Ref<T> validateInfos: UseFormValidateInfos<T> /** * 对整个表单的内容进行验证。 接收一个回调函数,或返回 Promise。 * @see https://element-plus.org/zh-CN/component/form.html#form-exposes */ validate(callback?: UseFormValidateCallback): UseFormValidationResult /** * 验证具体的某个字段。 * @see https://element-plus.org/zh-CN/component/form.html#form-exposes */ validateField(props?: Arrayable<UseFormItemPropertyKey>, callback?: UseFormValidateCallback): Promise<any> /** * 重置表单数据(与 element-plus 不一致) * @param newModel 新的 model 数据,默认使用初始值 */ resetFields(newModel?: Partial<T>): void /** * 清除验证信息 * @param props 需要被清除的验证信息的 key,支持点语法和数组语法。不传入时清除所有验证信息。 * @see https://element-plus.org/zh-CN/component/form.html#form-exposes */ clearValidate(props?: Arrayable<UseFormItemPropertyKey>): void /** * 清除深度验证信息及相关监听 * @param props 需要被清除的深度验证信息的 key,支持点语法和数组语法。不传入时清除所有深度验证信息及相关监听。 * @param strick true: 取值路径仅最后一级 key 不存在时也会被清除;false: 取值路径仅最后一级 key 不存在时不会被清除 */ clearDeepInfos(props?: Arrayable<UseFormItemPropertyKey>, strick?: boolean): void } interface GetResult { value: any exist: boolean diff: number } export function toArray<T>(value?: T | T[], clone?: boolean): T[] { if (Array.isArray(value)) return clone ? value.slice() : value if (value === undefined || value === null) return [] return [value] } export function normalizeKey(key: Arrayable<UseFormPropertyKey>, nkc: Record<string | number, string>): string { if (typeof key === 'number') return String(key) if (Array.isArray(key)) key = key.join('.') return nkc[key] ?? (nkc[key] = key.replace(/\[(\w+)\]/g, '.$1').replace(/^\./, '')) } export function get(obj: Record<string, any>, key: string): GetResult { const keys = key.split('.') let value: any = obj for (let i = 0; i < keys.length; i++) { const k = keys[i] if (k in value) { value = value[k] } else { const d = keys.length - i - 1 return { value: undefined, exist: d === 0, diff: d } } } return { value, exist: true, diff: 0 } } export function isRequired(rule: UseFormRuleItem | UseFormRuleItem[]): boolean { return toArray(rule).some((r) => r.required) } export function isDeepRule(rule: UseFormRuleItem): boolean { return (rule.type === 'object' || rule.type === 'array') && !!(rule.fields || rule.defaultField) } function getRuleByDeepRule(key: string, ruleSource: Record<string, UseFormRuleItem | UseFormRuleItem[]>) { const keys = key.split('.') const rules: UseFormRuleItem[] = [] for (let i = 0; i < keys.length; i++) { let j = i + 1 let deepRules: UseFormRuleItem[] = toArray(ruleSource[keys.slice(0, j).join('.')]) while (deepRules.length && j < keys.length) { const currentKey = keys[j] const currentRuels: UseFormRuleItem[] = [] for (const rule of deepRules) { if (isDeepRule(rule)) { currentRuels.push(...toArray(rule.defaultField)) currentRuels.push(...toArray(rule.fields?.[currentKey])) } } deepRules = currentRuels j++ } if (j >= keys.length) { rules.push(...deepRules) } } return rules } export function useForm<T extends UseFormModel, M extends T = T, R extends UseFormRules<T> = UseFormRules<T>>( modelRef: M, rulesRef: R, options?: UseFormOptions ): UseFormResult<T, M, R> export function useForm<T extends UseFormModel, M extends T = T, R extends WritableComputedRef<UseFormRules<T>> = WritableComputedRef<UseFormRules<T>>>( modelRef: M, rulesRef: R, options?: UseFormOptions ): UseFormResult<T, M, R> export function useForm<T extends UseFormModel, M extends T = T, R extends ComputedRef<UseFormRules<T>> = ComputedRef<UseFormRules<T>>>( modelRef: M, rulesRef: R, options?: UseFormOptions ): UseFormResult<T, M, R> export function useForm<T extends UseFormModel, M extends T = T, R extends ComputedGetter<UseFormRules<T>> = ComputedGetter<UseFormRules<T>>>( modelRef: M, rulesRef: R, options?: UseFormOptions ): UseFormResult<T, M, R> export function useForm<T extends UseFormModel, M extends T = T, R extends Ref<UseFormRules<T>> = Ref<UseFormRules<T>>>( modelRef: M, rulesRef: R, options?: UseFormOptions ): UseFormResult<T, M, R> export function useForm<T extends UseFormModel, M extends Ref<T> = Ref<T>, R extends UseFormRules<T> = UseFormRules<T>>( modelRef: M, rulesRef: R, options?: UseFormOptions ): UseFormResult<T, M, R> export function useForm<T extends UseFormModel, M extends Ref<T> = Ref<T>, R extends WritableComputedRef<UseFormRules<T>> = WritableComputedRef<UseFormRules<T>>>( modelRef: M, rulesRef: R, options?: UseFormOptions ): UseFormResult<T, M, R> export function useForm<T extends UseFormModel, M extends Ref<T> = Ref<T>, R extends ComputedRef<UseFormRules<T>> = ComputedRef<UseFormRules<T>>>( modelRef: M, rulesRef: R, options?: UseFormOptions ): UseFormResult<T, M, R> export function useForm<T extends UseFormModel, M extends Ref<T> = Ref<T>, R extends ComputedGetter<UseFormRules<T>> = ComputedGetter<UseFormRules<T>>>( modelRef: M, rulesRef: R, options?: UseFormOptions ): UseFormResult<T, M, R> export function useForm<T extends UseFormModel, M extends Ref<T> = Ref<T>, R extends Ref<UseFormRules<T>> = Ref<UseFormRules<T>>>( modelRef: M, rulesRef: R, options?: UseFormOptions ): UseFormResult<T, M, R> export function useForm<T extends UseFormModel, M extends T = T>(modelRef: M, rulesRef?: null, options?: UseFormOptions): UseFormResult<T, M, Ref<UseFormRules<T>>> export function useForm<T extends UseFormModel, M extends Ref<T> = Ref<T>>(modelRef: M, rulesRef?: null, options?: UseFormOptions): UseFormResult<T, M, Ref<UseFormRules<T>>> export function useForm<T extends UseFormModel, R extends UseFormRules<T> = UseFormRules<T>>( modelRef: null | undefined, rulesRef: R, options?: UseFormOptions ): UseFormResult<T, Ref<T>, R> export function useForm<T extends UseFormModel, R extends Ref<UseFormRules<T>> = Ref<UseFormRules<T>>>( modelRef: null | undefined, rulesRef: R, options?: UseFormOptions ): UseFormResult<T, Ref<T>, R> export function useForm<T extends UseFormModel>(modelRef?: null, rulesRef?: null, options?: UseFormOptions): UseFormResult<T, Ref<T>, Ref<UseFormRules<T>>> export function useForm<T extends UseFormModel, M extends T | Ref<T> = T | Ref<T>, R extends UseFormRules<T> | Ref<UseFormRules<T>> = UseFormRules<T> | Ref<UseFormRules<T>>>( modelRef?: M, rulesRef?: R, options?: UseFormOptions ): UseFormResult<T, M, R> { options ??= {} const cloneDeep = options.cloneDeep ?? structuredClone const model = (modelRef ?? ref({})) as M const rules = (typeof rulesRef === 'function' ? computed(rulesRef) : rulesRef ?? ref({})) as UseFormResult<T, M, R>['rules'] const initialModel = ref(cloneDeep(toRaw(unref(model)))) as Ref<T> const normalizedRules = ref<Record<string, UseFormRuleItem[]>>({}) const deepRuleKeys = new Map<string, boolean>() /** normalized key cache */ const nkc: Record<string | number, string> = {} let watchHandles: Record<string, WatchHandle> = {} const _validateInfos: Record<string, UseFormValidateInfo> = reactive({}) const validateInfos = new Proxy(_validateInfos, { get(target, p, receiver) { if (typeof p === 'string' && !p.startsWith('__v_')) { const key = normalizeKey(p, nkc) if (!(key in target) && key.includes('.') && options!.deepRule && !deepRuleKeys.has(key)) { const deepRules = getRuleByDeepRule(key, normalizedRules.value) if (deepRules.length) { setValidateInfo(key, { required: isRequired(deepRules) }, true) deepRuleKeys.set(key, true) normalizedRules.value[key] = deepRules addListeners(key) } else { deepRuleKeys.set(key, false) } } return Reflect.get(target, key, receiver) } return Reflect.get(target, p, receiver) }, has(target, p) { return Reflect.has(target, p) }, }) const scope = effectScope() function addListeners(props: Arrayable<string>) { scope.run(() => { toArray(props).forEach((key) => { if (!watchHandles[key]) { watchHandles[key] = watch( () => get(unref(model), key), (value) => triggerValidate(key, value) ) } }) }) } function setValidateInfo(key: string, info: UseFormValidateInfo | null, isCreate?: boolean) { if (info) { isCreate ? (_validateInfos[key] = info) : Object.assign(_validateInfos[key], info) } else { delete _validateInfos[key] } } function obtainValidateFields(props?: Arrayable<UseFormItemPropertyKey>) { const nr = unref(normalizedRules) const fileds = toArray(props, true).map((key) => normalizeKey(key, nkc)) if (fileds.length) return fileds.filter((key) => key in nr) const keys = Object.keys(nr) return options!.deepRule ? keys.filter((key) => !deepRuleKeys.has(key)) : keys } async function triggerValidate(key: string, getResult?: GetResult): Promise<boolean> { const { value, exist, diff } = getResult || get(unref(model), key) if (!exist && (!options!.strick || diff > 1)) { setValidateInfo(key, { validateStatus: '', error: undefined }) if (options!.deepRule) { deepRuleKeys.forEach((valid, k) => { if (valid && key.startsWith(k) && key !== k) { setValidateInfo(k, { validateStatus: '', error: undefined }) } }) } return true } setValidateInfo(key, { validateStatus: 'validating', error: undefined }) const deepKeys: string[] = [] if (options!.deepRule) { deepRuleKeys.forEach((valid, k) => { if (valid && k.startsWith(key) && key !== k) { deepKeys.push(k) setValidateInfo(k, { validateStatus: 'validating', error: undefined }) } }) } const validator = new AsyncValidator({ [key]: normalizedRules.value[key] }) return validator .validate({ [key]: value }, { firstFields: true }) .then(() => { setValidateInfo(key, { validateStatus: 'success' }) deepKeys.forEach((k) => { setValidateInfo(k, { validateStatus: 'success' }) }) return true }) .catch((err) => { const { errors, fields } = err as UseFormValidateFailure if (!errors && !fields) { console.error(err) } setValidateInfo(key, key in fields ? { validateStatus: 'error', error: errors?.[0]?.message } : { validateStatus: 'success', error: '' }) deepKeys.forEach((k) => { if (k in fields) setValidateInfo(k, { validateStatus: 'error', error: fields[k][0].message }) else setValidateInfo(k, { validateStatus: 'success', error: undefined }) }) return Promise.reject(fields) }) } async function doValidateField(props?: Arrayable<UseFormItemPropertyKey>): Promise<boolean> { const fields = obtainValidateFields(props) if (fields.length === 0) return true const validationErrors: ValidateFieldsError = {} for (const field of fields) { try { await triggerValidate(field) } catch (fields) { Object.assign(validationErrors, fields) } } if (Object.keys(validationErrors).length === 0) return true return Promise.reject(validationErrors) } const validateField: UseFormResult<T, M, R>['validateField'] = async (props, callback) => { const shouldThrow = typeof callback !== 'function' try { const result = await doValidateField(props) // When result is false meaning that the fields are not validatable if (result === true) { await callback?.(result) } return result } catch (e) { if (e instanceof Error) throw e const invalidFields = e as ValidateFieldsError // if (props.scrollToError) { // scrollToField(Object.keys(invalidFields)[0]) // } await callback?.(false, invalidFields) return shouldThrow && Promise.reject(invalidFields) } } const validate: UseFormResult<T, M, R>['validate'] = (callback) => validateField(undefined, callback) const resetFields: UseFormResult<T, M, R>['resetFields'] = (newModel) => { Object.assign(unref(model), unref(initialModel), newModel) nextTick(clearValidate) } const clearValidate: UseFormResult<T, M, R>['clearValidate'] = (props) => { const fields = obtainValidateFields(props) fields.forEach((key) => { setValidateInfo(key, { validateStatus: '', error: undefined }) }) deepRuleKeys.forEach((valid, key) => { if (valid && fields.some((f) => key.startsWith(f) && key !== f)) { setValidateInfo(key, { validateStatus: '', error: undefined }) } }) } function clearDeepInfo(key: string, strick?: boolean) { const { exist, diff } = get(unref(model), key) if (!exist && (strick || diff)) { setValidateInfo(key, null) deepRuleKeys.delete(key) watchHandles[key]() delete watchHandles[key] delete normalizedRules.value[key] } } const clearDeepInfos: UseFormResult<T, M, R>['clearDeepInfos'] = (props, strick) => { if (!options!.deepRule) return const keys = toArray(props) if (keys.length) { keys.forEach((key) => { key = normalizeKey(key, nkc) if (deepRuleKeys.get(key)) { clearDeepInfo(key, strick) } }) } else { deepRuleKeys.forEach((valid, key) => { valid && clearDeepInfo(key, strick) }) } } let isFirst = true function updateRules() { deepRuleKeys.clear() const newRules: Record<string, UseFormRuleItem[]> = (normalizedRules.value = {}) const oldHandles = watchHandles watchHandles = {} const listenKeys: string[] = [] Object.entries(unref(rules)).forEach(([key, rule]) => { key = normalizeKey(key, nkc) newRules[key] = toArray(rule as UseFormRuleItem, true) let isCreate = false if (key in oldHandles) { watchHandles[key] = oldHandles[key] } else { isCreate = true listenKeys.push(key) } setValidateInfo(key, { required: isRequired(newRules[key]) }, isCreate) }) Object.keys(oldHandles).forEach((key) => { if (!(key in watchHandles)) { oldHandles[key]() setValidateInfo(key, null) } }) addListeners(listenKeys) if (!isFirst && options!.validateOnRuleChange) { validateField() } isFirst = false } scope.run(() => { watch(rules, updateRules, { immediate: true, deep: true }) }) return { model, rules, initialModel, validateInfos, validateField, validate, resetFields, clearValidate, clearDeepInfos } }