Vue mimics ElementUI's form form instance code

  • 2021-11-01 02:15:39
  • OfStack

Implementation requirements

The form imitating ElementUI is divided into four layers: index component, Form form component, FormItem form item component, Input and CheckBox component. The specific division of labor is as follows:

index component:

Implementation: Introducing Form component, FormItem component and Input component respectively to realize assembly;

Form form components:

Implementation: Reserved slot, management data model model, custom verification rule rules, global verification method validate;

FormItem form item component:

Implementation: Reserved slot, display label label, perform data verification, display verification results;

Input and CheckBox components:

Implementation: Bind data model v-model, notify FormItem component to execute verification;

Input component

The specific implementation is as follows:

1. Custom components must implement v-model: value and @ input.

2. When the data in the input box changes, notify the parent component to perform verification.

3. When the type type bound to the Input component is password, use v-bind= "$attrs" inside the component to obtain content other than props.

4. Set inheritAttrs to false to prevent the top-level container from inheriting properties.

Input component implementation:


<template>
 <div>
 <input :value="value" @input="onInput" v-bind="$attrs" />
 </div>
</template>

<script>
export default {
 inheritAttrs: false, //  Avoid inheriting properties from top-level containers 
 props: {
 value: {
 type: String,
 default: ""
 }
 },
 data() {
 return {};
 },
 methods: {
 onInput(e) {
 //  Notify the parent component of a change in value 
 this.$emit("input", e.target.value);
 
 //  Notice  FormItem  Perform verification 
 //  This writing is not robust because  Input  Components and  FormItem  Components may be alternate generations 
 this.$parent.$emit("validate");
 }
 }
};
</script>

<style scoped></style>

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

CheckBox Component

1. Custom implementation of checkBox bidirectional data binding, which is similar to input, 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>

FormItem component

The specific implementation is as follows:

1. Reserve slots for Input components or CheckBox components.

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

3. Listen for verification events and perform verification (using async-validator plug-in for verification).

4. If the verification rules are not met, the verification results need to be displayed.

In the process of development, we need to think about several issues:

1. How to get the data and verification rules that need to be verified inside the component?

2. There will be multiple menu items in the Form form, such as user name, password, mailbox, etc., so how does the FormItem component know which menu is being verified now?

FormItem component implementation:


<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() {
 //  Listen for validation events and perform validation 
 this.$on("validate", () => {
 this.validate();
 });
 },
 methods: {
 validate() {
 //  Perform verification of components 
 // 1 , get data 
 const values = this.formModel.model[this.prop];
 // 2 Gets the verification rules 
 const rules = this.formModel.rules[this.prop];
 // 3 Perform verification 
 const schema = new Schema({
 [this.prop]: rules
 });

 //  Parameter 1 Yes, number of meals 2 Is an array of validation error objects 
 // validate  Returns the  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>

Let's first answer the two questions raised above under 1. Here, we will involve value transfer between components. Please refer to the previous article "Component Value Transfer and Communication":
Firstly, the data and verification rules of the form are defined in the index component and mounted on the Form component. The verification items of the form occur in the FormItem component. First, the transmitted data is received in the Form component through props, and then transmitted to future generations through provide/inject in the FormItem component.

When we check the form with ElementUI every day, we will find that one prop attribute will be set on every form that needs to be checked, and the attribute value and the bound data are 1. The purpose here is to be able to obtain relative validation rules and data objects when performing validation in the FormItem component.

In the FormItem component, form data and validation rules can be obtained by using inject to get the injected Form instance, combined with the prop attribute.


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

// 2 Gets the verification rules 
const rules = this.formModel.rules[this.prop];

Use async-validator plug-in to instantiate an schema object to perform verification. schema. validate needs to pass two parameters. Parameter 1 is a key-value pair object composed of the field to be verified at present and the corresponding rules, and parameter 2 is an callback function to obtain error information (which is an array). The validate method returns an Promise < Boolean > .

Note: In the validate method of this component, the last use of return is to perform global validation usage in the Form component.

Form Component

The specific implementation is as follows:

1. Reserve slots for FormItem components.

2. Pass the instance of Form to future generations, such as the data and rules used by FormItem to obtain verification.

3. Perform global verification

Form component implementation:


<template>
 <div>
 <slot></slot>
 </div>
</template>

<script>
export default {
 provide() {
 return {
 formModel: this //  Transfer  Form  Example to descendants, such as  FormItem  Data and rules used to obtain verification 
 };
 },
 props: {
 model: {
 type: Object,
 required: true
 },
 rules: {
 type: Object
 },
 labelWidth: String
 },
 data() {
 return {};
 },
 methods: {
 validate(cb) {
 //  Perform global verification 
 // map  The result is several  Promise  Array 
 const tasks = this.$children.filter(item => item.prop).map(item => item.validate());
 //  All tasks must be verified successfully before they are verified 
 Promise.all(tasks)
 .then(() => {
 cb(true);
 })
 .catch(() => {
 cb(false);
 });
 }
 }
};
</script>

<style scoped></style>

We use provide in Form component to inject the current component object, which is convenient for subsequent descendants to obtain data/method.

When performing global verification, first use filter to filter out components that do not need verification (prop attribute set on FormItem component, as long as there is this attribute, it needs verification), and then execute validate method in components respectively (if return data is not used in FormItem component, all undefined is finally obtained), and a number of Promise arrays are returned.

An Promise. all () method is briefly introduced:

The Promise. all () method receives an input of type iterable of promise (note: Array, Map, and Set are all of type iterable of ES6) and returns only one instance of Promise. The result of resolve callbacks of all promise of that input is an array. The resolve callback of the Promise is executed when all resolve callbacks of the incoming promise have ended, or when promise is not in the incoming iterable. Its reject callback execution is that any reject callback of promise that is entered is executed or an illegal promise that is entered will immediately throw an error, and reject is the first to throw an error message.

index Component

Defining model data, checking rules and so on, introducing Form component, FormItem component and Input component to 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 the password " prop="remember">
 <CheckBox v-model="formModel.remember"></CheckBox>
 </FormItem>
 <FormItem>
 <button @click="onLogin"> Login </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(" User name cannot be empty "));
 } else if (value !== "admin") {
 callback(new Error(" User name 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 that the password must be selected ", validator: validatePass }]
 }
 };
 },
 methods: {
 onLogin() {
 this.$refs.loginForm.validate(isValid => {
 if (isValid) {
 alert(" Login Successful ");
 } else {
 alert(" Login failed ");
 }
 });
 }
 },
 components: {
 Input,
 CheckBox,
 FormItem,
 Form
 }
};
</script>

<style scoped></style>

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

We also left a small tail ~ above, that is, notify the parent component to perform verification in Input component. At present, this. $parent. $emit () is used. This writing has a drawback, that is, when Input component and FormItem component are alternate generations later, this. $parent can't get FormItem component.
We can encapsulate an dispatch method, which mainly realizes the upward loop to find the parent element and distribute events. The code implementation is as follows:


dispatch(eventName, data) {
 let parent = this.$parent;
 //  Find parent element 
 while (parent) {
 //  Parent element with $emit Trigger 
 parent.$emit(eventName, data);
 //  Find parent element recursively 
 parent = parent.$parent;
 }
}

This method can be introduced and used by mixins: mixins/emmiters. js


export default {
 methods: {
 dispatch(eventName, data) {
 let parent = this.$parent;
 //  Find parent element 
 while (parent) {
 //  Parent element with $emit Trigger 
 parent.$emit(eventName, data);
 //  Find parent element recursively 
 parent = parent.$parent;
 }
 }
 }
};

Modify Input components:


<template>
 <div>
 <input :value="value" @input="onInput" v-bind="$attrs" />
 </div>
</template>

<script>
import emmiter from "@/mixins/emmiter";

export default {
 inheritAttrs: false, //  Avoid inheriting properties from top-level containers 
 mixins: [emmiter],
 props: {
 value: {
 type: String,
 default: ""
 }
 },
 data() {
 return {};
 },
 methods: {
 onInput(e) {
 //  Notify the parent component of a change in value 
 this.$emit("input", e.target.value);
 
 //  Notice  FormItem  Perform verification 
 //  This writing is not robust because  Input  Components and  FormItem  Components may be alternate generations 
 // this.$parent.$emit("validate");
 
 this.dispatch("validate"); //  Use  mixin  Medium  emmiter  Adj.  dispatch Solve cross-level problems 
 }
 }
};
</script>

<style scoped></style>

Summarize


Related articles: