具有数据校验功能的表单组件——Form
之前有介绍了组件间的两种通信方法:provide / inject 和 dispatch / broadcast,前者是 Vue.js 内置的,主要用于子组件获取父组件(包括跨级)的状态;后者是自行实现的一种混合,用于父子组件(包括跨级)间通过自定义事件通信。本小节则基于这两种通信方法,来实现一个具有数据校验功能的表单组件——Form。
Form 组件概览(以IView为例,antd UI、element UI 同理)
表单类组件在项目中会大量使用,比如输入框(Input)、单选(Radio)、多选(Checkbox)、下拉选择器(Select)等。在使用表单类组件时,也会经常用到数据校验,如果每次都写校验程序来对每一个表单控件校验,会很低效,因此需要一个能够校验基础表单控件的组件,也就是本节要完成的 Form 组件。一般的组件库都提供了这个组件,比如 iView,它能够校验内置的 15 种控件,且支持校验自定义组件,如下图所示:(也可以在线访问本示例体验:
https://run.iviewui.com/jwrqnFss)
Form 组件分为两个部分,一个是外层的 Form 表单域组件,一组表单控件只有一个 Form,而内部包含了多个 FormItem 组件,每一个表单控件都被一个 FormItem 包裹。基本的结构看起来像:
</i-formForm 要用到数据校验,并在对应的 FormItem 中给出校验失败的提示,校验我们会用到一个开源库:async-validator,基本主流的组件库都是基于它做的校验。使用它很简单,只需按要求写好一个校验规则就好,比如:
[
{ required: true, message: ‘邮箱不能为空’, trigger: ‘blur’ },
{ type: ‘email’, message: ‘邮箱格式不正确’, trigger: ‘blur’ }这个代表要校验的数据先判断是否为空(required: true),如果为空,则提示“邮箱不能为空”,触发校验的事件为失焦(trigger: ‘blur’),如果第一条满足要求,再进行第二条的验证,判断是否为邮箱格式(type: ‘email’)等等,还支持自定义校验规则。更详细的用法可以参看它的文档。
接口设计
我们先使用最新的 Vue CLI 3 创建一个空白的项目(如果你还不清楚 Vue CLI 3 的用法,需要先补习一下了,可以阅读文末的扩展阅读 1),并使用 vue-router 插件,同时安装好 async-validator 库。在
src/components 下新建一个 form 文件夹,并初始化两个组件 form.vue 和 form-item.vue,然后初始化项目,配置路由,创建一个页面能够被访问到。
本节所有代码可以在 https://github.com/icarusion/vue-component-book 中查看,你可以一边看源码,一边阅读本节;也可以边阅读,边动手实现一遍,遇到问题再参考完整的源码。
第 2 节我们介绍到,编写一个 Vue.js 组件,最重要的是设计好它的接口,一个 Vue.js 组件的接口来自三个部分:props、slots、events。而 Form 和 FormItem 两个组件主要做数据校验,用不到 events。Form 的 slot 就是一系列的 FormItem,FormItem 的 slot 就是具体的表单控件,比如输入框 。那主要设计的就是 props 了。在
Form 组件中,定义两个 props:
• model:表单控件绑定的数据对象,在校验或重置时会访问该数据对象下对应的表单数据,类型为 Object。
• rules:表单验证规则,即上面介绍的 async-validator 所使用的校验规则,类型为 Object。
在 FormItem 组件中,也定义两个 props:
• label:单个表单组件的标签文本,类似原生的 元素,类型为 String。
• prop:对应表单域 Form 组件 model 里的字段,用于在校验或重置时访问表单组件绑定的数据,类型为 String。
定义好 props,就可以写出大概的用例了:
当前的两个组件只是个框框,还没有实现任何功能,不过万事开头难,定义好接口,剩下的就是补全组件的逻辑,而对于使用者,知道了 props、events、slots,就已经能写出上例的使用代码了。到此,Form 和 FormItem 的代码如下:
{ if (field) this.fields.push(field); }); this.$on('on-form-item-remove', (field) => { if (field.prop) this.fields.splice(this.fields.indexOf(field), 1); }); }定义的数据 fields 就是用来缓存所有 FormItem 实例的。 触发校验 Form 支持两种事件来触发校验: • blur:失去焦点时触发,常见的有输入框失去焦点时触发校验; • change:实时输入时触发或选择时触发,常见的有输入框实时输入时触发校验、下拉选择器选择项目时触发校验等。以上两个事件,都是有具体的表单组件来触发的,我们先来编写一个简单的输入框组件 i-input。在 components 下新建目录 input,并创建文件 input.vue:
// form-item.vue,部分代码省略
export default {
methods: {
setRules () {
this.
o
n
(
′
o
n
−
f
o
r
m
−
b
l
u
r
′
,
t
h
i
s
.
o
n
F
i
e
l
d
B
l
u
r
)
;
t
h
i
s
.
on('on-form-blur', this.onFieldBlur); this.
on(′on−form−blur′,this.onFieldBlur);this.on(‘on-form-change’, this.onFieldChange);
},
},
mounted () {
if (this.prop) {
this.dispatch(‘iForm’, ‘on-form-item-add’, this);
this.setRules();
}
}通过调用 setRules 方法,监听表单组件的两个事件,并绑定了句柄函数 onFieldBlur 和 onFieldChange,分别对应 blur 和 change 两种事件类型。当 onFieldBlur 或 onFieldChange 函数触发时,就意味着 FormItem 要对当前的数据进行一次校验。当前的数据,指的就是通过表单域 Form 中定义的 props:model,结合当前 FormItem 定义的 props:prop 来确定的数据,可以回顾上文写过的用例。因为 FormItem 中只定义了数据源的某个 key 名称(即属性 prop),要拿到 Form 中 model 里的数据,需要用到第 3 节的通信方法 provide / inject。所以在 Form 中,把整个实例(this)向下提供,并在 FormItem 中注入:
// form.vue,部分代码省略
export default {
provide() {
return {
form : this
};
}// form-item.vue,部分代码省略
export default {
inject: [‘form’]准备好这些,接着就是最核心的校验功能了。blur 和 change 事件都会触发校验,它们调用同一个方法,只是参数不同。相关代码如下:
// form-item.vue,部分代码省略
import AsyncValidator from ‘async-validator’;
export default {
inject: [‘form’],
props: {
prop: {
type: String
},
},
data () {
return {
validateState: ‘’, // 校验状态
validateMessage: ‘’, // 校验不通过时的提示信息
}
},
computed: {
// 从 Form 的 model 中动态得到当前表单组件的数据
fieldValue () {
return this.form.model[this.prop];
}
},
methods: {
// 从 Form 的 rules 属性中,获取当前 FormItem 的校验规则
getRules () {
let formRules = this.form.rules;
formRules = formRules ? formRules[this.prop] : [];
return [].concat(formRules || []);
},
// 只支持 blur 和 change,所以过滤出符合要求的 rule 规则
getFilteredRule (trigger) {
const rules = this.getRules();
return rules.filter(rule => !rule.trigger || rule.trigger.indexOf(trigger) !== -1);
},
/**
* 校验数据
* @param trigger 校验类型
* @param callback 回调函数
*/
validate(trigger, callback = function () {}) {
let rules = this.getFilteredRule(trigger);
if (!rules || rules.length === 0) {
return true;
}
// 设置状态为校验中
this.validateState = ‘validating’;
// 以下为 async-validator 库的调用方法
let descriptor = {};
descriptor[this.prop] = rules;
const validator = new AsyncValidator(descriptor);
let model = {};
model[this.prop] = this.fieldValue;
validator.validate(model, { firstFields: true }, errors => {
this.validateState = !errors ? ‘success’ : ‘error’;
this.validateMessage = errors ? errors[0].message : ‘’;
callback(this.validateMessage);
});
},
onFieldBlur() {
this.validate(‘blur’);
},
onFieldChange() {
this.validate(‘change’);
}
}在 FormItem 的 validate() 方法中,最终做了两件事:
- 设置了当前的校验状态 validateState 和校验不通过提示信息 validateMessage(通过值为空);
- 将 validateMessage 通过回调 callback 传递给调用者,这里的调用者是 onFieldBlur 和 onFieldChange,它们只传入了第一个参数 trigger,callback 并未传入,因此也不会触发回调,而这个回调主要是给 Form 用的,因为 Form 中可以通过提交按钮一次性校验所有的 FormItem(后文会介绍)这里只是表单组件触发事件时,对当前 FormItem 做校验。
除了校验,还可以对当前数据进行重置。重置是指将表单组件的数据还原到最初绑定的值,而不是清空,因此需要预先缓存一份初始值。同时我们将校验信息也显示在模板中,并加一些样式。相关代码如下:
Form 组件中,预先缓存了全部的 FormItem 实例,自然也能在 Form 中调用它们。通过点击提交按钮全部校验,或点击重置按钮全部重置数据,只需要在 Form 中,逐一调用缓存的 FormItem 实例中的 validate 或 resetField 方法。相关代码如下:
// form.vue,部分代码省略
export default {
data () {
return {
fields: []
};
},
methods: {
// 公开方法:全部重置数据
resetFields() {
this.fields.forEach(field => {
field.resetField();
});
},
// 公开方法:全部校验数据,支持 Promise
validate(callback) {
return new Promise(resolve => {
let valid = true;
let count = 0;
this.fields.forEach(field => {
field.validate(’’, errors => {
if (errors) {
valid = false;
}
if (++count === this.fields.length) {
// 全部完成
resolve(valid);
if (typeof callback === ‘function’) {
callback(valid);
}
}
});
});
});
}
},虽然说 Vue.js 的 API 只来自 prop、event、slot 这三个部分,但一些场景下,需要通过 ref 来访问这个组件,调用它的一些内置方法,比如上面的 validate 和 resetFields 方法,就需要使用者来主动调用。resetFields 很简单,就是通过循环逐一调用 FormItem 的 resetField 方法来重置数据。validate 稍显复杂,它支持两种使用方法,一种是普通的回调,比如:
validate.then((valid) => {
if (valid) {
window.alert(‘提交成功’);
} else {
window.alert(‘表单校验失败’);
}
})在 Form 组件定义的 Promise 中,只调用了 resolve(valid),没有调用 reject(),因此不能直接使用 .catch() ,不过聪明的你稍作修改,肯定能够支持到!完整的用例如下:
具有数据校验功能的表单组件——Form
https://github.com/icarusion/vue-component-book
项目基于 Vue CLI 3 构建,下载安装依赖后,通过 npm run serve 可访问。
结语
组件最终的效果看起来有点 “low”,但它实现的功能却不简单。通过这个实战,你或许已经感受到本小册一开始说的,组件写到最后,都是在拼 JavaScript 功底。的确,Vue.js 组件为我们提供了一种新的代码组织形式,但归根到底,是离不开 JS 的。这个实战,你应该对独立组件间的通信用法有进一步的认知了吧,不过,这还不是组件通信的终极方案,下一节,我们就来看看适用于任何场景的组件通信方案。
注:本节部分代码参考
iView。
扩展阅读
• 一份超级详细的Vue-cli3.0使用教程