上一章我们讲了element的图标,按钮,链接,这章咱讲表单
表单
表单的代码量就稍微多了一点,我会一步步解析它
Form
首先是Form组件的created方法,它会绑定两个事件,一个是添加field,一个是删除field
this.$on('el.form.addField', (field) => {
if (field) {
this.fields.push(field);
}
});
/* istanbul ignore next */
this.$on('el.form.removeField', (field) => {
if (field.prop) {
this.fields.splice(this.fields.indexOf(field), 1);
}
});
那么这个field是什么呢,其实它是通过插槽嵌入的子组件FormItem,当FormItem执行mounted回调时,会将FormItem实例也就是this通过触发事件进行保存起来
Form组件主要的四个方法validate校验规则,validateField校验部分规则,resetFields重置校验和数据,clearValidate重置校验
四个方法都是遍历子组件FormItem(也就是field)并触发它的逻辑进行处理,validateField和clearValidate通过fields.filter(field => ~props.indexOf(field.prop))过滤出相关的FormItem选项
FormItem
mounted() {
if (this.prop) {
this.dispatch('ElForm', 'el.form.addField', [this]);
let initialValue = this.fieldValue;
if (Array.isArray(initialValue)) {
initialValue = [].concat(initialValue);
}
Object.defineProperty(this, 'initialValue', {
value: initialValue
});
this.addValidateEvents();
}
},
beforeDestroy() {
this.dispatch('ElForm', 'el.form.removeField', [this]);
}
FormItem执行dispatch方法,这个方法是通过mixin混入进去的,也是通过比较有意思的方法去触发父组件Form
methods: {
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
}
}
通过while向上查找,如果存在name值为ElForm的组件也就是Form组件,就执行它绑定的添加删除field的事件
把当前FormItem实例存入Form中后,fieldValue会以initialValue为键添加到实例上,因为是通过Object.defineProperty定义的默认不可更改,通过prop属性获取到Form的model属性所对应的prop,再通过计算属性监听数据的变化,对于prop的写法可以是xxx或xxx:xxx或xxx.xxx
fieldValue的值通过getPropByPath进行了处理,它对path进行了转换
/\[(\w+)\]/g => '.$1'
/^\./ => ''
// getPropByPath返回值
return {
o: tempObj, // prop指向的值所处的对象
k: keyArr[i], // prop指向的键
v: tempObj ? tempObj[keyArr[i]] : null // prop指向的值
};
当处理完之后通过split('.')将path分割,获取model的子元素,像path='input.el.text'对应的就是model.input.el.text,所以fieldValue返回的就是相应的值
fieldValue() {
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;
}
mounted继续执行this.addValidateEvents(),这个函数会判断是否传入了rules/required或者父组件是否有rules,优先级:FormItem的rules > Form的rules
如果满足其中一个条件,就会添加两个事件来监听输入框,单选框等表单元素的变化
addValidateEvents() {
const rules = this.getRules(); // 获取表单规则
if (rules.length || this.required !== undefined) {
this.$on('el.form.blur', this.onFieldBlur);
this.$on('el.form.change', this.onFieldChange);
}
}
当子组件如Input,Radio失去焦点或者改变时,触发这些事件
触发这些事件之后,会执行this.validate('blur' || 'change'),validate根据第一个参数过滤rules,getFilteredRule函数根据trigger也就是'blur' || 'change'过滤
可以看出,trigger支持字符串和数组两种写法,过滤后的rules在进行浅拷贝,objectAssign是Object.assign的polyfill
getFilteredRule(trigger) {
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));
}
将过滤后的rules存入到一个对象中,descriptor = { [this.prop]: rules },element使用了async-validator这个库,他接受一个规则对象,也就是rules,通过它生成校验器去校验数据,调用validate函数,如果不符合规则,会返回报错信息,如果上层组件有Form就执行Form绑定的validate方法,可惜我没看到这个方法,如果想触发单个FormItem进行校验可以使用这个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);
});
}
clearValidate用来清理状态,也非常简单
clearValidate() {
this.validateState = '';
this.validateMessage = '';
this.validateDisabled = false;
}
resetField通过getPropByPath函数获取相关对象和键,通过obj[key] = this.initialValue还原初始值
其中TimeSelect组件比较特殊,需要触发该组件的fieldReset事件,这个以后有机会再说
LabelWrap
这个组件是FormItem的label,它是通过render实现的,获取父组件Form的autoLabelWidth,也就是左外边距,通过getComputedStyle(this.$el.firstElementChild).不断去计算文本宽度,对label进行自适应
render() {
const slots = this.$slots.default;
if (!slots) return null;
if (this.isAutoWidth) {
const autoLabelWidth = this.elForm.autoLabelWidth;
const style = {};
if (autoLabelWidth && autoLabelWidth !== 'auto') {
const marginLeft = parseInt(autoLabelWidth, 10) - this.computedWidth;
if (marginLeft) {
style.marginLeft = marginLeft + 'px';
}
}
return (<div class="el-form-item__label-wrap" style={style}>
{ slots }
</div>);
} else {
return slots[0];
}
}
累了,就写到这里吧