Form组件分为两个部分,外层需要有个Form表单域组件,内部包含多个FormItem组件,在FormItem组件 中插入表单元素。Form要用到数据校验,并在对用的FormItem中给出校验失败的提示,校验规则使用一个开源 库async-validator(https://github.com/yiminghe/async-validator),安装命令npm i async-validate。
步骤:
步骤1、Form组件中,定义两个props:
model: 表单控件绑定的数据对象,类型为object
rules: 表单验证规则,类型为object
代码:
<!-- form.vue -->
<template>
<form>
<slot></slot>
</form>
</template>`在这里插入代码片`
<script>
export default {
name: 'iForm',
props: {
model: { type: Object }, // 表单数据对象
rules: { type: Object } // 表单校验规则
}
}
</script>
步骤2、FormItem组件中,定义两个props:
label: 单个表单组件的标签文本
prop: 对应表单域Form组件的model里的字段,用域在校验或重置时访问表单组件绑定的数据,类型为String
代码如下:
<!-- form-item.vue -->
<template>
<div>
<label v-if="label">{{ label }}</label>
<div>
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'iFormItem',
props: {
label: {
type: String,
default: ''
},
prop: {
type: String
}
}
}
</script>
步骤3:在Form中缓存FormItem实例,当每个FormItem渲染的时候,将其自身(this)作为参数通过自定义的dispatch派发到Form组件中,通过一个数组缓存起来;当FormItem销毁时,同理,将其从Form缓存数组中移除。
代码如下:form-item.vue
import mixins from '../../mixins/mixins.js';
export default {
name: 'cFormItem',
mixins: [ mixins ],
/** 组件渲染时,将实例缓存在 Form 中 */
mounted () {
// 如果没有传⼊ prop,则⽆需校验,也就⽆需缓存
if (this.prop) {
//当前的FormItem将this作为参数,派发到form组件中,在form中,需要在created中进行$on监听派发的事件
this.dispatch('cForm', 'on-form-item-add', this);
}
},
/** 当组件销毁前,将实例从form的缓存中移除 */
beforeDestroy() {
//将this作为参数,派发到form组件中,在form中,需要在created中进行$on监听派发的事件
this.dispatch('cForm', 'on-form-item-remove', this)
},
mixins.js代码:
function broadcast(componentName, eventName, params){
// 遍历当前组件的所有孩子节点
this.$children.forEach(child => {
const name = child.$options.name // 获取组件标识name名称
if(name === componentName){ // 找到与传入组件名相同的组件
console.log(child,[eventName].concat(params),'if')
// 等同于在子组件中this.$emit('click',params),这里的child相当于this
child.$emit.apply(child,[eventName].concat(params))
}else{
// 递归调用 broadcast方法,apply的所有参数都必须放在数组中,第一位代表this
broadcast.apply(child, [componentName,eventName].concat([params]));
}
})
}
export default {
methods:{
/** 子给父传递消息 派发 */
dispatch(componentName,eventName,params){
let parent = this.$parent || this.$root; // 获取根节点
let name = parent.$options.name; // 获取根节点的名字
while (parent && (!name || name !== componentName)) { // 当根节点及名字存在,且名字不为传入的名字
parent = parent.$parent; // 获取根节点的父级
if(parent){
name = parent.$options.name; // 获取根节点的父级名称
}
}
if(parent){ // 祖父节点
// 触发当前实例上的事件
parent.$emit.apply(parent,[eventName].concat(params))
}
},
/** 父给子传递消息 广播 */
broadcast(componentName,eventName,params){
// 递归调用broadcast方法,call的第一位为this,后面依次跟参数,参数与参数之间用逗号隔开
broadcast.call(this,componentName,eventName,params)
}
}
}
这里需要注意的是:vue组件渲染顺序是由内而外的,所以FormItem要先于Form渲染,所以在 FormItem 的 mounted
触发时,我们向Form派发事件on-form-item-add,并将当前FormItem的实例(this)传递给了Form,就在 此时,Form
组件中的mounted并未触发,我们需要在Form的created中通过$on进行监听自定义的事件。因为Form的created要先于
FormItem的mounted。
代码如下:form.vue
export default{
name:'cForm',
data(){
return{
fields:[] // 用于缓存FormItem实例
}
},
created(){
// 通过$on监听FromItem派发出来的事件on-form-item-add
this.$on('on-form-item-add', (field)=>{
if(field){
// 接收,并用数组缓存起来
this.fields.push(field)
}
})
// 通过$on监听FromItem派发出来的事件on-form-item--remove
this.$on('on-form-item-remove', (field)=>{
if(field.prop){
this.fields.splice(this.fields.indexOf(field),1)
}
})
}
}
步骤4、 触发校验且编写input组件。
Form支持两种事件来触发校验:
blur: 失去焦点触发
change: 实时输入是触发或选择时触发
简单的input组件代码如下: input.vue
<template>
<input
class="c-input__inner"
:type="type"
:value="currentValue"
@input="handleInput"
@blur="handleBlur"
@focus="handleFocus"
@change="handleChange"
>
</template>
<script>
import mixins from '@/assets/js/mixins'
export default {
name: "cInput",
mixins:[mixins],
props:{
// input框上实时响应的数据 实现双向数据绑定第一步
value:{
type:String,
default:''
},
// input框的类型
type:{
type: String,
default: 'text'
}
},
data(){
return{
currentValue: this.value // 实现双向数据绑定第二步
}
},
watch:{
// 当value发生变化时,给this.currrentValue赋值
value: { // 实现双向数据绑定第三步
handler (val) {
this.currentValue = val
},
deep: true,
immediate: true
}
},
methods:{
handleInput(event){
// 实现双向数据绑定 第四步
const value = event.target.value; //获取当前的值
this.currentValue = value // input响应当前的值
this.$emit('input', value)
// 向FormItem派发一个change事件,以及值
this.dispatch('cFormItem', 'on-form-change', value)
},
/** 原生input框的change方法 */
handleChange(){
this.$emit('change', this.currentValue) // 绑定change事件
},
/** 原生input框的blur方法 */
handleBlur(){
this.$emit('blur',this.currentValue) // 绑定失去焦点事件
// 向FormItem派发一个blur事件,以及值
this.dispatch('cFormItem', 'on-form-blur', this.currentValue)
},
/** 原生input框的focus方法 */
handleFocus(){
this.$emit('focus',this.currentValue) // 绑定失去焦点事件
}
}
}
</script>
<style lang="scss" scoped>
.c-input__inner{
background-color: #fff;
border-radius: 4px;
height: 30px;
line-height: 30px;
outline: none;
border:1px solid #dcdfe6;
display: inline-block;
padding: 0 15px;
}
</style>
说明:绑定在input上的原生事件@input、@blur,每当触发时,都会调用handleInput、handleBlur,并通过dispatch方法
向上级的FormItem组件派发自定义事件on-form-change、on-form-blur;派发的事件主要用于校验。
步骤5、在FormItem中通过$on进行监听input派发的事件
// form-item.vue
export default {
methods: {
/** 当触发这两个方法时,就意味着要进行一次校验 */
setRules () {
this.$on('on-form-blur', this.onFieldBlur); // 监听input派发来的on-form-change事件
this.$on('on-form-change', this.onFieldChange); // 监听input派发来的on-form-blur事件
},
},
mounted () {
if (this.prop) {
this.dispatch('cForm', 'on-form-item-add', this);
this.setRules();
}
}
}
步骤6、 通过provide/inject通信方法啊,拿到prop对用的Form中model里的数据。在From中,把整个实例(this)向下提供,并注入到FormItemz中
代码如下:
// form.vue
export default {
provide() {
return { form : this };
}
}
// form-item.vue
export default {
inject: ['form']
}
步骤7、在FormItem中引入校验规则方法,编写blur与change触发校验方法
代码如下:
// form-item.vue
import AsyncValidator from 'async-validator';
export default {
name: "cFormItem",
inject:['form'], // 注入依赖
props:{
// 表单的label元素
label:{
type:String,
default:''
},
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; // 获取表单的检验规则
// 当前formItem存在校验规则,则记录住校验规则,否则记录为[]
formRules = formRules ? formRules[this.prop]:[];
// 返回当前formItem校验规则集合
return [].concat(formRules || []);
},
/** 所以过滤出符合要求的rules规则(只支持blur和change),*/
getFilteredRule (trigger) {
const rules = this.getRules(); // 获得当前触发的formItem的校验信息
console.log(rules,'rules')
// 只返回校验事件符合要求的rules规则
return rules.filter(rule => !rule.trigger || rule.trigger.indexOf(trigger) !== -1);
},
/**
* 校验数据
* @param trigger 校验类型
* @param callback 回调函数 --- 回调主要是给form用的,
* form中可以通过提交按钮一次性校验所有的formitem
*/
validateItem(trigger, callback = function () {}) {
let rules = this.getFilteredRule(trigger); // 当前获取blur和change的校验规则
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;
console.log(model,'model,lllll')
// 调用AsyncValidator开源库中的校验方法
validator.validate(model, { firstFields: true }, errors => {
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
callback(this.validateMessage);
});
},
/** 失去焦点时触发校验规则 */
onFieldBlur() {
this.validateItem('blur');
},
/** 内容改变时触发校验规则 */
onFieldChange() {
this.validateItem('change');
},
}
}
说明:在 FormItem 的 validate() ⽅法中,最终做了两件事:
1. 设置了当前的校验状态 validateState 和校验不通过提示信息 validateMessage(通过值为空);
2. 将 validateMessage 通过回调 callback传递给调⽤者,这⾥的调⽤者是 onFieldBlur 和 onFieldChange,
它们只传⼊了第⼀个参数 trigger,callback 并未传⼊,因此也不会触发回调,⽽这个回调主要是给 Form ⽤的,
因为 Form 中可以通过提交按钮⼀次性校验所有的 FormItem(后⽂会介绍)这⾥ 只是表单组件触发事件时,
对当前 FormItem 做校验。
步骤8、对当前数据进行重置,即将当前表单组件的数据还原到最初绑定的值。
代码如下: form-item.vue
<template>
<div>
<label v-if="label" :class="{ 'c-form-item-label-required': isRequired }">{{ label }}</label>
<div>
<slot></slot>
<div v-if="validateState === 'error'" class="c-form-item-message">
{{ validateMessage }}
</div>
</div>
</div>
</template>
<script>
export default {
props:{
// 表单的label元素
label:{
type:String,
default:''
},
prop:{
type:String
}
},
data(){
return{
initialValue:'', // 记录数据
isRequired: false, // 是否为必填
validateState:'', // 校验的状态
validateMessage:'', // 校验不通过的提示信息
}
},
mounted() {
//渲染组件时,将实例缓存在form中
if(this.prop){// 如果没有传入prop,则无需检验,也无需缓存
//将this作为参数,派发到form组件中,在form中,需要在created中进行$on监听派发的事件
this.dispatch('cForm','on-form-item-add',this)
// 设置初始值,以便在重置时恢复默认值
this.initialValue = this.fieldValue;
// 监听input组件派发的两个事件
this.setRules();
}
},
methods:{
/** 当触发这两个方法时,就意味着要进行一次校验 */
setRules(){
let rules = this.getRules();
if(rules.length){
rules.every((rule)=>{
// 如果当前校验规则中有必填项,则标记出来
this.isRequired = rule.required;
})
}
// 监听input派发来的on-form-change事件
this.$on('on-form-change',this.onFieldChange)
// 监听input派发来的on-form-blur事件
this.$on('on-form-blur', this.onFieldBlur);
},
/** 从form的rules中,获取当前FormItem的校验规则 */
getRules(){
let formRules = this.form.rules; // 获取表单的检验规则
// 当前formItem存在校验规则,则记录住校验规则,否则记录为[]
formRules = formRules ? formRules[this.prop]:[];
// 返回当前formItem校验规则集合
return [].concat(formRules || []);
},
/** 对当前数据进行重置 */
resetField(){
this.validateState = '' // 清空校验状态
this.validateMessage = '' // 清空错误信息
this.form.model[this.prop] = this.initialValue; // 还原当前formItem的值为默认值
},
}
}
</script>
<style lang="scss" scoped>
.c-form-item-label-required:before {
content: '*'; color: red;
}
.c-form-item-message {
color: red;
font-size:12px;
}
</style>
步骤9、在form中实现全部检验和全部重置
代码如下: // form.vue
<script>
export default {
data(){
return{
fields:[] // 用于缓存FormItem实例
}
},
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);
}
}
});
});
});
}
}
}
</script>