手把手带你封装一个form表单

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>
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值