模仿Element-UI设计一个Vue的Form表单组件

1,909 阅读1分钟

1、使用Element-UI的表单组件

首先,我们先来看一下Element-UI的Form表单是怎么使用的:

<!--EleForm.vue-->
<template>
  <div>
    <el-form :model="ruleForm" :rules="rules" ref="ruleForm">
      <el-form-item label="用户名" prop="name">
        <el-input v-model="ruleForm.name"></el-input>
      </el-form-item>
      <el-form-item label="密码" prop="password">
        <el-input type="password" v-model="ruleForm.password"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button @click="login('ruleForm')">登录</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>
<script>
export default {
  data() {
    return {
      // 数据模型
      ruleForm: {
        name: "",
        password: ""
      },
      // 校验规则
      rules: {
        name: [{ required: true, message: "请输入用户名" }],
        password: [{ required: true, message: "请输入密码" }]
      }
    };
  },
  methods: {
    login(formName) {
      this.$refs[formName].validate(valid => {
        if (valid) {
          alert("submit!");
        } else {
          alert("error submit!!");
        }
      });
    }
  }
};
</script>

从上面的代码分析可得,Element-UI的表单组件的设计思路是这样的:

  • el-form:管理数据模型【model】和校验规则【rules】,提供全局的校验方法【validate】
  • el-form-item:显示label【label】标签,执行校验【prop】,显示校验结果
  • el-input:绑定数据模型【v-model】,通知FormItem进行校验

按照这个思路,我们带着下面的问题来设计自己的表单组件:

  • el-input是自定义组件,怎么实现v-model?
  • el-form-item怎么知道何时进行校验?校验的数据和规则怎么得到?
  • el-form怎么进行全局校验?它如何把数据模型和校验规则传递给内部组件?

2、实现MyInput

  • 问题:el-input是自定义组件,怎么实现v-model?

v-model是语法糖,自定义组件实现双绑只需指定:value="val" + @input = "val=e.target.value"

<!--MyInput.vue-->
<template>
  <div>
    <input :type="type" :value="value" @input="inputHandler" />
  </div>
</template>
<script>
export default {
  name: "MyInput",
  props: {
    type: {
      type: String,
      default: "text"
    },
    value: {
      type: String,
      default: ""
    }
  },
  methods: {
    inputHandler(e) {
      // 仅派发事件
      this.$emit("input", e.target.value);
      // 通知校验
      // 为什么这里是this.$parent.$emit,而不是this.$emit?
      this.$parent.$emit("validate");
      
    }
  }
};
</script>

3、实现MyFormItem

<!--MyFormItem.vue-->
<template>
  <div>
    <label v-if="label">{{label}}</label>
    <div>
        <!--这里的插槽插入MyInput.vue内容-->
        <slot></slot>
        <!-- 校验错误信息 -->
        <p v-if="validateStatus === 'error'">{{errorMes}}</p>
    </div>
  </div>
</template>
<script>
// 借助“async-validator”进行表单校验
import Schema from "async-validator";
export default {
  name: "MyFormItem",
  // 注入'from',为了使用form提供的数据模型【model】和校验规则【rules】
  inject: ["form"],
  props: {
    label: {
      type: String,
      default: ""
    },
    // 用于获取指定字段值和校验规则
    prop: {
      type: String,
      default: ""
    }
  },
  data() {
    return {
      errMes: ""
    };
  },
  mounted() {
    // 监听Input组件的“this.$parent.$emit(’validate‘)”
    // 因为MyFormItem组件的input位置一开始只是个插槽,没有<my-input/>标签,所以无法以"<my-input @validate=''/>"监听事件
    this.$on("validate", () => {
      this.validate();
    });
  },
  methods: {
    // 单项校验
    validate() {
      // 1.获取值和校验规则
      const value = this.form.model[this.prop];
      const rule = this.form.rules[this.prop];
      // 2.创建Schema实例 {username: rules}
      const schema = new Schema({ [this.prop]: rule });
      // 3.执行校验,校验对象,回调函数
      // validate返回校验结果Promise
      return schema.validate({ [this.prop]: value }, errors => {
        if (errors) {
          this.errMes = errors[0].message;
        } else {
          this.errMes = "";
        }
      });
    }
  }
};
</script>

4、实现MyForm

<!--MyForm.vue-->
<template>
  <div>
    <!-- 插入<my-form-item/>的内容 -->
    <slot></slot>
  </div>
</template>
<script> 
export default {
  name: "MyForm",
  // 把form实例提供出去,以便后代组件使用它的数据模型和校验规则
  provide() {
    return {
      form: this
    };
  },
  props: {
    model: {
      type: Object,
      required: true
    },
    rules: {
      type: Object
    }
  },
  methods: {
    validate(cb) {
      // 全局校验
      // 1.不是所有项都需要校验, MyFormItem有prop属性才需要校验
      // 2. tasks是promise数组
      const tasks = this.$children.filter(item => item.prop).map(item => item.validate())
      // 3. 所有MyFormItem必须全通过, 整个表单才是验证通过
      Promise.all(tasks)
        .then(() => cb(true))
        .catch(() => cb(false));
    }
  }
};
</script>

5、测试自己的表单组件

<!--TestMyForm.vue-->
<template>
  <div>
    <MyForm :model="model" :rules="rules" ref="myForm">
      <MyFormItem label="用户名" prop="username">
        <MyInput v-model="model.username"></MyInput>
      </MyFormItem>
      <MyFormItem label="密码" prop="password">
        <MyInput v-model="model.password" type="password"></MyInput>
      </MyFormItem>
      <MyFormItem>
        <button @click="login">登录</button>
      </MyFormItem>
    </MyForm>
  </div>
</template>
<script>
import MyForm from "MyForm.vue";
import MyFormItem from "MyFormItem.vue";
import MyInput from "MyInput.vue";
export default {
  name: "TestMyForm",
  components: {
    MyForm,
    MyFormItem,
    MyInput
  },
  data() {
    return {
      model: { username: "", password: "" },
      rules: {
        username: [{ required: true, message: "请输入用户名" }],
        password: [{ required: true, message: "请输入密码" }]
      }
    };
  },
  methods: {
    login() {
      // 全局校验
      this.$refs["myForm"].validate(isValid => {
        if (isValid) {
          alert("submit!");
        } else {
          alert("error submit!!");
        }
      });
    }
  }
};