element-plus-useform
Version:
element-plus useForm hook,使表单验证脱离组件实例
291 lines • 10.9 kB
JavaScript
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