模仿elementUI风格封装一个表单组件

971 阅读3分钟

本文目标

  • 学会使用elementUI的表单组件
  • 组件的传值方式
  • 封装input组件、带校验的form组件

先看看elementUI的表单组件是怎么用的

下面是从elementUI官网复制过来的代码,可以看出:一个基本的form表单由el-formel-form-itemel-input三个组件组成 1、 el-form:接收两个参数:model接收外部传的表单的数据,rules接收校验规则

2、el-form-item“这个组件主要展示label和显示校验的错误信息

3、el-input:双向绑定

<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>

封装input组件

目标:对m-input在外面用的时候可以使用v-model,同时派发"校验事件"

1、自定义组件要使用v-model,则必须要:value / @input

2、触发input事件的时候,同时派发校验事件,告诉父组件去校验

3、这里用到了v-bind="$attrs": 将调用组件时的组件标签上绑定的非props的特性(class和style除外)向下传递。在子组件中应当添加inheritAttrs: false(避免父作用域的不被认作props的特性绑定应用在子组件的根元素上)。注意: $attrs存储的是props之外的部分

4、与attrs类似的还有:v-on="$listeners":将父组件标签上的自定义事件向下传递其子组件可以直接通过emit(eventName)的方式调用

上面两个属性可以处理组件在其中传递props以及事件的过程中,不必在写多余的代码,仅仅是将$attrs以及$listeners向上或者向下传递即可。

<template>
  <div class="m-input">
    <input :value="value" @input="onInput" v-bind="$attrs">
  </div>
</template>
<script>
    export default {
        inheritAttrs: false, // 避免顶层容器继承属性
        props: {
            value: {
                type: String,
                default: ''
            }
            // 因为用了$attrs 所以placeholder不需要这个props就不需要写了
        },
        methods: {
            onInput(e) {
                // 通知父组件数值变化
                this.$emit('input', e.target.value);
                // 通知FormItem校验
                this.$parent.$emit('validate');
            }
        },
    }
</script>

封装form-item组件

目标:

  • 展示label和校验错误的信息
  • 留插槽,里面放input
  • 对单个项进行校验
<template>
  <div class="m-form-item">
    <i class="warn-icon" v-if="isNecessary&&label">*</i>
    <label v-if="label">
      <span>{{label}}</span>
    </label>
    <slot></slot>
    <p class="warning-msg">{{warningMsg}}</p>
  </div>
</template>

这里的校验需要用到一个校验库async-validator,具体用法见 github地址

// 安装
npm i async-validator
// 用法
import Schema from 'async-validator';
const descriptor = {
  name(rule, value, callback, source, options) {
    const errors = [];
    if (!/^[a-z0-9]+$/.test(value)) {
      errors.push(new Error(
        util.format('%s must be lowercase alphanumeric characters', rule.field),
      ));
    }
    return errors;
  },
};
const validator = new Schema(descriptor);
validator.validate({ name: 'Firstname' }, (errors, fields) => {
  if (errors) {
    return handleErrors(errors, fields);
  }
  // validation passed
});

js部分要做的是接受当前项的数据,以及当前项的校验规则,最后进行校验。但是form-item接收的参数只有两个:labelprop都是字符串,拿不到form表单的数据。 由于可以嵌套多层表单,因此不能只用父子传值,把数据传进来。这时候就涉及vue组件传值、通信的几种方式;

组件传值、通信

父=>子

  • 属性props
  • 引用refs:this.$refs.form.mobile='123'
  • children: this.$children[0].mobile='123'

子=>父

  • 自定义事件:$emit('name',123)/@name

兄弟通信

通过同一个父亲组件搭桥 this.$parent.on('name',123)/this.$parent.emit('name')

爷孙之间

祖父辈通过provide提供数据

provide() {
   return {formData: 123}
}

子孙辈通过inject注入数据 inject['formData']

任意两个组件

用vuex或者eventBus

回到form-item获取到最外层父组件form的数据,因此需要form组件provide当前实例给孙子组件,那么form-item就可以注入整个form表单的实例

import Schema from "async-validator"

data() {
    return {
      isNecessary: false, // 是否必填
      warningMsg: '' // 错误的校验信息
    }
  },
  inject: ['from'], // form表单提供的数据
  props: {
    label: String,
    prop: String
  }

数据以及规则都已经拿到,下面就是进行校验了!下面按照async-validator文档的用法写校验方法

methods: {
    onValidate() {
      console.log(this.prop) //当前item的规则名称
      console.log(this.form) //整个form的实例:{model:外部接收的数据, rules:外部接收的规则}
      const descriptor = { // 这里拿到规则描述
        [this.prop]: this.from.rules[this.prop]
      }
      const validator = new Schema(descriptor);
      return validator.validate({ [this.prop]: this.from.model[this.prop] }, (errors) => {
        if (errors) {
          console.log(errors);
          this.warningMsg = errors[0].message
        } else {
        // 校验通过则清除错误消息
          this.warningMsg = ''
        }
      })
    }
  }

最后监听校验事件,并且执行校验方法

 mounted() {
    if (JSON.stringify(this.from.rules) !== '{}') {
      this.isNecessary = true
    }
    this.$on('validate', () => {
      this.onValidate()
    })
  },

封装form组件

从上面可以知道form组件要做的就是两件事:1、接收数据和校验规则;2、进行全局校验。剩下的就交给form-item来做,所以页面结构就很简单了。只需要留一个插槽就行,里面的都是自定义

<template>
  <div>
    <slot></slot>
  </div>
</template>

js部分就是要处理两件事:

  • 接收外部数据和校验规则数组
  • 对规则进行全局校验
provide() {
    return {
      'from': this // 直接传整个实例过去
    }
  },
props: {
    model: {
      type: Object,
      required: true
    },
    rules: Object
  }

下面是拿到全部form-item的校验方法return出来的结果;注意: async-validator return的校验结果是一个Promise。

这里只需要拿出全部的校验任务,并且执行就可以

 methods: {
    validate(cb) {
      const tasks = this.$children
        .filter(item => item.prop)
        .map(item => item.onValidate());
      //   所有任务必须全部成功才算校验通过
      console.log(tasks); // tasks是一个promise的数组
// Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值
      Promise.all(tasks)
        .then(() => cb(true))
        .catch(() => cb(false));
    }
  }

这样一个基本的表单就完成,用法也与elementUI基本一致

当提交表单的时候只需要通过this.$refs调用formvalidate()方法,就可以对整个表单校验

<template>
  <div class="home">
    <m-form :model="formData" :rules="rules" ref="form">
      <m-form-item label="手机号" prop="phone">
       <m-input type="text" v-model="formData.phone" placeholder="输入手机号" />
      </m-form-item>
      <m-form-item label="密码" prop="password">
        <m-input type="password" v-model="formData.password" />
      </m-form-item>
      <m-form-item>
        <div @click="submit">提交</div>
      </m-form-item>
    </m-form>
  </div>
</template>

<script>
import mForm from '@/components/mForm/index.vue'
import mFormItem from '@/components/mForm/Item.vue'
import mInput from '@/components/mInput/index.vue'
export default {
  name: 'Home',
  components: {
    mForm,
    mFormItem,
    mInput
  },
  data() {
    return {
      formData: {
        phone: '',
        password: ''
      },
      rules: {
        phone: [
          { required: true, message: '请输入手机号' }
        ],
        password: [
          { required: true, message: '请输入密码' }
        ]
      }
    }
  },
  methods: {
    submit() {
      this.$refs.form.validate((valid) => {
        console.log(valid);
        if (valid) {
          alert('submit!');
        } else {
          alert('error!');
          return false;
        }
      });
    }
  },
}
</script>