最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue模仿ElementUI的form表单

    正文概述 掘金(沵算what)   2021-03-10   449

    实现要求

    • 模仿 ElementUI 的表单,分为四层结构:index 组件、Form 表单组件、FormItem 表单项组件、InputCheckBox 组件,具体分工如下:
    • index 组件:
      • 实现:分别引入 Form 组件、FormItem 组件、Input 组件,实现组装;
    • Form 表单组件:
      • 实现:预留插槽、管理数据模型 model、自定义校验规则 rules、全局校验方法 validate
    • FormItem 表单项组件:
      • 实现:预留插槽、显示 label 标签、执行数据校验、显示校验结果;
    • InputCheckBox 组件:
      • 实现:绑定数据模型 v-model、通知 FormItem 组件执行校验;

    Input 组件

    具体实现如下:

    1、自定义组件要实现 v-model 必须实现 :value@input

    2、当输入框中的数据发生变化时,通知父组件执行校验。

    3、当 Input 组件绑定的 type 类型为 password 时,在组件内部使用 v-bind="$attrs" 获取 props 之外的内容。

    4、设置 inheritAttrsfalse, 从而避免顶层容器继承属性。

    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>
    

    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>

    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() 方法:

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

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元