实现要求
模仿 ElementUI 的表单,分为四层结构:index 组件、Form 表单组件、FormItem 表单项组件、Input 和 CheckBox 组件,具体分工如下:
index 组件:
- 实现:分别引入 Form 组件、FormItem 组件、Input 组件,实现组装;
Form 表单组件:
- 实现:预留插槽、管理数据模型 model、自定义校验规则 rules、全局校验方法 validate;
FormItem 表单项组件:
- 实现:预留插槽、显示 label 标签、执行数据校验、显示校验结果;
Input 和 CheckBox 组件:
- 实现:绑定数据模型 v-model、通知 FormItem 组件执行校验;
Input 组件
具体实现如下:
1、自定义组件要实现 v-model 必须实现 :value 和 @input。
2、当输入框中的数据发生变化时,通知父组件执行校验。
3、当 Input 组件绑定的 type 类型为 password 时,在组件内部使用 v-bind=”$attrs” 获取 props 之外的内容。
4、设置 inheritAttrs 为 false, 从而避免顶层容器继承属性。
Input 组件实现:
<template> <div> <input :value="value" @input="onInput" v-bind="$attrs" /> </div> </template> <script> export default { inheritAttrs: false, // 避免顶层容器继承属性 props: { value: { type: String, default: "" } }, data() { return {}; }, methods: { onInput(e) { // 通知父组件数值发生变化 this.$emit("input", e.target.value); // 通知 FormItem 执行校验 // 这种写法不健壮,因为 Input 组件和 FormItem 组件之间可能会隔代 this.$parent.$emit("validate"); } } }; </script> <style scoped></style>
注意:代码中使用 this.$parent 派发事件,这种写法不健壮,当 Input 组件和 FormItem 组件之间隔代时会出现问题。具体解决方式见文章尾部代码优化部分。
CheckBox 组件
1、自定义实现 checkBox 的双向数据绑定,和 input 大同小异,必须实现 :checked 和 @change。
CheckBox 组件实现:
<template> <section> <input type="checkbox" :checked="checked" @change="onChange" /> </section> </template> <script> export default { props: { checked: { type: Boolean, default: false } }, model: { prop: "checked", event: "change" }, methods: { onChange(e) { this.$emit("change", e.target.checked); this.$parent.$emit("validate"); } } }; </script> <style scoped lang="less"></style>
FormItem 组件
具体实现如下:
1、给 Input 组件或者 CheckBox 组件预留插槽。
2、如果用户在组件上设置 label 属性,要展示 label 标签。
3、监听校验事件,并执行校验(使用 async-validator 插件进行校验)。
4、如果不符合校验规则,需要显示校验结果。
在开发的过程中,我们需要思考几个问题:
1、在组件内部,如何得到需要校验的数据和校验规则?
2、在 Form 表单中会有多个菜单项,如:用户名、密码、邮箱…等等,那么 FormItem 组件是如何得知现在校验的是哪个菜单呢?
FormItem 组件实现:
<template> <div class="formItem-wrapper"> <div class="content"> <label v-if="label" :style="{ width: labelWidth }">{{ label }}:</label> <slot></slot> </div> <p v-if="errorMessage" class="errorStyle">{{ errorMessage }}</p> </div> </template> <script> import Schema from "async-validator"; export default { inject: ["formModel"], props: { label: { type: String, default: "" }, prop: String }, data() { return { errorMessage: "", labelWidth: this.formModel.labelWidth }; }, mounted() { // 监听校验事件,并执行校验 this.$on("validate", () => { this.validate(); }); }, methods: { validate() { // 执行组件的校验 // 1、获取数据 const values = this.formModel.model[this.prop]; // 2、获取校验规则 const rules = this.formModel.rules[this.prop]; // 3、执行校验 const schema = new Schema({ [this.prop]: rules }); // 参数1是值,餐数2是校验错误对象数组 // validate 返回的是 Promise<Boolean> return schema.validate({ [this.prop]: values }, errors => { if (errors) { this.errorMessage = errors[0].message; } else { this.errorMessage = ""; } }); } } }; </script> <style scoped lang="less"> @labelWidth: 90px; .formItem-wrapper { padding-bottom: 10px; } .content { display: flex; } .errorStyle { font-size: 12px; color: red; margin: 0; padding-left: @labelWidth; } </style>
我们先回答一下上面提出的两个问题,此处会涉及到组件之间传值,可以参考之前的文章《组件传值、通讯》:
首先表单的数据和校验规则是定义在 index 组件内部,并且挂载在 Form 组件上,表单的校验项发生在 FormItem 组件中,先在 Form 组件内部通过 props 接受到传递的数据,然后通过 provide/inject 的方式在 FormItem 组件中传递给后代组件。
我们日常在用 ElementUI 的表单校验是会发现,在每一个需要校验的表单上会设置一个 prop 属性,并且属性值和绑定的数据一致。此处的用途是为了能够在 FormItem 组件中执行校验时获取相对的校验规则和数据对象。
在 FormItem 组件中通过使用 inject 获取注入的 Form 实例,和 prop 属性组合使用,可以获取到表单数据和校验规则。
// 1、获取数据 const values = this.formModel.model[this.prop]; // 2、获取校验规则 const rules = this.formModel.rules[this.prop];
使用 async-validator 插件实例化一个 schema 对象,用来执行校验,schema.validate 需要传递两个参数,参数1是当前需要校验的字段和相对应的 rules 组成的键值对对象,参数2是一个 callback 函数,用来获取错误信息(是一个数组)。validate 方法返回的是一个 Promise<Boolean>。
注意:此组件的 validate 方法中,最后使用 return 的目的是为了在 Form 组件中执行全局校验使用。
Form 组件
具体实现如下:
1、给 FormItem 组件预留插槽。
2、传递 Form 实例给后代,比如 FormItem 用来获取校验的数据和规则。
3、执行全局校验
Form 组件实现:
<template> <div> <slot></slot> </div> </template> <script> export default { provide() { return { formModel: this // 传递 Form 实例给后代,比如 FormItem 用来获取校验的数据和规则 }; }, props: { model: { type: Object, required: true }, rules: { type: Object }, labelWidth: String }, data() { return {}; }, methods: { validate(cb) { // 执行全局校验 // map 结果是若干 Promise 数组 const tasks = this.$children.filter(item => item.prop).map(item => item.validate()); // 所有任务必须全部校验成功才算校验通过 Promise.all(tasks) .then(() => { cb(true); }) .catch(() => { cb(false); }); } } }; </script> <style scoped></style>
我们在 Form 组件中使用 provide 注入当前组件对象,方便后续子孙代获取数据/方法使用。
执行全局校验的时候,先使用 filter 过滤掉不需要校验的组件(我们在 FormItem 组件上设置的 prop 属性,只要有此属性,就是需要校验的),然后分别执行组件中的 validate 方法(如果在 FormItem 组件中不使用 return 数据,最后获取到的全都是 undefined),返回的是一个若干 Promise 数组。
简单介绍一个 Promise.all() 方法:
Promise.all() 方法接收一个promise的iterable类型(注:Array,Map,Set都属于ES6的iterable类型)的输入,并且只返回一个Promise实例, 那个输入的所有promise的resolve回调的结果是一个数组。这个Promise的resolve回调执行是在所有输入的promise的resolve回调都结束,或者输入的iterable里没有promise了的时候。它的reject回调执行是,只要任何一个输入的promise的reject回调执行或者输入不合法的promise就会立即抛出错误,并且reject的是第一个抛出的错误信息。
index 组件
定义模型数据、校验规则等等,分别引入 Form 组件、FormItem 组件、Input 组件,实现组装。
index 组件实现:
<template> <div> <Form :model="formModel" :rules="rules" ref="loginForm" label-width="90px"> <FormItem label="用户名" prop="username"> <Input v-model="formModel.username"></Input> </FormItem> <FormItem label="密码" prop="password"> <Input type="password" v-model="formModel.password"></Input> </FormItem> <FormItem label="记住密码" prop="remember"> <CheckBox v-model="formModel.remember"></CheckBox> </FormItem> <FormItem> <button @click="onLogin">登录</button> </FormItem> </Form> </div> </template> <script> import Input from "@/components/form/Input"; import CheckBox from '@/components/form/CheckBox' import FormItem from "@/components/form/FormItem"; import Form from "@/components/form/Form"; export default { data() { const validateName = (rule, value, callback) => { if (!value) { callback(new Error("用户名不能为空")); } else if (value !== "admin") { callback(new Error("用户名错误 - admin")); } else { callback(); } }; const validatePass = (rule, value, callback) => { if (!value) { callback(false); } else { callback(); } }; return { formModel: { username: "", password: "", remember: false }, rules: { username: [{ required: true, validator: validateName }], password: [{ required: true, message: "密码必填" }], remember: [{ required: true, message: "记住密码必选", validator: validatePass }] } }; }, methods: { onLogin() { this.$refs.loginForm.validate(isValid => { if (isValid) { alert("登录成功"); } else { alert("登录失败"); } }); } }, components: { Input, CheckBox, FormItem, Form } }; </script> <style scoped></style>
当我们点击登录按钮时,会执行全局校验方法,我们可以使用 this.$refs.xxx 获取 DOM 元素和组件实例。
在上面我们还留了一个小尾巴~,就是在 Input 组件中通知父组件执行校验,目前使用的是 this.$parent.$emit(),这样写有一个弊端,就是当 Input 组件和 FormItem 组件之后隔代的时候,再使用 this.$parent 获取不到 FormItem 组件。
我们可以封装一个 dispatch 方法,主要实现向上循环查找父元素并且派发事件,代码实现如下:
dispatch(eventName, data) { let parent = this.$parent; // 查找父元素 while (parent) { // 父元素用$emit触发 parent.$emit(eventName, data); // 递归查找父元素 parent = parent.$parent; } }
该方法可以借用 mixins 引入使用:mixins/emmiters.js
export default { methods: { dispatch(eventName, data) { let parent = this.$parent; // 查找父元素 while (parent) { // 父元素用$emit触发 parent.$emit(eventName, data); // 递归查找父元素 parent = parent.$parent; } } } };
修改 Input 组件:
<template> <div> <input :value="value" @input="onInput" v-bind="$attrs" /> </div> </template> <script> import emmiter from "@/mixins/emmiter"; export default { inheritAttrs: false, // 避免顶层容器继承属性 mixins: [emmiter], props: { value: { type: String, default: "" } }, data() { return {}; }, methods: { onInput(e) { // 通知父组件数值发生变化 this.$emit("input", e.target.value); // 通知 FormItem 执行校验 // 这种写法不健壮,因为 Input 组件和 FormItem 组件之间可能会隔代 // this.$parent.$emit("validate"); this.dispatch("validate"); // 使用 mixin 中 emmiter 的 dispatch,解决跨级问题 } } }; </script> <style scoped></style>
总结
到此这篇关于Vue模仿ElementUI的form表单的文章就介绍到这了,更多相关Vue模仿ElementUI form表单内容请搜索NICE源码以前的文章或继续浏览下面的相关文章希望大家以后多多支持NICE源码!