我们一般在后台系统中,很常见的操作时表格里面嵌套表单,之前我的网上找到了一些封装的用法:
<el-form :model="formData" :rules="ruleData" ref="formDom">
<el-table :data="formData.tableData">
<el-table-column
v-for="item in column"
:key="item.prop"
:label="item.label"
>
<template slot-scope="scope">
<el-form-item
:ref="'tableData.' + scope.$index + '.' + item.prop"
:prop="'tableData.' + scope.$index + '.' + item.prop"
:rules="ruleData[item.prop]"
>
<el-input
v-model="scope.row[item.prop]"
@change="handleChange(scope, item)"
></el-input>
</el-form-item>
</template>
</el-table-column>
</el-table>
</el-form>
// data中的数据
formData: {
tableData: [
{ name: "", age: "" },
{ name: "", age: "" },
{ name: "", age: "" },
{ name: "", age: "" },
],
},
ruleData: {
name: { message: "请输入名字", required: true },
age: { message: "请输入年龄", required: true },
},
column: [
{ label: "名字", prop: "name" },
{ label: "年龄", prop: "age" },
],
在这里我不太理解prop为什么要写成"'tableData.' + scope.$index + '.' + item.prop"
这个样子就实现了校验的效果。后来在一次项目中偶然去查看了一下el-form
的源码,才明白其中的道理;
首先我们看到el-form
组件的文件路径是这样的,那么其实form-item
是一个单独的组件,我们通过form -> form-item嵌套的时候,其实最后实现校验的过程是一个个去校验 form-item,form组件最终的校验:
// 最主要的核心功能
validate(callback){
this.fields.forEach(field => {
field.validate('', (message, field) => {
if (message) {
valid = false;
}
invalidFields = objectAssign({}, invalidFields, field);
if (typeof callback === 'function' && ++count === this.fields.length) {
callback(valid, invalidFields);
}
});
});
}
其中 this.fields就是el-form-item
集合:
// el-form的created中
this.$on('el.form.addField', (field) => {
if (field) {
this.fields.push(field);
}
});
// el-form-item的mounted中
this.dispatch('ElForm', 'el.form.addField', [this]);
// 内部自己使用 dispatch实现组件通讯
其中最主要的就是调用el-form-item
的validate
方法:
validate(trigger, callback = noop) {
this.validateDisabled = false;
const rules = this.getFilteredRule(trigger);
if ((!rules || rules.length === 0) && this.required === undefined) {
callback();
return true;
}
this.validateState = 'validating';
const descriptor = {};
if (rules && rules.length > 0) {
rules.forEach(rule => {
delete rule.trigger;
});
}
descriptor[this.prop] = rules;
const validator = new AsyncValidator(descriptor);
const model = {};
model[this.prop] = this.fieldValue;
validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
callback(this.validateMessage, invalidFields);
this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
});
}
其中主要的核心功能是单个的 rules(校验规则)
和model(数据)
,
rules的获取:
// 首先通过getFilteredRule方法过滤rules
getFilteredRule(trigger) {
// 获取 rules
const rules = this.getRules();
return rules.filter(rule => {
if (!rule.trigger || trigger === '') return true;
if (Array.isArray(rule.trigger)) {
return rule.trigger.indexOf(trigger) > -1;
} else {
return rule.trigger === trigger;
}
}).map(rule => objectAssign({}, rule));
},
// getFilteredRule最核心的方式就是getRules
getRules() {
// 这个就是我们在 el-form中传递的rules
let formRules = this.form.rules;
// 这个就是我们自己在 el-form-item传递的rules
const selfRules = this.rules;
// 这里判断是不是必输的
const requiredRule = this.required !== undefined ? { required: !!this.required } : [];
// 这里通过formRules结合prop获取最新的prop(其实是一个对象,里面有key,value比较重要的值)
const prop = getPropByPath(formRules, this.prop || '');
formRules = formRules ? (prop.o[this.prop || ''] || prop.v) : [];
// 最终将rules做一个整合
return [].concat(selfRules || formRules || []).concat(requiredRule);
},
最终我们通过分析发现其实去匹配 el-form
中整体的rules是通过getPropByPath
这个方法的:
function getPropByPath(obj, path) {
let tempObj = obj;
path = path.replace(/\[(\w+)\]/g, '.$1');
path = path.replace(/^\./, '');
let keyArr = path.split('.');
let i = 0;
for (let len = keyArr.length; i < len - 1; ++i) {
let key = keyArr[i];
if (key in tempObj) {
tempObj = tempObj[key];
} else {
throw new Error('please transfer a valid prop path to form item!');
}
}
return {
o: tempObj,
k: keyArr[i],
v: tempObj[keyArr[i]]
};
}
path.split('.')
这里会对prop进行切割,我们最终得到的值其实是这样的:
最终通过formRules = formRules ? (prop.o[this.prop || ''] || prop.v) : [];
将formRules
这个值变成了undefined
,
再返回validate
方法里面,看model
,通过这里model[this.prop] = this.fieldValue
,我们来看fieldValue
:
function getPropByPath(obj, path) {
// 这里的值其实一个对象数组:
// { tableData: [
// { name: "", age: "" },
// { name: "", age: "" },
// { name: "", age: "" },
// { name: "", age: "" },
// ] },
let tempObj = obj;
path = path.replace(/\[(\w+)\]/g, '.$1');
path = path.replace(/^\./, '');
// 那第一个来说就是 tableData.0.name
let keyArr = path.split('.');
let i = 0;
for (let len = keyArr.length; i < len - 1; ++i) {
// 不断的去匹配 tempObj 中的值
let key = keyArr[i];
if (key in tempObj) {
tempObj = tempObj[key];
} else {
throw new Error('please transfer a valid prop path to form item!');
}
}
return {
o: tempObj,
k: keyArr[i],
v: tempObj[keyArr[i]]
};
}
// 其实就是一个计算属性
fieldValue() {
// 我们给 el-form传递的model值
const model = this.form.model;
if (!model || !this.prop) { return; }
let path = this.prop;
if (path.indexOf(':') !== -1) {
path = path.replace(/:/, '.');
}
// 还是通过这个方法获取到对应的值
return getPropByPath(model, path, true).v;
},
最终fieldValue
就表示了表格组件中每个小块的值,通过 prop
和model
去匹配对应的值,和rules
结合AsyncValidator
实现表单校验。这里我们也可以看出给el-form
传递model
,给el-form-item
传递prop
,rules
的重要性。
写到这里我还有一个疑问,那就是在getRules
方法中,通过getPropByPath
其实最终是把formRules
转换成一个undefined
的,那么是不是在此次封装中给el-form
传递的rules
没用了,其实真正起主导作用的还是给el-form-item
传递的rules
,那么把el-form-item
中的rules
删除掉是不是就校验不了了。通过验证证实了我的猜想。