Vue模仿ElementUI的form表单实例代码

 4173

这篇文章主要给大家介绍了关于Vue模仿ElementUIform表单的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

实现要求

模仿 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 组件实现:

  1. <template>
  2.     <div>
  3.         <input :value="value" @input="onInput" v-bind="$attrs" />
  4.     </div>
  5. </template>
  6. <script>
  7. export default {
  8.     inheritAttrs: false, // 避免顶层容器继承属性
  9.     props: {
  10.         value: {
  11.             type: String,
  12.             default: ""
  13.         }
  14.     },
  15.     data() {
  16.         return {};
  17.     },
  18.     methods: {
  19.         onInput(e) {
  20.             // 通知父组件数值发生变化
  21.             this.$emit("input", e.target.value);
  22.  
  23.             // 通知 FormItem 执行校验
  24.             // 这种写法不健壮,因为 Input 组件和 FormItem 组件之间可能会隔代
  25.             this.$parent.$emit("validate");
  26.         }
  27.     }
  28. };
  29. </script>
  30. <style scoped></style>

注意:代码中使用 this.$parent 派发事件,这种写法不健壮,当 Input 组件和 FormItem 组件之间隔代时会出现问题。具体解决方式见文章尾部代码优化部分。

CheckBox 组件

1、自定义实现 checkBox 的双向数据绑定,和 input 大同小异,必须实现 :checked 和 @change。

CheckBox 组件实现:

  1. <template>
  2.     <section>
  3.         <input type="checkbox" :checked="checked" @change="onChange" />
  4.     </section>
  5. </template>
  6. <script>
  7. export default {
  8.     props: {
  9.         checked: {
  10.             type: Boolean,
  11.             default: false
  12.         }
  13.     },
  14.     model: {
  15.         prop: "checked",
  16.         event: "change"
  17.     },
  18.     methods: {
  19.         onChange(e) {
  20.             this.$emit("change", e.target.checked);
  21.             this.$parent.$emit("validate");
  22.         }
  23.     }
  24. };
  25. </script>
  26. <style scoped></style>

FormItem 组件

具体实现如下:

1、给 Input 组件或者 CheckBox 组件预留插槽。

2、如果用户在组件上设置 label 属性,要展示 label 标签。

3、监听校验事件,并执行校验(使用 async-validator 插件进行校验)。

4、如果不符合校验规则,需要显示校验结果。

在开发的过程中,我们需要思考几个问题:

1、在组件内部,如何得到需要校验的数据和校验规则?

2、在 Form 表单中会有多个菜单项,如:用户名、密码、邮箱...等等,那么 FormItem 组件是如何得知现在校验的是哪个菜单呢?

FormItem 组件实现:

  1. <template>
  2.     <div>
  3.         <div>
  4.             <label v-if="label" :style="{ width: labelWidth }">{{ label }}:</label>
  5.             <slot></slot>
  6.         </div>
  7.         <p v-if="errorMessage">{{ errorMessage }}</p>
  8.     </div>
  9. </template>
  10. <script>
  11. import Schema from "async-validator";
  12. export default {
  13.     inject: ["formModel"],
  14.     props: {
  15.         label: {
  16.             type: String,
  17.             default: ""
  18.         },
  19.         prop: String
  20.     },
  21.     data() {
  22.         return {
  23.             errorMessage: "",
  24.             labelWidth: this.formModel.labelWidth
  25.         };
  26.     },
  27.     mounted() {
  28.         // 监听校验事件,并执行校验
  29.         this.$on("validate", () => {
  30.             this.validate();
  31.         });
  32.     },
  33.     methods: {
  34.         validate() {
  35.             // 执行组件的校验
  36.             // 1、获取数据
  37.             const values = this.formModel.model[this.prop];
  38.             // 2、获取校验规则
  39.             const rules = this.formModel.rules[this.prop];
  40.             // 3、执行校验
  41.             const schema = new Schema({
  42.                 [this.prop]: rules
  43.             });
  44.             // 参数1是值,餐数2是校验错误对象数组
  45.             // validate 返回的是 Promise<Boolean>
  46.             return schema.validate({ [this.prop]: values }, errors => {
  47.                 if (errors) {
  48.                     this.errorMessage = errors[0].message;
  49.                 } else {
  50.                     this.errorMessage = "";
  51.                 }
  52.             });
  53.         }
  54.     }
  55. };
  56. </script>
  57. <style scoped>
  58. @labelWidth: 90px;
  59. .formItem-wrapper {
  60.     padding-bottom: 10px;
  61. }
  62. .content {
  63.     display: flex;
  64. }
  65. .errorStyle {
  66.     font-size: 12px;
  67.     color: red;
  68.     margin: 0;
  69.     padding-left: @labelWidth;
  70. }
  71. </style>

我们先回答一下上面提出的两个问题,此处会涉及到组件之间传值,可以《组件传值、通讯》:
首先表单的数据和校验规则是定义在 index 组件内部,并且挂载在 Form 组件上,表单的校验项发生在 FormItem 组件中,先在 Form 组件内部通过 props 接受到传递的数据,然后通过 provide/inject 的方式在 FormItem 组件中传递给后代组件。

我们日常在用 ElementUI 的表单校验是会发现,在每一个需要校验的表单上会设置一个 prop 属性,并且属性值和绑定的数据一致。此处的用途是为了能够在 FormItem 组件中执行校验时获取相对的校验规则和数据对象。

在 FormItem 组件中通过使用 inject 获取注入的 Form 实例,和 prop 属性组合使用,可以获取到表单数据和校验规则。

  1. // 1、获取数据
  2. const values = this.formModel.model[this.prop];
  3.  
  4. // 2、获取校验规则
  5. 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 组件实现:

  1. <template>
  2.     <div>
  3.         <slot></slot>
  4.     </div>
  5. </template>
  6. <script>
  7. export default {
  8.     provide() {
  9.         return {
  10.             formModel: this // 传递 Form 实例给后代,比如 FormItem 用来获取校验的数据和规则
  11.         };
  12.     },
  13.     props: {
  14.         model: {
  15.             type: Object,
  16.             required: true
  17.         },
  18.         rules: {
  19.             type: Object
  20.         },
  21.         labelWidth: String
  22.     },
  23.     data() {
  24.         return {};
  25.     },
  26.     methods: {
  27.         validate(cb) {
  28.             // 执行全局校验
  29.             // map 结果是若干 Promise 数组
  30.             const tasks = this.$children.filter(item => item.prop).map(item => item.validate());
  31.             // 所有任务必须全部校验成功才算校验通过
  32.             Promise.all(tasks)
  33.             .then(() => {
  34.                 cb(true);
  35.             })
  36.             .catch(() => {
  37.                 cb(false);
  38.             });
  39.         }
  40.     }
  41. };
  42. </script>
  43. <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 组件实现:

  1. <template>
  2.     <div>
  3.         <Form :model="formModel" :rules="rules" ref="loginForm" label-width="90px">
  4.             <FormItem label="用户名" prop="username">
  5.                 <Input v-model="formModel.username"></Input>
  6.             </FormItem>
  7.             <FormItem label="密码" prop="password">
  8.                 <Input type="password" v-model="formModel.password"></Input>
  9.             </FormItem>
  10.             <FormItem label="记住密码" prop="remember">
  11.                 <CheckBox v-model="formModel.remember"></CheckBox>
  12.             </FormItem>
  13.             <FormItem>
  14.                 <button @click="onLogin">登录</button>
  15.             </FormItem>
  16.         </Form>
  17.     </div>
  18. </template>
  19. <script>
  20. import Input from "@/components/form/Input";
  21. import CheckBox from '@/components/form/CheckBox'
  22. import FormItem from "@/components/form/FormItem";
  23. import Form from "@/components/form/Form";
  24. export default {
  25.     data() {
  26.         const validateName = (rule, value, callback) => {
  27.             if (!value) {
  28.                 callback(new Error("用户名不能为空"));
  29.             } else if (value !== "admin") {
  30.                 callback(new Error("用户名错误 - admin"));
  31.             } else {
  32.                 callback();
  33.             }
  34.         };
  35.         const validatePass = (rule, value, callback) => {
  36.             if (!value) {
  37.                 callback(false);
  38.             } else {
  39.                 callback();
  40.             }
  41.         };
  42.         return {
  43.             formModel: {
  44.                 username: "",
  45.                 password: "",
  46.                 remember: false
  47.             },
  48.             rules: {
  49.                 username: [{ required: true, validator: validateName }],
  50.                 password: [{ required: true, message: "密码必填" }],
  51.                 remember: [{ required: true, message: "记住密码必选", validator: validatePass }]
  52.             }
  53.         };
  54.     },
  55.     methods: {
  56.         onLogin() {
  57.             this.$refs.loginForm.validate(isValid => {
  58.                 if (isValid) {
  59.                     alert("登录成功");
  60.                 } else {
  61.                     alert("登录失败");
  62.                 }
  63.             });
  64.         }
  65.     },
  66.     components: {
  67.         Input,
  68.         CheckBox,
  69.         FormItem,
  70.         Form
  71.     }
  72. };
  73. </script>
  74. <style scoped></style>

当我们点击登录按钮时,会执行全局校验方法,我们可以使用 this.$refs.xxx 获取 DOM 元素和组件实例。

在上面我们还留了一个小尾巴~,就是在 Input 组件中通知父组件执行校验,目前使用的是 this.$parent.$emit(),这样写有一个弊端,就是当 Input 组件和 FormItem 组件之后隔代的时候,再使用 this.$parent 获取不到 FormItem 组件。

我们可以封装一个 dispatch 方法,主要实现向上循环查找父元素并且派发事件,代码实现如下:

  1. dispatch(eventName, data) {
  2.     let parent = this.$parent;
  3.     // 查找父元素
  4.     while (parent) {
  5.         // 父元素用$emit触发
  6.         parent.$emit(eventName, data);
  7.         // 递归查找父元素
  8.         parent = parent.$parent;
  9.     }
  10. }

该方法可以借用 mixins 引入使用:mixins/emmiters.js

  1. export default {
  2.     methods: {
  3.         dispatch(eventName, data) {
  4.             let parent = this.$parent;
  5.             // 查找父元素
  6.             while (parent) {
  7.                 // 父元素用$emit触发
  8.                 parent.$emit(eventName, data);
  9.                 // 递归查找父元素
  10.                 parent = parent.$parent;
  11.             }
  12.         }
  13.     }
  14. };

修改 Input 组件:

  1. <template>
  2.     <div>
  3.         <input :value="value" @input="onInput" v-bind="$attrs" />
  4.     </div>
  5. </template>
  6. <script>
  7. import emmiter from "@/mixins/emmiter";
  8. export default {
  9.     inheritAttrs: false, // 避免顶层容器继承属性
  10.     mixins: [emmiter],
  11.     props: {
  12.         value: {
  13.             type: String,
  14.             default: ""
  15.         }
  16.     },
  17.     data() {
  18.         return {};
  19.     },
  20.     methods: {
  21.         onInput(e) {
  22.             // 通知父组件数值发生变化
  23.             this.$emit("input", e.target.value);
  24.  
  25.             // 通知 FormItem 执行校验
  26.             // 这种写法不健壮,因为 Input 组件和 FormItem 组件之间可能会隔代
  27.             // this.$parent.$emit("validate");
  28.  
  29.             this.dispatch("validate"); // 使用 mixin 中 emmiter 的 dispatch,解决跨级问题
  30.         }
  31.     }
  32. };
  33. </script>
  34. <style scoped></style>


本文网址:https://www.zztuku.com/index.php/detail-8731.html
站长图库 - Vue模仿ElementUI的form表单实例代码
申明:如有侵犯,请 联系我们 删除。

评论(0)条

您还没有登录,请 登录 后发表评论!

提示:请勿发布广告垃圾评论,否则封号处理!!

    编辑推荐