Form
组件是 Element-ui
中的一个重要的组件,用于创建和管理表单,本文将从 Form
组件的构成和 Form
组件的实现逻辑来分析源代码。
一. Form 组件的构成
先来看一下 Form
组件的构成,在 element-ui
源代码中,Form
组件包含三个部分:
- 表单整体
Form
。 - 表单项
Form-Item
。 - 表单项前缀
lable-wrap
。
html
------ Form
|------ form.vue
|------ form-item.vue
|------ lable-wrap.vue
Form 组件的结构分析:


二. Form 组件的实现逻辑
将 Form
组件提供的功能进行拆分,主要可以拆分成如下几部分功能:
- 基础表单域的展示。
- 表单域内数据的重置功能。
- 表单域标签的位置、宽度以及后缀可设置。
- 表单域的验证功能。
- 一些其他的功能:可以设置为行内表单、控件尺寸可控制、控件的启用/禁用可控制。
以上是拆分的 Form
组件的主要功能,接下来围绕每一个功能的代码实现逻辑来分析 Form
组件。
2.1 基础表单域
基础表单域实现的重点逻辑在于整理表单项集合:即在表单项挂载时,将表单项加入集合中,在表单项销毁时,将表单项从集合中去除。
- 建立
Form
、Form-Item
和Label-wrap
组件。 - 通过
Form
组件来整合每一个表单项字段的集合,在Form
组件中设置fields
变量,来存储每一个表单项。 - 在
Form-Item
组件中的mounted
钩子里面,触发Form
组件的自定义方法addField
,并且将Form-Item
组件的this
作为参数传入。在Form
组件监听addField
方法,方法中将传入的this
全部推入fields
数组变量中。 - 当
Form-Item
组件触发beforeDestroy
钩子时,触发Form
组件的自定义方法removeField
,并且将Form-Item
组件的this
作为参数传入。在Form
组件监听removeField
方法,将被销毁的组件对应的字段删除。
Form 组件:
- 首先是以插槽
slot
的形式,接受组件内部传入的表单项。 - 然后在
created
钩子中,监听addField
和removeField
两个自定义方法,整理表单项。
html
<template>
<form class="el-form">
<slot></slot>
</form>
</template>
<script>
export default {
name: 'ElForm',
componentName: 'ElForm',
props: {
model: Object, // 表单数据对象
},
data() {
return {
fields: [], // 设置 fields 变量用于存储表单项
}
},
created() {
// 监听 el.form.addField
this.$on('el.form.addField', (field) => {
if (field) {
// 将每一个表单项都推入到 fields 数组中
this.fields.push(field);
}
})
// 监听 el.form.removeField
this.$on('el.form.removeField', (field) => {
if (field.prop) {
// 找到对应的字段从 fields 数组中删除
this.fields.splice(this.fields.indexOf(field), 1);
}
});
}
}
</script>
Form-Item 组件:
Form-Item
组件包含两部分:表单项的前缀和表单项的控件。表单项的前缀,也就是表单项的中文描述,这部分单独提出了一个组件label-wrap
。表单项的控件,也就是Input
、Select
等控件,这部分采用插槽slot
的形式去接收传入Form-Item
组件的控件。Form-Item
组件在mounted
钩子中需要触发Form
组件的addField
方法,把字段添加到Form
组件的fields
数组中。在beforeDestroy
钩子中需要触发Form
组件的removeField
方法,将字段从fields
数组中删除。
html
<template>
<div class="el-form-item">
<!-- label-wrap 组件 -->
<label-wrap>
<label class="el-form-item__label" v-if="label || $slots.label">
<slot name="label">{{label}}</slot>
</label>
</label-wrap>
<!-- 表单项控件 -->
<div class="el-form-item__content">
<slot></slot>
</div>
</div>
</template>
<script>
import LabelWrap from './label-wrap';
import emitter from '../../utils/mixins/emitter';
export default {
name: 'ElFormItem',
componentName: 'ElFormItem',
mixins: [emitter],
props: {
label: String,
prop: String,
},
components: {
LabelWrap
},
mounted() {
if (this.prop) {
// 触发父组件的自定义方法 addField,添加表单项
this.dispatch('ElForm', 'el.form.addField', [this]);
}
},
beforeDestroy() {
// 触发 Form 组件的 el.form.removeField 方法,在组件销毁时删除表单项
this.dispatch('ElForm', 'el.form.removeField', [this]);
}
}
</script>
在 Form-Item
组件中,关于 label
的展现处理如下:
html
<label-wrap>
<label class="el-form-item__label" v-if="label || $slots.label">
<slot name="label">{{label}}</slot>
</label>
</label-wrap>
在 label-wrap
组件中传入 label
标签,判断如果传入了 label
,或者传入了名称是 label
的具名插槽,则展示 label
标签。
在 label
标签的内部接收父组件传过来的插槽内容,如果父组件没有传过来名为 label
的插槽内容,则展示 slot
标签内部的后备内容,也就是 label
属性传入的内容。
从这段代码可以看出,如果是 label 属性和插槽模板同时传值的情况下,插槽的优先级较高。
Lable-wrap 组件:
在 label-wrap
组件内部采用 render()
函数来构建 DOM。
使用 render()
函数是因为 render()
函数更加灵活,可以处理更复杂的渲染逻辑,由于 label
支持很多属性设置(比如说 label-width
设置为 auto
的自动撑开宽度功能),需要根据设置渲染不同的标签,所以使用 render()
函数来更加灵活的渲染。
js
<script>
export default {
render() {
const slots = this.$slots.default;
if (!slots) return null;
// render 函数来返回虚拟DOM节点
return slots[0];
}
}
</script>
2.2 表单域的重置功能
表单域内数据的重置功能是指在点击重置按钮时,将表单项的值重置为初始设置的值。
- 需要根据
Form
组件传入的model
以及Form-Item
组件的prop
,查询到Form-Item
表单项的初始值,并且设置变量记录初始值。 Form-Item
组件提供resetField
方法,该方法将model
中对应表单项的prop
的值设置为初始值。Form
组件提供resetFields
方法,该方法循环Form-Item
,调用其resetField
方法。
Form-Item 组件:
- 通过循环向上查找,获取父级的表单组件。
- 获取组件的初始值,在
mounted
钩子中,用initialValue
变量记录组件的初始值。 - 提供
resetField
方法,在该方法中,将Form-Item
对应的值重置为初始值。
js
computed: {
// 获取表单组件
form() {
// 获取该组件的父级
let parent = this.$parent;
let parentName = parent.$options.componentName;
// 如果父级不是 ElForm 则继续向上获取,直到获取到 ElForm
while (parentName !== 'ElForm') {
parent = parent.$parent;
parentName = parent.$options.componentName;
}
// 返回父级
return parent;
},
// 获取组件的 value 值
fieldValue() {
// 获取到 form 组建的 model 属性
const model = this.form.model;
// 如果 model 或者 prop 任意一个不存在,则直接 return
if (!model || !this.prop) { return; }
// 将 prop 属性的值作为在 model 对象寻找值的 path
let path = this.prop;
// 如果 prop 属性的值存在:则替换成.(作为属性下的属性)
if (path.indexOf(':') !== -1) {
path = path.replace(/:/, '.');
}
// 在 model 下寻找对应属性的值
return getPropByPath(model, path, true).v;
},
},
mounted() {
if (this.prop) {
...
// 获取表单项的值作为初始值
let initialValue = this.fieldValue;
// 兼容数组类型
if (Array.isArray(initialValue)) {
initialValue = [].concat(initialValue);
}
// 将初始值绑定在 this 上
Object.defineProperty(this, 'initialValue', {
value: initialValue
});
}
},
methods: {
resetField() {
let model = this.form.model;
let value = this.fieldValue;
let path = this.prop;
if (path.indexOf(':') !== -1) {
path = path.replace(/:/, '.');
}
// 通过 getPropByPath 方法获取到表单项属性所在对象、属性的 key、以及属性的值
let prop = getPropByPath(model, path, true);
// 将属性的值重置为初始值
if (Array.isArray(value)) {
prop.o[prop.k] = [].concat(this.initialValue);
} else {
prop.o[prop.k] = this.initialValue;
}
},
},
getPropByPath 方法:
该方法根据传入的对象和路径,返回三个值:属性所在对象、属性的 key 和属性的值。
js
export function getPropByPath(obj, path, strict) {
let tempObj = obj;
// 将中括号属性转换为 .
path = path.replace(/\[(\w+)\]/g, '.$1');
// 将属性前面的 . 去掉 比如:.into.name 转换为 into.name
path = path.replace(/^\./, '');
// 用 . 来分割
let keyArr = path.split('.');
// 一层一层的来找到对应的属性
let i = 0;
for (let len = keyArr.length; i < len - 1; ++i) {
if (!tempObj && !strict) break;
let key = keyArr[i];
if (key in tempObj) {
tempObj = tempObj[key];
} else {
if (strict) {
throw new Error('please transfer a valid prop path to form item!');
}
break;
}
}
return {
o: tempObj, // 属性所在对象
k: keyArr[i], // 属性的 key
v: tempObj ? tempObj[keyArr[i]] : null // 属性的值
};
};
Form 组件:
提供 resetFields
方法,该方法循环 Form-Item
,调用其 resetField
方法。
js
methods: {
resetFields() {
// 循环表单项,依次调用每一个表单项的 resetField 方法
this.fields.forEach(field => {
field.resetField();
});
},
}
2.3 表单项标签宽度设置
表单项标签宽度是指表单项的前缀中文描述的宽度,设置标签宽度区分两种情况:
- 设置一个具体的宽度,所有表单项的宽度都遵循那个具体的宽度。
- 设置为
auto
,表单项的宽度自适应,不会折行。
2.3.1 label-width 设置为具体宽度
当 label-width
被设置为具体的宽度数值时,需要将 label
的宽度设置为这个数值,并且将 label
后面表单项控件的 margin-left
设置为 label-width
的值。
相关代码:
js
computed: {
// lablel 的样式
labelStyle() {
// 设置 ret 变量为一个空对象
const ret = {};
// 如果 labelPosition 设置为 top 则直接返回
if (this.form.labelPosition === 'top') return ret;
// 如果设置了 label-width 优先取 form-item 的,再取 form 的
const labelWidth = this.labelWidth || this.form.labelWidth;
// 如果 labelWidth 存在,返回设置的 labelWidth
if (labelWidth) {
ret.width = labelWidth;
}
return ret;
},
// 包裹控件的 div 的样式
contentStyle() {
const ret = {};
const label = this.label;
// 如果 labelPosition 设置为 top,或者是行内表单,则直接返回
if (this.form.labelPosition === 'top' || this.form.inline) return ret;
// 如果不存在 label 属性,也不存在 label-width 属性,则直接返回
if (!label && !this.labelWidth) return ret;
const labelWidth = this.labelWidth || this.form.labelWidth;
// 把右侧控件的 margin-left 设置为 labelWidth 的值
ret.marginLeft = labelWidth;
return ret;
},
}
Template
上面绑定 labelStyle
和 contentStyle
:
html
<div class="el-form-item">
<!-- label-wrap 组件 -->
<label-wrap>
<label
...
:style="labelStyle"
>
<slot name="label">{{label}}</slot>
</label>
</label-wrap>
<!-- 表单项控件 -->
<div ... :style="contentStyle">
<slot></slot>
</div>
</div>
2.3.2 label-width 设置为 auto
当 label-width
被设置为 auto
时,表示根据 label
的文字宽度自适应,将 lable
中的文字保持在一行上面,根据最大的宽度作为 lable
的宽度。
实现步骤:
- 先去计算每一个
Form-Item
设置为auto
时需要占用的宽度。 - 再去将每一个
Form-Item
占用的宽度在Form
组件中记录下来,获取其中的最大值作为整个表单的label
宽度。 - 用计算出的
label
的最大宽度,去给包裹控件的div
赋值margin-left
,从而预留出label
的宽度。 - 如果
label-position
设置为right
,则需要用最大的label
宽度减去该项label
本身占的宽度,作为label
的margin-left
,将label
推向right
方向。 - 处理销毁逻辑,如果
Form-Item
组件被销毁,相应的label-wrap
组件也会被销毁,此时需要重新计算宽度。
相关代码:
第一步:先去计算每一个 Form-Item 设置为 auto 时需要占用的宽度
(1)label-wrap
组件传入属性 is-auto-width
和属性 update-all
,用于判断设置的 label-width
是否设置为 auto
。
Form-Item 组件:
html
<!-- Form-Item 组件 -->
<label-wrap
:is-auto-width="labelStyle && labelStyle.width === 'auto'"
:update-all="form.labelWidth === 'auto'"
>
...
</label-wrap>
label-wrap 组件:
js
// label-wrap 组件
props: {
isAutoWidth: Boolean,
updateAll: Boolean
},
(2)label-wrap
组件内部计算设置为 auto
所需要的宽度
给元素增加 div
包裹:
js
// label-wrap 组件
render() {
const slots = this.$slots.default;
if (!slots) return null;
if (this.isAutoWidth) {
// 如果 width 是 auto,则用一个 div 包裹,用于计算这个包裹元素的宽度
return (<div class="el-form-item__label-wrap">
{ slots }
</div>);
} else {
return slots[0];
}
},
计算 label
的宽度:
js
// label-wrap 组件
methods: {
// 获取元素的宽度
getLabelWidth() {
if (this.$el && this.$el.firstElementChild) {
// 使用 window.getComputedStyle 获取元素的宽度
const computedWidth = window.getComputedStyle(this.$el.firstElementChild).width;
// 使用 Math.ceil 将获取到的宽度向上取整
return Math.ceil(parseFloat(computedWidth));
} else {
return 0;
}
},
// 更新元素的宽度
updateLabelWidth(action = 'update') {
// 如果宽度设置的是 auto,并且获取到了组件根元素的第一个子元素
if (this.$slots.default && this.isAutoWidth && this.$el.firstElementChild) {
// action 是 update 去获取 label 的宽度
if (action === 'update') {
this.computedWidth = this.getLabelWidth();
}
}
}
},
data() {
return {
computedWidth: 0 // label 的实时宽度计算
};
},
mounted() {
// 在 mounted 钩子里面获取 label 的宽度
this.updateLabelWidth('update');
},
updated() {
// 在组件发生更新之后的 updated 钩子里面获取 label 的宽度
this.updateLabelWidth('update');
},
(3)计算后触发 Form-Item
组件的控件外包裹 div
的 margin-left
的更新
在 Form-Item
组件使用 provide
提供数据:
js
// Form-Item 组件
provide() {
return {
elFormItem: this
};
},
在 label-wrap
组件中使用 inject
接收数据,并且监听 computedWidth
的变化,如果变化了触发 Form-Item
组件的方法,去重新给控件包裹 div
赋值 margin-left
。
label-wrap
组件,监听 computedWidth
的变化,如果发生了变化就去触发 Form-Item
的 label
宽度的更新:
js
// label-wrap 组件
inject: ['elFormItem'],
watch: {
computedWidth(val) {
if (this.updateAll) {
this.elFormItem.updateComputedLabelWidth(val);
}
}
},
Form-Item
组件,触发 Form-Item
组件的方法,去重新给控件包裹 div
赋值 margin-left
:
js
// Form-Item 组件
data() {
return {
computedLabelWidth: '' // 计算的 label 宽度
}
},
methods: {
// 更新 label 为 auto 时,计算出来的 label 宽度
updateComputedLabelWidth(width) {
// 调用 Form-Item 组件的 updateComputedLabelWidth 去更新当前 label 的宽度
this.computedLabelWidth = width ? `${width}px` : '';
},
},
computed: {
contentStyle() {
const ret = {};
const label = this.label;
// 如果 labelPosition 设置为 top,或者是行内表单,则直接返回
if (this.form.labelPosition === 'top' || this.form.inline) return ret;
// 如果不存在 label 属性,也不存在 label-width 属性,则直接返回
if (!label && !this.labelWidth) return ret;
const labelWidth = this.labelWidth || this.form.labelWidth;
// 如果设置的宽度包含 auto
if (labelWidth === 'auto') {
if (this.labelWidth === 'auto') {
// 如果是表单项设置的 auto 宽度,则将 margin-left 设置为计算的表单项的 label 宽度
ret.marginLeft = this.computedLabelWidth;
}
} else {
// 如果设置的宽度均为固定宽度,则把右侧控件的 margin-left 设置为 labelWidth 的值
ret.marginLeft = labelWidth;
}
return ret;
},
}
第二步:再去将每一个 Form-Item 占用的宽度在 Form 组件中记录下来,获取其中的最大值作为整个表单的 label 宽度
(1)在 label-wrap
组件中的监听 computedWidth
变化的方法中,调用 Form
组件的方法,用于记录 label
的宽度。
Form
组件增加 provide
:
js
provide() {
return {
elForm: this
};
},
label-wrap
增加调用 Form
组件的方法:
js
// label-wrap 组件
inject: ['elForm' ...],
watch: {
computedWidth(val, oldVal) {
if (this.updateAll) {
// 调用 Form 组件的 registerLabelWidth 去记录每一个 label 的宽度
this.elForm.registerLabelWidth(val, oldVal);
...
}
}
},
(2)Form
组件设置数组 potentialLabelWidthArr
,用于记录所有 label
的宽度,并在其中找到最大值。
Form 组件:
js
// Form 组件
data() {
return {
...
potentialLabelWidthArr: [], // 设置 potentialLabelWidthArr 用于存储所有计算的 label 的 width
}
},
computed: {
autoLabelWidth() {
if (!this.potentialLabelWidthArr.length) return 0;
// 找到 potentialLabelWidthArr 数组中的最大值
const max = Math.max(...this.potentialLabelWidthArr);
return max ? `${max}px` : '';
}
},
methods: {
...
// 找到对应 label-width 的索引
getLabelWidthIndex(width) {
const index = this.potentialLabelWidthArr.indexOf(width);
if (index === -1) {
throw new Error('[ElementForm]unpected width ', width);
}
return index;
},
// 在数组 potentialLabelWidthArr 中存储 label 的 width
registerLabelWidth(val, oldVal) {
if (val && oldVal) {
// 更新宽度,找到旧宽度的索引,将新宽度更新上去
const index = this.getLabelWidthIndex(oldVal);
this.potentialLabelWidthArr.splice(index, 1, val);
} else if (val) {
// 新增宽度,直接 push 到 potentialLabelWidthArr 数组中
this.potentialLabelWidthArr.push(val);
}
},
}
第三步:用计算出的 label 的最大宽度,去给包裹控件的 div 赋值 margin-left,从而预留出 label 的宽度
js
// Form-Item 组件
computed: {
contentStyle() {
const ret = {};
const label = this.label;
// 如果 labelPosition 设置为 top,或者是行内表单,则直接返回
if (this.form.labelPosition === 'top' || this.form.inline) return ret;
// 如果不存在 label 属性,也不存在 label-width 属性,则直接返回
if (!label && !this.labelWidth) return ret;
const labelWidth = this.labelWidth || this.form.labelWidth;
// 如果设置的宽度包含 auto
if (labelWidth === 'auto') {
if (this.labelWidth === 'auto') {
// 如果是表单项设置的 auto 宽度,则将 margin-left 设置为计算的表单项的 label 宽度
ret.marginLeft = this.computedLabelWidth;
} else if (this.form.labelWidth === 'auto') {
// 如果 Form 组件的 label-width 是 auto,则将计算的所有 Form-Item 的最大值赋值给 margin-left
ret.marginLeft = this.elForm.autoLabelWidth;
}
} else {
// 如果设置的宽度均为固定宽度,则把右侧控件的 margin-left 设置为 labelWidth 的值
ret.marginLeft = labelWidth;
}
return ret;
},
...
第四步:如果 label-position 设置为 right,则需要用最大的 label 宽度减去该项 label 本身占的宽度,作为 label 的 margin-left,将 label 推向 right 方向
在 label-wrap
组件中,计算其 margin-left
的值:
js
render() {
const slots = this.$slots.default;
if (!slots) return null;
if (this.isAutoWidth) {
// 获取 Form 组件的 autoLabelWidth
const autoLabelWidth = this.elForm.autoLabelWidth;
const style = {};
// 如果 form 组件计算出了 autoLabelWidth,并且其 label-position 设置的不是 left,则计算其 margin-left 用于右对齐
if (autoLabelWidth && this.elForm.labelPosition !== 'left') {
// 用 Form 组件计算出的最大的 label 宽度,减去该项 label 本身占的宽度,取整后则为 label 的 margin-left
const marginLeft = parseInt(autoLabelWidth, 10) - this.computedWidth;
if (marginLeft) {
style.marginLeft = marginLeft + 'px';
}
}
// 如果 width 是 auto,则用一个 div 包裹,用于计算这个包裹元素的宽度
return (<div class="el-form-item__label-wrap" style={style}>
{ slots }
</div>);
} else {
return slots[0];
}
},
这里与源代码有所不同,源代码在设置了 label-width
为 auto
时,没有 this.elForm.labelPosition !== 'left'
的判断,导致如果设置了 label-position="left"
,也不会生效,在这里修改了一下,增加了 this.elForm.labelPosition !== 'left'
的判断。
第五步:处理 label-wrap 组件销毁逻辑,在 label-wrap 组件销毁时,清除 potentialLabelWidthArr 数组中对应的值
label-wrap
组件,在 beforeDestroy
钩子中,触发 Form
组件的删除 potentialLabelWidthArr
数组中该 label
宽度的方法:
js
// label-wrap 组件
beforeDestroy() {
// 在组件触发 beforeDestroy 钩子时,去将宽度删除
this.updateLabelWidth('remove');
},
methods: {
// 更新元素的宽度
updateLabelWidth(action = 'update') {
// 如果宽度设置的是 auto,并且获取到了组件根元素的第一个子元素
if (this.$slots.default && this.isAutoWidth && this.$el.firstElementChild) {
// action 是 update 去获取 label 的宽度
if (action === 'update') {
...
} else if (action === 'remove') {
// update 为 remove 时,从数组中删除该 label 的宽度
this.elForm.deregisterLabelWidth(this.computedWidth);
}
}
}
}
Form
组件,从数组中删除该 label
的宽度:
js
// Form 组件
methods: {
// 从 potentialLabelWidthArr 数组中删除对应宽度
deregisterLabelWidth(val) {
const index = this.getLabelWidthIndex(val);
this.potentialLabelWidthArr.splice(index, 1);
}
}
2.4 表单域的验证功能
表单域的验证是引入的 async-validator 进行校验。
实现步骤:
- 需要先安装并引入
async-validator
。 - 实现
Form-Item
组件(单个表单项)的校验。 - 通过循环全部字段,依次调用
Form-Item
组件的校验方法,来实现整体Form
组件的校验。 - 增加清空校验的方法
clearValidate
。 - 在重置方法
resetField
中增加关于验证的处理。
具体实现:
第一步:需要先安装并引入 async-validator
执行 npm install async-validator
去安装。
在 Form-Item
组件中引入 async-validator
:
js
import AsyncValidator from 'async-validator';
第二步:实现 Form-Item 组件(单个表单项)的校验
接收 rules
属性,采用 async-validator
进行验证。
(1)接收 rules
属性;提供 getRules
方法,整理传入的规则;并且提供 getFilteredRule
方法,根据校验触发的方式(如:blur
、change
等)来去筛选对应的规则。
js
// Form-Item 组件
props: {
rules: [Object, Array],
},
methods: {
// 将规则整理成数组形式
getRules() {
// 获取 Form 组件传入的 rules
let formRules = this.form.rules;
// 获取 Form-Item 组件本身的 rules
const selfRules = this.rules;
// 判断是否传入了 required,如果传入了 required 将其转换为布尔值
const requiredRule = this.required !== undefined ? { required: !!this.required } : [];
// 获取到表单项对应 prop 的规则的值
const prop = getPropByPath(formRules, this.prop || '');
formRules = formRules ? (prop.o[this.prop || ''] || prop.v) : [];
// 将规则整理成数组形式(增加 required 传参的影响)
return [].concat(selfRules || formRules || []).concat(requiredRule);
},
// 根据 trigger 去筛选对应的规则
getFilteredRule(trigger) {
const rules = this.getRules();
// 循环所有规则
return rules.filter(rule => {
if (!rule.trigger || trigger === '') return true;
// 筛选包含传入的 trigger 的规则
if (Array.isArray(rule.trigger)) {
return rule.trigger.indexOf(trigger) > -1;
} else {
return rule.trigger === trigger;
}
}).map(rule => Object.assign({}, rule));
},
}
(2)提供记录校验情况的变量
js
// Form-Item 组件
data() {
return {
validateState: '', // 校验的状态(success or error)
validateMessage: '', // 校验失败的提示信息
validateDisabled: false, // 是否禁止校验
}
},
(3)提供 validate
方法来实现校验
js
// Form-Item 组件的校验方法
validate(trigger, callback = () => {}) {
// 将 validateDisabled 设置为 false,表示可以验证
this.validateDisabled = false;
// 使用 getFilteredRule 获取验证规则,根据 trigger 来筛选
const rules = this.getFilteredRule(trigger);
// 如果没有验证规则,并且也没有传入 required 属性,则直接返回 true
if ((!rules || rules.length === 0) && this.required === undefined) {
callback();
return true;
}
// 将验证状态设置为 validating,表示验证中
this.validateState = 'validating';
// 整理传入校验器的数据的格式
const descriptor = {};
// 将 rules 规则中的 trigger 删除,避免传入多余的字段
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;
// 使用 validate 方法:model 是校验的数据;firstFields 表示指定的第一个校验规则生成错误时调用回调;
validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
// 将校验状态存储到 validateState 变量中
this.validateState = !errors ? 'success' : 'error';
// 将错误信息存储到 validateMessage 变量中
this.validateMessage = errors ? errors[0].message : '';
// 回调函数:第一个参数是校验的错误信息,第二个参数是错误的字段及未通过的规则
callback(this.validateMessage, invalidFields);
// 让 Form 组件可以监听到 validate 事件
this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
});
},
第三步:通过循环全部字段,依次调用 Form-Item 组件的校验方法,来实现整体 Form 组件的校验
Form
组件的校验需要增加两个方法:
validate
方法:用于校验整个表单。validateField
方法:用于校验部分传入的字段。
(1)validate
方法
js
// Form 组件校验整个表单的 validate 方法
validate(callback) {
// 如果没有传 model,返回一个警告信息
if (!this.model) {
console.warn('[Element Warn][Form]验证方法需要 model 参数');
return;
}
let promise;
// 如果没有 callback,则返回 promise
if (typeof callback !== 'function' && window.Promise) {
promise = new window.Promise((resolve, reject) => {
callback = function(valid, invalidFields) {
// 如果 valid 为 true,则 resolve,否则 reject(用户既可以传入callback,也可以用 .then、.catch)
valid ? resolve(valid) : reject(invalidFields);
};
});
}
let valid = true; // 校验状态,初始值为true
let count = 0; // 校验次数,初始值为0
// 如果 fields 为空,则立即返回 true
if (this.fields.length === 0 && callback) {
callback(true);
}
let invalidFields = {}; // 存入错误的字段
this.fields.forEach(field => {
field.validate('', (message, field) => {
if (message) {
valid = false;
}
invalidFields = Object.assign({}, invalidFields, field);
// 如果 callback 是 function,并且全部字段都校验了
if (typeof callback === 'function' && ++count === this.fields.length) {
// 则把校验状态和错误的字段返回
callback(valid, invalidFields);
}
});
});
// 如果用户没传入 callback,则返回 promise
if (promise) {
return promise;
}
},
(2)validateField
方法
js
// Form 组件验证传入的部分字段的 validateField 方法
validateField(props, cb) {
props = [].concat(props);
// 从全部字段中筛选传入的字段
const fields = this.fields.filter(field => props.indexOf(field.prop) !== -1);
// 如果没有传入字段则抛出警告
if (!fields.length) {
console.warn('[Element Warn]需要传入字段!');
return;
}
// 依次调用表单项的 validate 方法
fields.forEach(field => {
field.validate('', cb);
});
},
第四步:增加清空校验的方法 clearValidate
首先,Form-Item
组件需要增加 clearValidate
方法,来清空当前表单项的校验。
js
// Form-Item 组件的清空校验
clearValidate() {
// 将校验状态和错误信息置为空,将是否禁用校验置为 false
this.validateState = '';
this.validateMessage = '';
this.validateDisabled = false;
},
Form
组件的 clearValidate
方法为根据字段去循环调用 Form-Item
组件的 clearValidate
方法,如果没有传入字段,则清空整个表单的校验。
js
// Form 组件的清除表单校验
clearValidate(props = []) {
// 筛选传入的字段:1. 如果没有传入的字段,则校验全部字段 2. 如果传入的字段为单个字符串,则整理为数组形式
const fields = props.length
? (typeof props === 'string'
? this.fields.filter(field => props === field.prop)
: this.fields.filter(field => props.indexOf(field.prop) > -1)
) : this.fields;
// 依次调用每一个表单项的 clearValidate 方法
fields.forEach(field => {
field.clearValidate();
});
},
第五步:在重置方法 resetField 中增加关于验证的处理
Form-Item
组件在 resetField
方法中增加将验证状态和验证信息重置的处理,并且在重置表单的值之前先去禁用表单的验证,防止触发了 change
方法的验证。
js
// Form-Item 组件在 resetField 方法中增加验证的处理
resetField() {
// 重置验证状态和验证信息
this.validateState = '';
this.validateMessage = '';
let model = this.form.model;
let value = this.fieldValue;
let path = this.prop;
if (path.indexOf(':') !== -1) {
path = path.replace(/:/, '.');
}
// 通过 getPropByPath 方法获取到表单项属性所在对象、属性的 key、以及属性的值
let prop = getPropByPath(model, path, true);
// 先禁用验证,因为之后需要重置表单项,触发 change 方法
this.validateDisabled = true;
// 将属性的值重置为初始值
if (Array.isArray(value)) {
prop.o[prop.k] = [].concat(this.initialValue);
} else {
prop.o[prop.k] = this.initialValue;
}
// 重置字段后,再将是否禁用验证重置为 false,表示目前可以验证
this.$nextTick(() => {
this.validateDisabled = false;
});
},
三. 总结
此篇将 Element-ui
中 Form
组件提供的功能进行了拆分,分析了一下 Form
组件的源代码中的几个重点功能,包含:表单域的展示,表单域的重置功能,表单域前缀的宽度计算,表单域的验证这几个功能。在看源代码的过程中,可以先根据 Form
提供的属性和方法,将 Form
提供的功能进行分类,这样通过 Form
提供的功能层层递进的去看源代码。
并且,在看源代码的过程中,发现了如果 label-width
设置为 auto
,那么 label-position
设置为 left
也不会生效,这里还对源代码进行了修改。
在看稍微复杂一些的组件的源代码时,可以先根据功能点进行拆分,然后边看边一步一步的去实现组件的各个功能。