Bootstrap

Vue 系列 - v-model自定义组件结合el-form做表单校验

  日常业务中,form表单很常见了,在使用Vue开发时,使用ElementUI作为组件开发使用;除了UI组件提供的通用form组件,如:input、select等等 , 由于业务的不同,不仅仅局限于简单的输入、选择 . 会存在组合操作,需要按照业务逻辑处理输入/选择的值 . 探究各UI组件的实现 , 并将自定义form组件融入到form表单中 , 也很重要.

示例中各框架、组件版本
Vue@2.6
Element@2.14.1

阅读本文你可以了解到:

  1. ElementUI form表单基本使用 , 表单校验流程.
  2. v-model 自定义组件 , 并结合element from进行校验.
ElementUI

这是一个基础示例:

elementForm.vue

<template>
    <div style="width:40%">
        <el-form ref="form" :model="userInfo" :rules="rules">
            <el-form-item label="姓名" prop="name">
                <el-input v-model="userInfo.name" placeholder="姓名" maxlength="15"></el-input>
            </el-form-item>
            <el-form-item label="年龄" prop="age">
                <el-input v-model="userInfo.age" placeholder="年龄" ></el-input>
            </el-form-item>
            <el-form-item label="性别" prop="gender">
                <el-radio-group v-model="userInfo.gender">
                    <el-radio label="0"></el-radio>
                    <el-radio label="1"></el-radio>
                </el-radio-group>
            </el-form-item>
            <el-form-item label="爱好" prop="hobby">
                <el-checkbox-group v-model="userInfo.hobby">
                    <el-checkbox v-for="item in hobbies" :key="item" :label="item" name="hobby"></el-checkbox>
                </el-checkbox-group>
                <div>
                    <el-input v-model.trim="hobby" placeholder="自定义" @change="handleAddHooby" size="mini" style="width:140px;"></el-input>
                </div>
            </el-form-item>
            <el-form-item label="生日" prop="birthday">
                <el-date-picker v-model="userInfo.birthday"></el-date-picker>
            </el-form-item>
            <el-form-item>
                <el-button type="primary" @click="handleSubmitInfo">提交</el-button>
            </el-form-item>
        </el-form>
    </div>
</template>
<script>
export default {
    data(){
        function validateAge(rule,value,callback){
            try{
                if(value===''){
                    callback('请输入年龄')
                }
                if(!value.match(/^\d+$/)){
                    callback('请输入数字')
                }
                if(value*1 <0 || value*1 >150){
                    callback('请输入合理的年龄值')
                }
                callback()
            }catch(err){
                console.log(err)
                callback('请输入年龄')
            }
        }
        return {
            hobby:"",
            hobbies:['篮球','读书','游戏','唱歌','跳舞'],
            userInfo:{
                name:'',
                age:'',
                gender:'1',
                hobby:[],
                birthday:'',
            },
            rules:{
                name:[{required:true,message:'请输入姓名'}],
                age:[{validator:validateAge,trigger:'change'}]
            }
        }
    },
    methods:{
        handleAddHooby(){
            if(this.hobbies.includes(this.hobby)){
                return
            }
            this.hobbies.push(this.hobby)
            this.hobby = ''
        },
        handleSubmitInfo(){
            console.log(this.userInfo)
            this.$refs.form.validate((error,errrorInfo)=>{
                if(error){
                    return
                }
            })
        }
    }
}
</script>

关注点:

  • <el-form ref="form" \> 绑定 , 可获取到form表单实例,调用方法,比如:validate 手动校验、resetFields 重置表单 等.
  • <el-form-item prop="name" \> 绑定 , 与form绑定的规则集rules 键值对应 , 校验时获取校验规则.
  • <el-input v-model="userInfo.name" \> 组件的输入控制 , v-model 指令的使用
v-model实现自定义组件

发现在使用组件el-input, 在没有使用v-model绑定值时,用户无法输入.

<el-input placeholder="姓名"></el-input>

在查看Element源码部分后,发现是在代码里做了控制.

setNativeInputValue() {
    const input = this.$refs.input; // this.$refs.input 源码为 this.getInput();
    if (!input) return;
    if (input.value === this.nativeInputValue) return;
    input.value = this.nativeInputValue;
},

  示例中定义的model prop 同 props 的value属性值, 所以当你在父级使用该组件时, 同时使用了v-model和value ,那么你不会在该组件中获取到props的value值(v-model优先级高,value 被忽略); 只有仅在使用value时, 你在自定义组件中才可以获取到.

custom-input.vue

<template>
    <div>
        <p>这是一个自定义输入组件</p>
        <input ref="input" 
            v-model="inputValue" 
            @input="handleInput"
             />
    </div>
</template>
<script>
export default {
    data(){
        return {
            inputValue:'',
        }
    },
    model:{ // 定义v-model如何去处理该组件 ,值属性定义、事件定义
        prop:"value",
        event:"custom"
    },
    props:{ 
        value:[String,Number], // 由于绑定的属性值与v-model定义的prop一致, 两者选其一, v-model优先, value会被忽略
    },
    mounted(){
        this.inputValue = this.value
    },
    methods:{
      	/**
      	 * 处理输入, 如果不使用v-model则 父组件需要监听 cusmo 的事件,并更新 value 的值
      	 * 使用了 v-model, 则不用管了, v-model 根据model 定义的event事件类型, 监听事件, 进行值更新.
      	 **/
        handleInput(e){
            console.log(this.inputValue)
            this.$emit('custom', Math.random()*10);
        },
    },
}
</script>

自定义组件引用 , v-model 和value 同时绑定, value 会被忽略.

// ... 
<custom-input  v-model="inputValue" value="admin" />
// ...
data(){
    return {
	inputValue:'',
    }
}

当然 , 自定义分发的事件this.$emit('custom', Math.random()*10); , 我们也可以在父组件对其进行监听

// ... 
<custom-input  v-model="inputValue" @custom='handleVModel' />
// ...
data(){
    return {
	inputValue:'',
    }
},
methods:{
    handleVModel(val){
      console.log("listener - vModel :",val)
    }
}

自定义输入组件关注点:

  • model 组件属性设置 , 定义v-model 指令如何处理当前组件:数据名称、事件名称;

    默认 model 的值 prop - value ; event - input ; 示例中定义了事件 event - custom

    model:{
      prop:"value",
      event:"custom"
    }
    

    可以自定义props\event 名称处理具体的业务.

  • 组件内数据变化,需要分发事件, 当前定义的组件触发的事件类型 this.$emit('custom', event.target.value);

  • v-model 会忽略所有表单元素绑定的默认值 , 比如:value\checked\selected,

  • v-model 绑定的 model-prop 属性名称 会被传入组件的props ,需定义 props - value 拿到v-model的值

测试 - 定义不同model - prop 查看输入如何绑定 ; props 中的value 可用于初始化组件内部的 input 值;

// ... 
model:{ // 定义v-model如何去处理该组件 ,值属性定义、事件定义
    prop:"customValue", // 定义不同 的prop
    event:"custom"
},
props:{ 
    customValue:[String,Number], // 双向绑定的 prop 值, 需要在该组件props定义
    value:[String,Number], 
},
watch:{
    customValue(val){
        console.log(val)
    }
},
mthods:{
  /**
   * 内部的input输入框输入
   * 格式化内部输入的数据 , 将格式化的数据给v-model ,那父级绑定的就是给定的值
   **/
  handleInput(e){
      this.$emit('custom', Math.random()*10); //  此处定义的 v-model 响应的值给随机数, 区分和内部input的输入
  },
}
// .... 

  通过改造, 定义组件中不同的model-prop . 可以拿到value值来初始化组件内部的input值. v-model 定义的prop 需要定义在组件的props中, 可通过watch监听值更新打印查看.

思考: 自定义form输入组件中使用v-model时, ,v-model好比一个高阶组件 , 自身内部维护一个model-prop 名称的属性值, 监听model-event的事件名称的事件 . 事件触发更新自身的prop的属性的值. 值更新时,同时调用父级、自身的重新渲染.

el-form 校验原理

当用户输入值或更改值时,是如何检测并触发校验的

通过查看源代码部分, 通过监听value的值,分发事件到父级

watch: {
    value(val) {
      // this.$nextTick(this.resizeTextarea);
      if (this.validateEvent) {
        this.dispatch('ElFormItem', 'el.form.change', [val]); // 转发事件到el-form-item ; dispatch 为内部自定义事件转发函数
      }
    },
}

再看el-form-item 是怎么处理该事件的 , 在mounted 后, 调用addValidateEvents , 监听了事件,并调用了 addValidateEvents 方法.

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); // 监听事件 , 触发对应的回调函数
  }
},
onFieldChange() {
  if (this.validateDisabled) {
    this.validateDisabled = false;
    return;
  }

  this.validate('change'); // 触发校验 , change 为校验规则中 trigger 的值
},
validate(trigger, callback = noop) {
  this.validateDisabled = false;
  const rules = this.getFilteredRule(trigger); // 获取校验规则, 绑定在el-form上的rules、el-form-item上的rules、required 合并
  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);  // 引用的库 async-validator ; 初始化校验规则 , 实例对象 validator
  const model = {};

  model[this.prop] = this.fieldValue; 

  validator.validate(model, { firstFields: true }, (errors, invalidFields) => { // 调用validat 方法, 校验给定的值model
    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); // el-form 任意表单触发校验后的事件,包括当前属性名称、校验结果、校验信息 
  });
},
自定义组件结合el-form 进行校验

如果我们自定义的组件需要融入到el-form中,并统一校验规则出处 . 需要在自定义组件中在值发生更改后,分发事件出来.

需要改造我们之前自定义的组件custom-input.vue , 需要在v-model 值更改时,分发校验事件触发校验.

import emitter from 'element-ui/src/mixins/emitter'; // 分发事件的el-form处理方法 

// ...

mixins:[emitter], // 混入的方式 , 加载到当前组件
watch:{
    customValue(val){
        console.log(val)
        this.dispatch('ElFormItem', 'el.form.change', [val]); // 值发生变化时, 向el-form-item分发事件 , 调用组件内部的校验流程
    }
},

写完了,来引用到父级表单组件中测试一番. 看效果如何 , 使用如下:

<template>
    <div style="width:40%">
        <el-form ref="form" :model="userInfo" :rules="rules">
            <!-- // ... 省略其他  -->
            <el-form-item label="自定义" prop="randomValue">
                <custom-input  v-model="userInfo.randomValue" />
            </el-form-item>
            <el-form-item>
                <el-button type="primary" @click="handleSubmitInfo">提交</el-button>
            </el-form-item>
        </el-form>
    </div>
</template>
<script>
import CustomInput from './custom-input'

export default {
    data(){
        // ... 
        function validateAgeRandomValue(rule,value,callback){ // 对自定义组件的值进行自定义校验规则
            try{
                if(value===''){
                    callback('请输入随机数')
                }
                if(value<3){
                    callback('太小了')
                }
                if(value>7){
                    callback('太大了')
                }
                callback()
            }catch(err){
                callback('出错了')
            }
        }
        return {
            customInputValue:"",
            hobby:"",
            hobbies:['篮球','读书','游戏','唱歌','跳舞'],
            userInfo:{
                name:'',
                age:'',
                gender:'1',
                hobby:[],
                birthday:'',
                randomValue:'',
            },
            rules:{
                name:[{required:true,message:'请输入姓名'}],
                age:[{validator:validateAge,trigger:'change'}],
                randomValue:[{required:true,message:'请输入一个随机值'},{validator:validateAgeRandomValue}], // 自定义规则
            }
        }
    },
    components:{
        CustomInput
    },
  	// ...
}
</script>

测试,搞定! ✅

image-20210408205641948.png

本来还想继续写v-model 源码实现,看的好累, 东西太多了,看的头大;先不加了 😜

;