UNPKG

element-plus-useform

Version:

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

291 lines 10.9 kB
import AsyncValidator, {} from 'async-validator'; import { computed, effectScope, nextTick, reactive, ref, toRaw, unref, watch, } from 'vue'; export function toArray(value, clone) { if (Array.isArray(value)) return clone ? value.slice() : value; if (value === undefined || value === null) return []; return [value]; } export function normalizeKey(key, nkc) { 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, key) { const keys = key.split('.'); let value = 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) { return toArray(rule).some((r) => r.required); } export function isDeepRule(rule) { return (rule.type === 'object' || rule.type === 'array') && !!(rule.fields || rule.defaultField); } function getRuleByDeepRule(key, ruleSource) { const keys = key.split('.'); const rules = []; for (let i = 0; i < keys.length; i++) { let j = i + 1; let deepRules = toArray(ruleSource[keys.slice(0, j).join('.')]); while (deepRules.length && j < keys.length) { const currentKey = keys[j]; const currentRuels = []; 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(modelRef, rulesRef, options) { options ??= {}; const cloneDeep = options.cloneDeep ?? structuredClone; const model = (modelRef ?? ref({})); const rules = (typeof rulesRef === 'function' ? computed(rulesRef) : rulesRef ?? ref({})); const initialModel = ref(cloneDeep(toRaw(unref(model)))); const normalizedRules = ref({}); const deepRuleKeys = new Map(); /** normalized key cache */ const nkc = {}; let watchHandles = {}; const _validateInfos = 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) { scope.run(() => { toArray(props).forEach((key) => { if (!watchHandles[key]) { watchHandles[key] = watch(() => get(unref(model), key), (value) => triggerValidate(key, value)); } }); }); } function setValidateInfo(key, info, isCreate) { if (info) { isCreate ? (_validateInfos[key] = info) : Object.assign(_validateInfos[key], info); } else { delete _validateInfos[key]; } } function obtainValidateFields(props) { 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, getResult) { 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 = []; 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; 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) { const fields = obtainValidateFields(props); if (fields.length === 0) return true; const validationErrors = {}; 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 = 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; // if (props.scrollToError) { // scrollToField(Object.keys(invalidFields)[0]) // } await callback?.(false, invalidFields); return shouldThrow && Promise.reject(invalidFields); } }; const validate = (callback) => validateField(undefined, callback); const resetFields = (newModel) => { Object.assign(unref(model), unref(initialModel), newModel); nextTick(clearValidate); }; const 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, strick) { 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 = (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 = (normalizedRules.value = {}); const oldHandles = watchHandles; watchHandles = {}; const listenKeys = []; Object.entries(unref(rules)).forEach(([key, rule]) => { key = normalizeKey(key, nkc); newRules[key] = toArray(rule, 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 }; } //# sourceMappingURL=index.js.map