1. Demand analysis

First, a form should have the basic structure form-> formItem ->input

<template>
    <div>        
    <Form :model="formData" :rules='rules'>   
        <FormItem label="Username" prop='username'>
            <Input v-model='formData.username' placeholder="Please enter user name"></Input>
            {{formData.username}}
        </FormItem>
        <FormItem label="Password" prop='password'>
            <Input type='password' v-model='formData.password' placeholder="Please enter your password"></Input>
            {{formData.password}}
        </FormItem>
    </Form >  
    </div>
</template>

<script>
import Input from "@/components/form/Input.vue";
import Form from "@/components/form/Form.vue";
import FormItem from "@/components/form/FormItem.vue";

export default {
    components: {

        Input,
        FormItem,
        Form
        
    },
    data(){
        return{
            formData: {username:' '.password:' '
            },
            rules: {
                username: [{ required: true.message: "Please enter user name"}].password: [{ required: true.message: "Please enter your password"}}},}</script>

<style>

</style>
Copy the code

So the structure should look like this:

If it’s an containment relationship, if it’s an containment relationship, it’s going to be slot first so let’s put a slot first for the Form and formItem, so the code for them should look something like this

(Yes, you read that right, because form contains formItem, and formitem contains input. Therefore, it is ok to place two slots each at present)

/ / form. Vue and formitem. Vue
<template>
    <div>
        <slot></slot>
    </div>
</template>

Copy the code

2. Realization of foundation construction

Input component: V-model of a custom component

The next step is to set up a custom Input component. The usual V-Model binds the custom Input component bidirectionally, rather than the Input of the native tag. The v-model is essentially a syntactic sugar that simplifies the process of value/@input. Therefore, to implement a custom Input component v-model, you need to implement :value/@input inside the component

See documentation: V-Model for custom components cn.vuejs.org/v2/guide/co…

/ / Input component
<template>
  <div>
    <! -- Custom component bidirectional binding: :value @input -->
    <input  :value="value" @input="onInput">
  </div>
</template>

<script>
    export default {
    props: {
        value: {
        type: String.default: ' '}},methods: {
        onInput(e) {
            // Send an input event to the outfield
            this.$emit('input', e.target.value)
        }
    },
  }
</script>

// Used by the parent component
<Input v-model='username'></Input>
{{username}}


Copy the code

Ok to see the effect a custom component v-Model has been implemented

FormItem: Used to verify and display labels and error messages

1, implement label to display the name of the input box and error message, this is very simple, directly use props to pass the L attribute

/ / formitem code
<template>
    <div>
        <label v-if="label">{{label}}</label>
        <slot></slot>
        <p v-if="error">{{error}}</p>/ / add</div>
</template>

<script>
export default {
    data(){
        return {
        	error:' '}},props: {label: {
            type: String.default: ' '}},methods: {}}</script>

// Used by the parent component
<FormItem label="Username">
    <Input v-model='username'></Input>
    {{username}}
</FormItem> 
Copy the code

3. Form component: maintain data, global check, submit data after passing

The Form component needs model management data and Rules to manage validation rules

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

<script>
export default {
    data(){
        return{}},props: {model: {/ / must
            type: Object.required: true,},rules: {
            type: Object}},methods: {}}</script>

// Used by the parent component
<Form :model="formData" :rules='rules'>   
   <FormItem label="Username">
        <Input v-model='formData.username'></Input>
        {{formData.username}}
    </FormItem>
    <FormItem label="Password">
        <Input v-model='formData.password'></Input>
        {{formData.username}}
    </FormItem></Form > data(){ return{ formData:{ username:'' }, rules: { username: [{ required: true, message: "Please enter user name"}], password: [{required: true, message: "Please enter password"}]}}},Copy the code

At this point, a basic form structure is set up

3.1, dojo.provide/inject

The Form component has the values of Model and Rules, and now needs to manage its data. How do you pass the data from the Model to the child components? Through structural analysis, here are the relationships between the parent and descendant components. Provide/Inject is used for data transfer. In this case, we just pass the “this” of the entire form

// The form component adds the provide code
<template>
    <div>
        <slot></slot>
    </div>
</template>

<script>
export default {
    provide(){
        return {
            form:this}},... }</script>
// The child formItem receives the form
export default {
    inject: ["form"].data(){
        return {
            error:' '}},... }Copy the code

, $3.2 attr

For sub-components, there are usually other properties that need to be passed and that do not need to be written in props. You can use $attrs, such as the placeholder property of the input box, which will be passed directly to the input box

/ / <! -- v-bind="$attrs"
/ / input component
<input :type="type" :value="value" @input="onInput" v-bind="$attrs">
export default {
	inheritAttrs: false.// inheritAttrs is set to false to avoid being set to the root element
}

  
Copy the code

The effect is as follows:

3. Data verification

First, the input component gets the value and notifies the formItem validation. We cannot emit events to formItem directly at this point because formItem has only one slot for display and slot has not yet become an input component. So we can use its parent component to emit validation events, and formItem itself to emit events to FormItem, because formItem only has a slot for display, slot has not yet become an input component, and there is no place to listen. So we can use its parent component to emit validation events, and formItem itself to emit events to FormItem, because formItem only has a slot for display, slot has not yet become an input component, and there is no place to listen. Therefore, we can use its parent component to emit the validation event, and formItem itself can listen for changes in the event. Formitem can get changes in the input

// Add $parent to the Input component

methods: {
    onInput(e) {
        // Send an input event to the outfield
        this.$emit('input', e.target.value)

        // Notify the parent to perform validate when the entered value changes
        this.$parent.$emit('validate')}},/ / formitem components
// FormItem mountd listens on validate
 mounted() {
    this.$on("validate".() = > {
        console.log('Input component is changing')}); },Copy the code

Now look at the effect: you can listen for changes to the input component

Ok, so how do you know which input changes and make a validation rule for them? In this case, you need prop. Formitem adds prop and passes prop to it when it’s used

Continue to modify components


/ / when used
 <FormItem label="Username" prop='username'>
    <Input v-model='formData.username' placeholder="Please enter user name"></Input>
    {{formData.username}}
</FormItem>

// FormItem adds prop and validateFun ()
    props:{
        label: {
            type: String.default: ' '
        },
        prop: {
            type: String.default: ' '}},//methods
methods: {validateFun(){
        / / rules
        const rules = this.form.rules[this.prop];
        / / the current value
        const value = this.form.model[this.prop];
        // Verify the description
        const desc = { [this.prop]: rules };

        console.log(rules,value,desc)

    },
},
  
//mounted
mounted() {
    this.$on("validate".() = > {
        console.log('Input component is changing')
        this.validateFun()
    });
},
      
Copy the code

Look at the results:

Now that we can get the input value, we can start the validation process. The main library for validation is async-Validator

npm install async-validator
Copy the code

Using async-validator:github.com/yiminghe/as…

validateFun() {
  / / rules
  const rules = this.form.rules[this.prop];
  / / the current value
  const value = this.form.model[this.prop];

  // Validates the description object
  const desc = { [this.prop]: rules };
  // Create Schema instance
  const schema = new Schema(desc);
  // Returns the checked value
  return schema.validate({ [this.prop]: value }, errors= > {
    if (errors) {
      If errors is returned, the custom error text is displayed
      this.error = errors[0].message;
    } else {
      // The check is successful
      this.error = ""; }}); }Copy the code

Now let’s see what happens

Now that each formItem can be validated, you need to inform the Form component of the result for a Submit operation after the validation is successful

Analysis: The form needs to get all the formItem validation results.

// The form component validate method
validate(callback){
      // First filter out items without prop that do not need validation. Then get validation results for all components. Call validateFun directly for sub-components

      const validatResult = this.$children.filter(item= > item.prop).map(item= > item.validateFun())
      console.log(validatResult)
      // Since async-Validator is asynchronous, we need to use promise processing here
      Promise.all(validatResult)
      .then(() = > callback(true))
      .catch(() = > callback(false));
  },
  
// Submit when used
submit(){
      this.$refs["form"].validate(valid= > {
          if (valid) {
              alert("success");
          } else {
              alert("fail");
              return false; }})},Copy the code

So let’s see what happens

4. Robust thinking

At this point a basic form is complete, but here’s a thought. The input component can only send events to the parent component. What if the parent component of the input component is not a FormItem, but some other div element tag? ElemetUI and viewUI find that in this case, emitters. Js are used to distribute events github.com/ElemeFE/ele… So let’s introduce a file like that.


//broadcast: a top-down event,
function broadcast(componentName, eventName, params) {
  // Tell all child elements to iterate over componentName
    this.$children.forEach(child= > {
        var name = child.$options.componentName;
    
        if (name === componentName) {
          	// If the name of the child element is the same as the name passed in (so we need to add componentName to each child element)
            child.$emit.apply(child, [eventName].concat(params));
        } else{ broadcast.apply(child, [componentName, eventName].concat([params])); }}); }export default {
        methods: {
          // Bubble to find components with the same componentName and dispatch
          dispatch(componentName, eventName, params) {
              var parent = this.$parent || this.$root;
              var name = parent.$options.componentName;

              while(parent && (! name || name ! == componentName)) { parent = parent.$parent;if(parent) { name = parent.$options.componentName; }}if(parent) { parent.$emit.apply(parent, [eventName].concat(params)); }},broadcast(componentName, eventName, params) {
              broadcast.call(this, componentName, eventName, params); }}};Copy the code

As shown in the source code above, the Emitter component needs to pass in a componentName, so add componentName to each component to complete the function of Emitter

//formitem
<template>
    <div>
        <label v-if="label">{{label}}</label>
        <slot></slot>
        <p v-if="error" style="color:red">{{error}}</p>

    </div>
</template>

<script>
import Schema from "async-validator";
export default {
    inject: ["form"].componentName:'FormItem'.// Because Emitter needs this field to traverse
    data(){
        return {
            error:' '}},props: {label: {
            type: String.default: ' '
        },
        prop: {
            type: String.default: ' '}},methods: {validateFun(){
            / / rules
            const rules = this.form.rules[this.prop];
            / / the current value
            const value = this.form.model[this.prop];
            // Verify the description
            const desc = { [this.prop]: rules };

            console.log(rules,value,desc)
            // Create Schema instance
            const schema = new Schema(desc);
            // Returns the checked value
            return schema.validate({ [this.prop]: value }, errors= > {
                if (errors) {
                If errors is returned, the custom error text is displayed
                this.error = errors[0].message;
                } else {
                // The check is successful
                this.error = ""; }}); }},mounted() {
        this.$on("validate".() = > {
            console.log('Input component is changing')
            this.validateFun() }); }},</script>

Copy the code

5, summary

This article through the step by step analysis of the form needs to achieve the function, to complete a basic form form structure.