源码解析Vue最好用的组件库Element:Form 表单

1.$message 实现 (已完成)
2.表单 实现 (已完成)

接着上一篇继续讲表单部分。

表单效果

算了,复杂点,直接 el-form + el-form-item + el-input 加validate吧。

我们再使用的时候常常会这么写:

1
2
3
4
5
<el-form :model="ruleForm" :rules="rules" ref="ruleForm">
<el-form-item label="活动名称" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
</el-form>

简单说,三种标签: el-form el-form-item el-input,使用了数据绑定,数据校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
this.$refs.fuleForm.validate((valid) => {
if (valid) {
alert('submit!');
} else {
console.log('error submit!!');
return false;
}
});
var rules ={
name: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
]
}

技术实现

三种标签嵌套实现

三种标签如何实现,思路很简单 slot 即可,在标签内使用 标签。

分析 el-form

1
2
3
<div>
<slot></slot>
</div>

分析 el-form-item

1
2
3
4
5
6
7
8
9
10
11
<div>
<label-wrap>
<label>
<slot name="label"></slot>
</label>
</label-wrap>
<div>
<slot></slot>
<slot name='error'></slot>
</div>
</div>

分析 el-input

1
<input @input='onInput' :value='value' v-bind="$attrs" ref="input"/>

注意:

实际情况下,用户可能瞎用,比如插入了<div>标签。就破坏了 form -> form-item -> input的简单嵌套规则。

数据传递

因为是组件,显然不能使用外置的数据解决方案。显然是 Provide/Inject 的方案最合适。

Provide/Inject不是响应式的,如何变为响应式变化?Element 是这么做的:

1
2
3
4
5
6
7
8
export defalut {
props:{} // 传入的各种配置
provide(){
return {
elForm: this // 妙啊,把this实例续进去了!
}
}
}

input 落地

看了 from,在看最终的 input

技术补充

v-model

都说 v-model是语法糖:input事件触发事件,拿到输入的值。对外emit input 事件。

但不仅仅语法糖,我们观察输入中文,打拼音时候是不触发事件的,等到字选中才展示出来。

请看我的实例,右边框输入拼音,左边自动就出现了字母,如果是 v-model 就会等到选完字才出现。这里就不演示了。

补充: compositionstart compositionend 这两个是件就是语言输入开始和结束监听的变化。

我们监听 input compositionstart compositionend 这三个事件,借助 e.target.composing 就能组合文字状态,start时候composing设置为true,end时候设置false,然后赋值input

技术补充:inheritAttrs和$attr

通过 <el-input placeholder='xxx'> 提供的attr,如何传递,这里就用到了$attrs 这里是说,非 prop 属性会做展示。注意,这里需要设置 inheritAttrs: false 。这俩配合使用。

如果我在父组件中写了 c=2这个非prop属性。

1
2
3
<MyInput v-model="model.username"
c="2"
placeholder="xxx" />

正常情况下,最终会渲染成这种DOM,就是说 template 下的根标签,要不要展示。

1
2
3
<div c="2" placeholder="xxx">
<input type="text" c="2" placeholder="xxx">
</div>

如果设置为 false,那么 template下的根元素,就没有了。

1
2
3
<div>
<input type="text">
</div>

如何拿到? 答案$attrs

如果你希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false

form-item

现在再来说 formItem。刚才也提到因为是中间层,用户可能会做出奇奇怪怪的处理,所以数据传递用了provider/inject

那么在 formItem 里,我们就可以通过 inject拿到 model rules。这里我们可以做校验了。

1
this.$refs.fuleForm.validate()

先找到DOM元素。然后执行方法。

思考,为什么要在 el-form中传入 :model 。这是为了一次指定,处处使用。我们再 item 里可以随时拿到 model,通过 prop 获取到当前的值。再结合 rules 就可以单独去判断每一项是否符合要求。

校验可以通过 方法触发、也可以通过事件触发,比如input blur时候触发校验,这里绕了一圈编程了如何传递事件,结论是,把校验的事件发给祖辈,发给 el-form-item(普通校验)或el-form(整体校验),由祖辈再触发校验功能。

我们在 from-item里面执行下面的语句。触发事件,执行方法。

1
2
3
4
5
6
7
8
9
10
11
var a = {
mounted(){
this.$on('validate',()=>{
this.validate()
})
},
methods:{
validate(){
}
}
}

这里有两个疑难点:事件监听和处理+校验。前者我们封装一个 eventBus即可,后者element和ant都是用一个第三方外置库 async-validate——最麻烦的地方外包了。

这个 async-validate 的具体用法,可以先忽略,总之,根据规则创建 校验实例。

刚才提到我们要在 from上注册一个 validate方法,用来检测下面的所有 formItem 是否合法。

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = {
methods:{
validate(cb){
const tasks = this.$children
.filter(item => item.prop) // 过滤非prop
.map(item => item.vaildate)

Promise.all(tasks)
.then(()=> cb(true))
.catch(()=> cb(false))
}
}
}

注意,怎么在 from-item 里面确定找到el-form ,用递归。看一下就懂了。

1
2
3
4
5
6
7
8
9
10
11
12
form() { // 递归
let parent = this.$parent;
let parentName = parent.$options.componentName;
while (parentName !== "ElForm") {
if (parentName === "ElFormItem") {
this.isNested = true;
}
parent = parent.$parent;
parentName = parent.$options.componentName;
}
return parent;
}

顺着这个递归的思想,继续补充一个知识。

事件传递 祖先给子代发事件,校验。子代给祖先发事件校验。

src/mixins/emitter.js里有dispatch broadcast

广播:从上到下,遍历 this.$children 找到对应项。child.\$emit()

dispatch parent.$emit.apply(parent, [eventName].concat(params));

N 参考资料

请我喝杯咖啡吧~