13-开发自己的表单验证组件

368 阅读2分钟

开发组件

整体的一些思路,包括一些问题和解决方法都在代码注释中体现,而且可能提取出来并不能够像直接在代码中看到注释那么更好理解,按照顺序依次对inputformItemform进行一个开发,能够更好的理解,input主要实现的是一个双向数据绑定,当然并不是直接通过一个v-model去实现,v-model 是 v-bind:value 和 v-on:input 的语法糖,这边将通过它们去实现一个双向数据绑定,然后formItem主要是进行一个错误信息的显示,form则是定义数据和校验规则。

App.vue

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <h3>自己的组件</h3>
    {{ruleForm}}
    <!-- 绑定数据和校验规则 -->
    <m-form :model="ruleForm" :rules="rules" ref="ruleForm">
      <!-- prop值用于获取不同输入框的不同校验规则 -->
      <m-form-item label="用户名" prop="name">
        <m-Input v-model="ruleForm.name"></m-Input>
      </m-form-item>
      <m-form-item label="密码" prop="pwd">
        <m-Input type="password" v-model="ruleForm.pwd"></m-Input>
      </m-form-item>
      <m-form-item>
        <!-- button按钮这边就不再去写了 -->
        <button @click="submitForm('ruleForm')">提交</button>
      </m-form-item>
    </m-form>
  </div>
</template>

<script>
//Form 定义数据 负责定义校验规则
//FormItem 负责显示错误信息
//Input 负责双向数据绑定

//provide inject内部共享数据
import MInput from "./components/MyInput";
import MFormItem from "./components/MyFormItem";
import MForm from "./components/MyForm";
export default {
  name: "App",
  components: {
    MInput,
    MFormItem,
    MForm,
  },
  data() {
    return {
      ruleForm: {
        name: "Holo",
        pwd: "1234",
      },
      rules: {
        name: [
          { required: true, message: "请输入用户名" },
          { min: 6, max: 10, message: "请输入6~10位用户名" },
        ],
        pwd: [{ required: true, message: "请输入密码" }],
      },
    };
  },
  methods: {
    submitForm(name) {
      //this.$refs[name]获取的是当前的Form组件,需要在Form组件中定义 validate() 方法
      this.$refs[name].validate((valid) => {
        //valid是校验的结果,对于name、pwd等的一个处理,有多个需要验证,只要有一个不通过则不通过
        //需要进行一个统一的处理 我们可以用到es6中的 Promise.all()来进行处理
        if (valid) {
          alert("验证成功,可以提交");
        } else {
          alert("error 提交");
          return false;
        }
      });
    },
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

MyInput.vue

<template>
  <div>
    <!-- v-model 是 v-bind:value 和 v-on:input 的语法糖-->
    <input :type="type" :value="inputVal" @input="handleInput" />
  </div>
</template>

<script>
//Input 负责双向数据绑定
export default {
  name: "MyInput",
  //接收父组件传值
  props: {
    type: {
      type: String,
      default: "text",
    },
    //父组件中使用时绑定v-model,传值过来
    value: {
      type: String,
      default: "",
    },
  },
  data() {
    return {
      //将父组件传入的值赋给当前input
      inputVal: this.value,
    };
  },
  methods: {
    handleInput(e) {
      //实现双向数据绑定

      //在输入框中输入的同时给当前输入框的value赋值
      this.inputVal = e.target.value;
      //同时我们需要通知父组件那边绑定的v-model的值也进行修改
      //因为不能够在子组件中修改父组件中传入的props值,我们需要想其他的办法
      //通过再次触发自己的input事件并传入值,实现对父组件绑定的v-model的值进行修改
      this.$emit("input", this.inputVal);//子传父值
      /* 这个地方个人感觉很绕很绕,需要自己去理解,抛开其他的,就相当于你给一个input输入框
  绑定了v-model,然后你在输入框中输入的同时,v-model绑定的值也会修改,只不过这边子组件通过
  自己调用自己的input事件,实现了父组件那边的值的一个修改 */

      //紧接着下面我们要做的一件事就是通知父组件对输入框中的值进行一个校验
      //通过'this.$parent'获取父组件,然后触发父组件的验证函数实现
      this.$parent.$emit("validate", this.inputVal);
    },
  },
};
</script>

<style scoped>
</style>

MyFormItem.vue

<template>
  <div>
    <!-- 如果有label就显示 -->
    <label v-if="label">{{label}}</label>
    <!-- 留槽,用于插入其他表单组件,如input、button等 -->
    <slot></slot>
    <!-- 如果校验的状态值是error,则显示错误信息 -->
    <p v-if="validateStatus==='error'" class="error">{{errorMessage}}</p>
  </div>
</template>

<script>
//FormItem 负责显示错误信息

//0:绑定label和prop属性
//1.获取当前输入框的规则
//2.如果输入框和rule不匹配 显示错误信息
//3.Input组件中用户输入内容时,通知FormItem做校验(校验的方法)
//4.使用async-validator做校验   npm i async-validator -s 安装

//通过'npm i async-validator -s'指令安装并导入验证库
import Schema from "async-validator";

export default {
  name: "MyFormItem",
  props: {
    label: {
      type: String,
      defaule: "",
    },
    //prop值用于获取不同输入框的不同校验规则
    prop: {
      type: String,
      default: "",
    },
  },
  //通过 inject 获取父组件对象
  inject: ["form"],
  //项目加载的时候,子组件内部的方法先去走完,然后再走父组件的方法
  mounted() {
    //进行一个判断,如果prop有值,则其需要进行验证
    if (this.prop) {
      this.$parent.$emit("formItemAdd", this);
    }
  },
  created() {
    //接收一下子组件那边的一个触发事件
    this.$on("validate", this.validate);
  },
  data() {
    return {
      validateStatus: "", //校验的状态值
      errorMessage: "", //错误信息
    };
  },
  methods: {
    //输入框的值
    validate(value) {
      //因为父组件Form需要通过Promise.all()对所有子组件的校验结果进行一个统一的处理,
      //所以需要每一个FormItem验证都返回一个Promise对象
      //接着Form需要接收所有的FromItem组件实例,通过组件实例就能获取validate方法,
      //需要考虑生命周期,在 mounted 方法中实现

      return new Promise((resolve) => {
        //接下来准备对数据进行一个验证,但是需要从父组件中获取校验规则
        //此时,先停下来,去对父组件Form进行一个开发
        //获取校验对象 ==》Form组件对象 ==> rules[this.prop]
        //我们可能会想到通过this.$parent.rules[this.prop]来获取,但是,
        //我们需要考虑FormItem外层不是Form的情况
        //上面这种方法虽然能获取到,但是存在问题。如果FormItem的外层是一个div,
        //那么this.$paretn获取的时外层的div,div上可没有我们想要的属性
        //此时我们可以使用 provide 和 inject 来进行一个内部的数据共享
        //然后我们就可以通过 this.form.rules[this.prop] 获取父组件中的校验规则
        //console.log(this.form.rules[this.prop]);

        //接下来就可以构造校验对象,存储校验规则
        //由于this.prop是动态的,所以我们可以通过以下方式来进行一个对象的赋值
        /* let descriptor = {};
      descriptor[this.prop] = this.form.rules[this.prop]; */
        //构造校验的对象
        /* let obj = {};
      obj[this.prop] = value; */

        //对于上面的动态key的一个情况,可以采用es6的一个写法
        const descriptor = {
          [this.prop]: this.form.rules[this.prop],
        };

        const validator = new Schema(descriptor);
        //调用 validate 方法进行验证 传入 校验对象、回调函数
        validator.validate({ [this.prop]: value }, (errors) => {
          //console.log(errors);
          //errors是经过对应的校验对象校验之后的结果,是一个数组,有错误返回对应的错误信息,
          //没有错误则返回null
          if (errors) {
            //如果有错误,显示错误
            this.validateStatus = "error";
            this.errorMessage = errors[0].message;

            //返回校验结果
            resolve(false);
          } else {
            //如果没有,错误置空
            this.validateStatus = "";
            this.errorMessage = "";

            //返回校验结果
            resolve(true);
          }
        });
      });
      //到这里,数据的一个校验已经完成,接下来再回到Form,对数据提交的一个处理
    },
  },
};
</script>

<style scoped>
.error {
  color: red;
}
</style>

MyForm.vue

<template>
  <div>
    <!-- 结构上通过slot留槽即可 -->
    <slot></slot>
  </div>
</template>

<script>
//Form 定义数据 负责定义校验规则
export default {
  //接收数据模型(model)和校验规则
  props: {
    model: {
      type: Object,
      required: true,
    },
    rules: {
      type: Object,
    },
  },
  //我们需要将校验规则传递给子组件,问题来了,slot插槽无法关联子组件,此时我们需要用
  //到provide和inject进行内部共享数据
  provide() {
    //我们直接将当前的父组件对象传递
    return {
      form: this,
    };
  },
  //对所有的FormItem子组件进行一个接收
  created() {
    //缓存所有需要验证的表单项
    this.fileds = [];
    this.$on("formItemAdd", (item) => {
      this.fileds.push(item);
    });
  },
  methods: {
    validate(callback) {
      //获取所有的验证结果进行统一处理,只要有一个失败则失败
      //在当前的Form组件中拿不到验证结果,需要对FormItem组件的validate()方法进行一个修改,
      //使它返回一个Promise对象
      //然后在Form中将每一个FormItem对象获取添加到一个数组,接着就可以通过Promis.all()进
      //行一个处理

      //保存校验后的多个Promise对象
      const promises = this.fileds.map((item) =>
        //这边暂时通过 item.$children[0].inputVal 获取一下输入框中的值,初学者能力有限。。。
        item.validate(item.$children[0].inputVal)
      );

      //定义一个标记
      let flag = true;
      Promise.all(promises).then((res) => {
        //res是每一个表单项校验返回的结果,是一个true,false数组
        res.forEach((valid) => {
          if (!valid) {
            //如果有一个表单项验证不通过,则不通过
            flag = false;
          }
        });
        //将最后的结果返回
        callback(flag);
      });
    },
  },
};
</script>

<style scoped>
</style>

开发中用到的一些方法、插件等

map

map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。

语法

arr.map(callback(currentValue,[index],[array]))

callback 生成新数组元素的函数,使用三个参数:

  1. currentValue; callback 数组中正在处理的当前元素。
  2. index可选; callback 数组中正在处理的当前元素的索引。
  3. array可选; map 方法调用的数组。

返回值

一个由原数组每个元素执行回调函数的结果组成的新数组。

async-validator

具体参考 github.com/yiminghe/as…

props数据格式

类型: Array<string> | Object

详细:

props 可以是数组或对象,用于接收来自父组件的数据。props 可以是简单的数组,或者使用对象作为替代,对象允许配置高级选项,如类型检测、自定义验证和设置默认值。

你可以基于对象的语法使用以下选项:

  1. type:可以是下列原生构造函数中的一种:StringNumberBooleanArrayObjectDateFunctionSymbol、任何自定义构造函数、或上述内容组成的数组。会检查一个 prop 是否是给定的类型,否则抛出警告。
  2. defaultany 为该 prop 指定一个默认值。如果该 prop 没有被传入,则换做用这个值。对象或数组的默认值必须从一个工厂函数返回。
  3. requiredBoolean 定义该 prop 是否是必填项。在非生产环境中,如果这个值为 truthy 且该 prop 没有被传入的,则一个控制台警告将会被抛出。
  4. validatorFunction 自定义验证函数会将该 prop 的值作为唯一的参数代入。在非生产环境下,如果该函数返回一个 falsy 的值 (也就是验证失败),一个控制台警告将会被抛出。

关于props的更多信息 cn.vuejs.org/v2/api/#pro…