vue进阶学习之路3-自定义组件Form表单

4,217 阅读6分钟

前两章学习复习了自定义组件所需的两个基础知识点,为自定义组件Form的学习做了基础铺垫。如何自己去写一个自定义组件Form表单我们需要知道的如下:

一、Form表单组件功能需求

  • 实现数据的model绑定
  • 表单验证规则
  • 全局验证

二、分析elementUI结构与技术实现

ui组件一般大多数人就会直接找element,现在我们分析一下elementUI的form表单。 首先,看一下element的官方案例:

<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
    <el-form-item label="活动名称" prop="name">
        <el-input v-model="ruleForm.name"></el-input>
    </el-form-item>
    <el-form-item label="活动区域" prop="region">
        <el-select v-model="ruleForm.region" placeholder="请选择活动区域">
          <el-option label="区域一" value="shanghai"></el-option>
          <el-option label="区域二" value="beijing"></el-option>
        </el-select>
    </el-form-item>
    <el-form-item>
        <el-button type="primary" @click="submitForm('ruleForm')">立即创建</el-button>
        <el-button @click="resetForm('ruleForm')">重置</el-button>
    </el-form-item>
</el-form>
<script>
  export default {
    data() {
      return {
        ruleForm: {
          name: '',
          region: '',
        },
        rules: {
          name: [
            { required: true, message: '请输入活动名称', trigger: 'blur' },
            { min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
          ],
          region: [
            { required: true, message: '请选择活动区域', trigger: 'change' }
          ]
        }
      };
    },
    methods: {
      submitForm(formName) {
        this.$refs[formName].validate((valid) => {
          if (valid) {
            alert('submit!');
          } else {
            console.log('error submit!!');
            return false;
          }
        });
      },
      resetForm(formName) {
        this.$refs[formName].resetFields();
      }
    }
  }
</script>

拿element官网的基础案例来讲,有明确的嵌套关系,form在最外层,里面是formItem,最里面是Input。

  • form的任务:全局的管理数据模型,校验规则和全局校验
  • formItem: 显示标签-label、执行校验和显示校验结果
  • Input:收集数据(绑定数据模型、通知formItem执行校验)

三、自定义的Form表单组件逻辑思路

1、环境技术

  • vue-cli 3.x
  • node.10.x
  • vuejs 2.x

2、项目目录结构

index

form

formItem

qInput

3、遇到的问题点

  • qinput如何实现数据绑定?
  • formItem何时执行校验,校验的数据和规则如何得到?
  • qinput已经绑定数据,为什么form还要v-model绑定?
  • form 怎么进行全局校验?

4、效果

四、自定义Form表单代码实现

A.首页创建一个qinput.vue,格式如下:

<div>
    <!--双绑 :value @input-->
    <input :type="type" :value="value" @input="onInput" v-bind="$attrs">
</div>

这里type不能写死,实现双绑需要value @input ,value值从父组件formItem传递进来,@input事件监听,但是什么事情都不需要做,只需要做事件转发。代码如下:

export default {
  // 1.双绑
  // 2.通知校验
  name: 'yqInput',
  inheritAttrs: false,
  props: {
    type: {
      type: String,
      default: 'text'
    },
    value: {
      type: String,
      default: ''
    }
  },
  methods: {
    onInput (e) {
      // 
      this.$emit('input', e.target.value)
    }
  }
}

这里的emit,不去改值,仅派发事件是一个单向数据流的概念,父组件formItem传值过来后,input组件去显示,将来值放生了变化以后,只需要把变化告诉父组件formItem,这也是双绑的策略。

attrs是为了接收父组件传过来的参数,此类参数是props未声明的,原生特性可以用attrs,并且用v-bind绑定,v-bind会展开attrs。副作用是父组件也会继承属性,所以需要设置inheritAttrs: false, 不继承属性。 效果如下:

B.创建一个index.vue,引入input组件,绑定数据,格式如下:

<template>
    <div>
      <h3>表单</h3>
      <hr>
      <yq-input v-model="model.username" placeholder="请输入用户名"></yq-input>
      {{model}}
    </div>
</template>
<script>
import YqInput from './yqInput'
export default {
  name: 'index.vue',
  components: {
    YqInput
  },
  data () {
    return {
      model: {
        username: 'yq',
        password: ''
      }
    }
  }
}
</script>

效果如下:

C.按照最初设计的逻辑布局格式,创建一个formItem.vue,在formItem中,需要有label标题、报错信息,label标题、报错信息需要v-if判断一下不是必须显示的,如下:

<template>
    <div>
      <!--label-->
      <label v-if="label">{{label}}</label>
      <slot></slot>
      <p v-if="errorMsg">{{errorMsg}}</p>
    </div>
</template>
<script>
export default {
  name: 'formItem',
  props: {
    label: {
      type: String,
      default: ''
    },
    prop: { // 用于获取指定字段的值和校验规则
      type: String,
      default: ''
    }
  },
  data () {
    return {
      errorMsg: ''
    }
  }
}
</script>

同时,在index中引用FormItem组件,如下:

<template>
    <div>
      <h3>表单</h3>
      <hr>
      <form-item label="用户名" prop="username">
        <yq-input v-model="model.username" placeholder="请输入用户名"></yq-input>
      </form-item>
      {{model}}
    </div>
</template>
<script>
import YqInput from './yqInput'
import FormItem from './formItem'
export default {
  name: 'form.vue',
  components: {
    YqInput,
    FormItem
  },
  data () {
    return {
      model: {
        username: 'yq',
        password: ''
      }
    }
  }
}
</script>

D. 创建最外层Form组件,添加provide(为了子代都可以获取祖代Form的参数),接收传入的model与rules,如下:

<template>
    <div>
     <slot></slot>
    </div>
</template>
<script>
export default {
  name: 'form.vue',
  provide () {
    return {
      form: this
    }
  },
  props: {
    model: {
      type: Object,
      required: true
    },
    rules: {
      type: Object
    }
  },
  data () {
    return {
    }
  }
}
</script>

同时,在index中引用Form组件,绑定model与rules属性,加入登录按钮,添加全局验证的点击事件,如下:

<template>
  <div>
    <h3>表单</h3>
    <hr>
    <yq-form :model="model" :rules="rules">
      <form-item label="用户名" prop="username">
        <yq-input v-model="model.username" placeholder="请输入用户名"></yq-input>
      </form-item>
      <form-item>
        <button @click="onlogin">登录</button>
      </form-item>
    </yq-form>
  </div>
</template>
<script>
  import YqInput from './yqInput'
  import FormItem from './formItem'
  import YqForm from './form'
  export default {
    name: 'index.vue',
    components: {
      YqInput,
      FormItem,
      YqForm
    },
    data () {
      return {
        model: {
          username: 'yq',
          password: ''
        },
        rules: {
          username: [
            {required: true, message:'请输入用户名'}
          ]
        }
      }
    },
    methods: {
      onlogin () {
        // 全局校验
      }
    }
  }
</script>

下面做的验证,按照element整理出来的思路,我们在Form组件中注入值,formItem中做验证validate,首先在fomeItme通过inject(省略之前已写过的代码)拿出Form组件中注入值,如下:

...
export default {
    name: 'formItem',
    inject: ['form'],
...

fomeItme的子代组件input要通知校验

原来我们用input去派发事件,但是这样需要在父组件中写到标签上,实际上我们现在的结构input标签不存在,slot上面无法去附加事件,现在只能用另一个办法,可以通过parent去派发事件,修改如下:

methods: {
    onInput (e) {
      // 仅派发事件
      this.$emit('input', e.target.value)
      // 通知校验
      this.$parent.$emit('validate')
    }
但是parent会与父组件产生明显的耦合,element的官方用minxins,minxins的补充在文档的最后。

同时fomrItem自己监听,并执行一个validate方法,如下:

mounted () {
    this.$on('validate', () => {
      this.validate()
    })
  },
methods: {
    validate () {
     // 1.获取值和校验方法
      const rules = this.form.rules[this.prop]
      const value = this.form.model[this.prop]
    }
}

校验规则的首先安装一个库:async-validator,做一些校验规则,对异步的支持非常好,element也是用的这个。github.com/tmpfs/async…

npm i -S async-validator

在formItme中导入这个库,

import Schema from 'async-validator'

在方法中创建Schema的示例,如下:

validate () {
  // 1.获取值和校验方法
  const rules = this.form.rules[this.prop]
  const value = this.form.model[this.prop]
  // 2.创建Schema实例,格式: {username: rules}
  const schema = new Schema({[this.prop]: rules})
  // 3.执行校验,校验对象, 回掉函数
  return schema.validate({[this.prop]: value}, (errors) => {
    if (errors) {
      this.errorMsg = errors[0].message
    } else {
      this.errorMsg = ''
    }
  })
}

return为了告诉外围组件,执行的结果true or false, validate方法回返回一个promise执行结果, 效果:

这是单项校验,下面把全局校验添加上,那么全局校验在点击的时候使用,element在使用是用了ref并且有一个validate的事件,那么在index组件中加上ref(省略相同的代码),如下:

...
 <yq-form :model="model" :rules="rules" ref="loginForm">
...
onlogin () {
    // 全局校验
    this.$refs['loginForm'].validate(isValid => {
      if (isValid) {
        alert('可以登录')
      } else {
        alert('报错')
      }
    })
  }
...

在form组件中添加一个validate的事件,做全局校验,接收一个回调,校验失败还是成功要通过回调函数传出去,而且不是所有的项都需要校验,必须有prop属性的值才能校验,如下改动:

methods: {
    validate (cb) {
      // 全局校验
      // 1.不是所有的项都需要校验,把没有prop属性的过滤掉
      // 返回true就留下,执行留下数组中的所有校验方法,执行组件实例的validate方法,用map执行validate方法
      // tasks是一个promise的map执行结果数组
      const tasks = this.$children.filter(item => item.prop).map(item => item.validate())
       // 必须全部通过,Promise.all结果数组全部通过才能执行then
      Promise.all(tasks)
        .then(() => cb(true))
        .catch(() => cb(false))
    }
}

效果:

promise:es6.ruanyifeng.com/#docs/promi…

现在基本的form表单的绑定数据和验证都完成了,剩下的就自行美化样式。

补充 minxins修改parent的高耦合性问题:

1.首页新建一个minxins.js的文件,用于递向上寻找组件派发事件,用minxins的混入语法,把方法混入到vue的methods方法中,用于组件的扩展,代码如下:

export default {
  methods: {
    dispatch (componentName, eventName, params) { // 组件名称,事件名称,参数
      var parent = this.$parent || this.$root // 先找父组件,找不到找根
      var name = parent.$options.__proto__.name
      while (parent && (!name || name !== componentName)) { //判断名字是否与组件名字一样
        parent = parent.$parent
        if (parent) {
          name = parent.$options.componentName
        }
      }
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params))
      }
    }
  }
}

2.在main.js中引入minxins.js的文件

import Mixin from 'components/uiPc/form/mixins'
Vue.mixin(Mixin)

3.修改formItem组件,如下:

onInput (e) {
  // 仅派发事件
  this.$emit('input', e.target.value)
  // 通知校验
  // this.$parent.$emit('validate')
  this.dispatch('formItem', 'validate', e.target.value)
}