Implement requirements

  • A form that mimics ElementUI is divided into four layers:indexComponents,FormForm components,FormItemForm item components,InputCheckBoxComponents, the specific division of labor is as follows:
  • indexComponents:
    • Implementation: introduced separatelyFormComponents,FormItemComponents,InputComponent, realize assembly;
  • FormForm components:
    • Implementation: reserved slots, management data modelmodel, user-defined verification rulesrules, global verification methodvalidate;
  • FormItemForm item component:
    • Implementation: reserved slot, displaylabelLabel, perform data verification, display verification results;
  • InputCheckBoxComponents:
    • Implementation: Bind the data modelv-model, notifications,FormItemComponent verification;

Input component

The concrete implementation is as follows:

1, custom components to implement V-model must implement :value and @input.

2. Inform the parent component to perform verification when the data in the input box changes.

3. If the type of the Input component is password, run the v-bind=”$attrs” command inside the component to get anything other than props.

Set inheritAttrs to false to avoid inheriting attributes from the top-level container.

The Input component implements:

<template> <div> <input :value="value" @input="onInput" v-bind="$attrs" /> </div> </template> <script> export default { InheritAttrs: false, // Avoid inheriting attributes at the top layer. Props: {value: {type: String, default: ""}}, data() {return {}; }, methods: {onInput(e) {this.$emit("input", e.target.value); $emit("validate"); // This.$parent.$emit("validate"); }}}; </script> <style scoped></style>Copy the code

Note: the code uses this.$parent to issue events, which is not robust and can cause problems when the Input component and the FormItem component are intergenerational. See the code optimization section at the end of this article for specific solutions.

The CheckBox component

1, custom implementation of checkBox two-way data binding, and input much the same, must be implemented :checked and @change.

CheckBox component implementation:

<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>Copy the code

FormItem components

The concrete implementation is as follows:

1. Reserve a slot for the Input component or CheckBox component.

2. If the user sets the label property on the component, display the label label.

3. Listen for validation events and perform validation (using async-Validator plugin for validation).

4. If the verification rule is not met, display the verification result.

During the development process, we need to consider several questions:

1. Inside the component, how to obtain the data and verification rules to be verified?

2. There are multiple menu items in the Form, such as user name, password, email… Wait, so how does the FormItem component know which menu is being verified?

The FormItem component implements:

<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 }; $this.$on("validate", () => {this.validate(); }); }, methods: {validate() {const values = this.formModel.model[this.prop]; Const rules = this.formModel.rules[this.prop]; // const rules = this.formModel.rules[this.prop]; // 3, execute validation const schema = new schema ({[this.prop]: rules}); <Boolean> return schema.validate({[this.prop]: {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>Copy the code

Let’s first answer the above two questions, which involve the transfer of values between components. Please refer to the previous article “Component Transfer, Communication” :

First, the data and validation rules of the Form are defined in the index component and mounted on the Form component. The validation items of the Form occur in the FormItem component, and the data passed by the props is received in the Form component first. It is then passed to descendant components in the FormItem component as provide/ Inject.

In our daily form validation with ElementUI, we find that a prop property is set on every form that needs validation, and the value of the property is consistent with the bound data. The purpose here is to be able to get relative validation rules and data objects when performing validation in the FormItem component.

The FormItem component uses inject to get the injected Form instance, combined with the Prop property, to get Form data and validation rules.

// 1
const values = this.formModel.model[this.prop];

// 2. Obtain the verification rule
const rules = this.formModel.rules[this.prop];
Copy the code

The validator plugin is used to instantiate a schema object for validation. Schema. validate needs to be passed as two parameters. Parameter 1 is the key-value object consisting of the field to be validated and the corresponding rules. Parameter 2 is a callback function that retrieves error information (an array). The validate method returns a Promise

.

Note: In this component’s validate method, the final purpose of the return is to perform global validation in the Form component.

The Form component

The concrete implementation is as follows:

1. Reserve slots for FormItem components.

2. Pass Form instances to descendants, such as FormItem, to get validation data and rules.

3. Perform global verification

Form component implementation:

<template> <div> <slot></slot> </div> </template> <script> export default { provide() { return { formModel: This // passes the Form instance to the descendant, such as FormItem, to get the validation data and rules}; }, props: { model: { type: Object, required: true }, rules: { type: Object }, labelWidth: String }, data() { return {}; }, methods: 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>Copy the code

We use provide in the Form component to inject the current component object for future generations to retrieve data/methods for use.

When performing global validation, use filter first to filter out components that do not need validation (the prop property we set on the FormItem component needs validation as long as it has this property). The component’s validate method is then executed (if you do not use return data in the FormItem component, all you get is undefined), returning an array of promises.

A brief introduction to the promise.all () method:

The promise.all () method accepts an iterable of promises. Array, Map, and Set are all ES6 iterable types), and only one Promise instance is returned. The result of the resolve callbacks to all of those entered promises is an Array. The Promise’s resolve callback is executed when all the incoming Promise’s resolve callbacks have ended, or when no promises are left in the input iterable. Its Reject callback execution throws an error as soon as any incoming promise’s Reject callback is executed or an invalid promise is entered, and reject is the first error message thrown.

The index component

Define model data, check rules and so on, respectively introduce Form component, FormItem component, Input component, realize assembly.

Index component implementation:

<template> <div> <Form :model="formModel" :rules="rules" ref="loginForm" label-width="90px"> <FormItem label=" user name" Prop ="username"> <Input V-model ="formModel.username"></Input> </FormItem> <FormItem label=" password" prop="password"> <Input Type ="password" V-model ="formModel.password"></Input> </FormItem> <FormItem label=" remember password" prop="remember"> <CheckBox V-model ="formModel.remember"></CheckBox> </FormItem> </FormItem> <button @click="onLogin">  </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(" username cannot be null ")); } else if (value ! == "admin") {callback(new Error(" username 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: "Password required"}], remember: [{required: true, message: "Remember password mandatory ", validator: validatePass}]}}; }, methods: {onLogin() {this.$refs.loginform. Validate (isValid => {if (isValid) {alert(" login succeeded ");}, methods: {onLogin() {this. } else {alert(" login failed "); }}); } }, components: { Input, CheckBox, FormItem, Form } }; </script> <style scoped></style>Copy the code

When we click the login button, the global validation method is performed, and we can use this.$refs.xxx to get DOM elements and component instances.


$emit(); $emit(); $emit(); $emit(); $emit(); This.$parent does not get the FormItem component.

We can encapsulate a dispatch method that loops up to find the parent element and dispatches the event as follows:

dispatch(eventName, data) {
  let parent = this.$parent;
  // Find the parent element
  while (parent) {
    // The parent element fires with $emit
    parent.$emit(eventName, data);
    // Find the parent element recursivelyparent = parent.$parent; }}Copy the code

This method can be introduced using mixins: mixins/emmiters.js

export default {
  methods: {
    dispatch(eventName, data) {
      let parent = this.$parent;
      // Find the parent element
      while (parent) {
        // The parent element fires with $emit
        parent.$emit(eventName, data);
        // Find the parent element recursivelyparent = parent.$parent; }}}};Copy the code

Modify the Input component:

<template> <div> <input :value="value" @input="onInput" v-bind="$attrs" /> </div> </template> <script> import emmiter from "@/mixins/emmiter"; Export default {inheritAttrs: false, // Avoid the inheritAttrs attribute mixins: [emmiter], props: {value: {type: String, default: "" } }, data() { return {}; }, methods: {onInput(e) {this.$emit("input", e.target.value); // this.$parent.$emit("validate"); // This. this.dispatch("validate"); // Use the dispatch of emmiter in mixins to solve cross-level problems}}}; </script> <style scoped></style>Copy the code