Preface:

In everyday WEB project development using VUE, there is often a need to submit forms. We can use a component library like iView or Element to do this; But we often ignore the implementation logic, if you want to understand the implementation details, this article from 0 to 1, teach you to encapsulate your own Form component! Example code github.com/zhengjunxia…

Form component overview

There are many form-like components, such as Input, Radio, Checkbox, and so on. Data validation is also often used when working with forms. It would be inefficient to write validators to validate the input values of each Form, so you need a component that validates the underlying Form controls, which is the Form component that we’ll do in this section. The Form component is divided into two parts, one is the outer Form field component. A set of Form controls has only one Form, and the inner contains multiple FormItem components, each Form control is wrapped by a FormItem. The basic structure looks like this:

<! -- ./src/views/Form.vue -->
<iForm ref="form" :model="formData" :rules="rules">
   <iFormItem label="Name:" prop="name">
       <iInput v-model="formData.name" ></iInput>
   </iFormItem>
   <iFormItem label="Email:" prop="mail">
       <iInput v-model="formData.mail"></iInput>
   </iFormItem>
   <button @click="handleSubmit">submit</button>
   <button @click="handleReset">reset</button>
</iForm>
Copy the code

Forms require input validation and a validation prompt in the corresponding FormItem. We use an open source library called Async-Validator for validation. The rules are as follows:

[{required: true.message: 'Cannot be empty'.trigger: 'blur' },
  { type: 'email'.message: 'Format is not correct'.trigger: 'blur'}]Copy the code

Required indicates a mandatory item, message indicates a message indicating a verification failure, trigger indicates a verification condition, and its values include blur and change indicating a loss of focus and verification during input. If the first verification rule meets the requirements, perform the second verification. Type indicates the verification type, email indicates that the verification input value is in the email format, and user-defined verification rules are supported. See its documentation for more details on its use.

Initialize the project

Create a project using Vue CLI 3 and download the Async-Validator library.

After initializing the project, create a new form folder under SRC/Components and initialize the two components form.vue and FormItem. vue and an input.vue, and configure the route as you wish. After initializing the project, the items in the SRC directory are as follows:

The. / SRC ├ ─ ─ App. Vue ├ ─ ─ assets │ └ ─ ─ logo. The PNG ├ ─ ─ components │ ├ ─ ─ form │ │ ├ ─ ─ form. The vue │ │ └ ─ ─ formItem. Vue │ └ ─ ─ Input. Vue ├ ─ ─ main. Js ├ ─ ─ mixins │ └ ─ ─ emitter. Js ├ ─ ─ the router, js └ ─ ─ views └ ─ ─ Form. The vueCopy the code

Interface implementation

The interface for the component comes from three parts: props, slots, and Events. The Form and FormItem components are used to verify input data. Events are not used. Form slots are a series of formItems, and FormItem slots are concrete forms such as

.

In the Form component, define two props:

  • model: Data object bound to the form control that can be accessed during verification or reset. The type isObject.
  • rules: Form validation rules, as described aboveasync-validatorThe verification rule used, of typeObject.

In the FormItem component, we also define two props:

  • label: Label text for a single form component, similar to native<label>Element of typeString.
  • prop: corresponds to the form fieldFormcomponentmodelTo access the data bound to the form component during validation or reset, of typeString.

Once defined, the code to call the page looks like this:

<template>
  <div class="home"</h3> <iForm ref="form" :model="formData" :rules="rules">
      <iFormItem label="Name:" prop="name">
        <iInput v-model="formData.name"></iInput>
      </iFormItem>
      <iFormItem label="Email:" prop="mail">
        <iInput v-model="formData.mail"></iInput>
      </iFormItem>
    </iForm>
  </div>
</template>

<script>
// @ is an alias to /src
import iForm from '@/components/form/form.vue'
import iFormItem from '@/components/form/formItem.vue'
import iInput from '@/components/input.vue'

export default {
  name: 'home',
  components: { iForm, iFormItem, iInput },
  data() {
    return {
      formData: { name: ' ', mail: ' ' },
      rules: {
        name: [{ required: true, message: 'Cannot be empty', trigger: 'blur'}],
        mail: [
          { required: true, message: 'Cannot be empty', trigger: 'blur'},
          { type: 'email', message: 'Email format is not correct', trigger: 'blur'}
        ]
      }
    }
  }
}
</script>
Copy the code

Implementation details of the iForm, iFormItem, and iInput components in the code are covered later.

So far, the code for the iForm and iFormItem components looks like this:

<! -- ./src/components/form/form.vue --> <template> <div> <slot></slot> </div> </template> <script>export default {
  name: 'iForm'.data() {
    return { fields: [] }
  },
  props: {
    model: { type: Object },
    rules: { type: Object }
  },
  created() {
    this.$on('form-add', field => {
      if (field) this.fields.push(field);
    });
    this.$on('form-remove', field => {
      if (field.prop) this.fields.splice(this.fields.indexOf(field), 1);
    })
  }
}
</script>
Copy the code
<! -- ./src/components/form/formItem.vue --> <template> <div> <label v-if="label">{{ label }}</label>
    <slot></slot>
  </div>
</template>
<script>
export default {
  name: 'iFormItem',
  props: {
    label: { type: String, default: ' ' },
    prop: { type: String }
  }
}
</script>
Copy the code

The fields array is set in the iForm component to save the form instance in the component, which is convenient to obtain the form instance to judge the verification of each form. We bind two listener events, form-add and form-remove, in the Created life cycle to add and remove form instances.

Now we’re going to implement the bind event, but before we do that we’re going to have to imagine, how are we going to call the bind event method? In vue.js 1.x, there was a this.$dispatch method to bind custom events, but it was deprecated in vue.js 2.x. But we can implement a similar method called this.dispatch with $less to differentiate it from the old API. This method can be written to emitters. Js and referenced by mixins in components for code reuse. Create the mixins folder in SRC and create Emitters. Js in it as follows:

<! -- ./src/mixins/emitter.js -->export default {
  methods: {
    dispatch(componentName, eventName, params) {
      let parent = this.$parent || this.$root;
      let name = parent.$options.name;
      while(parent && (! name || name ! == componentName)) { parent = parent.$parent;if (parent) name = parent.$options.name;
      }
      if(parent) parent.$emit.apply(parent, [eventName].concat(params)); }}}Copy the code

As you can see, the Dispatch method compares the component’s $parent. Name with the componentName parameter. When the target parent component is found, it invokes $emit from the parent component to trigger the eventName binding event.

Formitem. vue then introduces the dispatch method via mixins to trigger the binding events form-add and form-remove as follows:

<! -- ./src/components/form/formItem.vue --> <template> <div> <label v-if="label">{{ label }}</label>
    <slot></slot>
  </div>
</template>
<script>
import Emitter from '@/mixins/emitter.js';
export default {
  name: 'iFormItem',
  mixins: [ Emitter ],
  props: {
    label: { type: String, default: ' ' },
    prop: { type: String }
  },
  mounted() {
    if (this.prop) {
      this.dispatch('iForm'.'form-add', this); }}, // Remove the instance from the Form cache before the component is destroyedbeforeDestroy () {
    this.dispatch('iForm'.'form-remove', this);
  },
}
</script>
Copy the code

Formitem. vue: Formitem. vue: formitem. vue: formItem.vue: formItem.vue: formItem.vue

  • inForm.vuerulesObject throughpropsTo pass toiFormComponent, then we can iniFormComponent throughprovideTo export the component instance and make it available to child componentspropsIn therulesObject;
  • Child componentsformItemCan be achieved byinjectTo inject the instance that needs to be accessed.

The code is as follows:

<! -- ./src/components/form/form.vue --> ...export default {
  name: 'iForm'.data() {
    return { fields: [] }
  },
  props: {
    model: { type: Object },
    rules: { type: Object }
  },
  provide() {
    return { form: this }
  },
  created() {
    this.$on('form-add', field => {
      if (field) this.fields.push(field);
    });
    this.$on('form-remove', field => {
      if (field.prop) this.fields.splice(this.fields.indexOf(field), 1);
    })
  }
}
</script>
Copy the code
<! -- ./src/components/form/formItem.vue --> ... import Emitter from'@/mixins/emitter.js';
export default {
  name: 'iFormItem',
  mixins: [ Emitter ],
  inject: [ 'form' ],
  props: {
    label: { type: String, default: ' ' },
    prop: { type: String }
  },
  mounted() {
    if (this.prop) {
      this.dispatch('iForm'.'form-add', this); }}, // Remove the instance from the Form cache before the component is destroyedbeforeDestroy () {
    this.dispatch('iForm'.'form-remove', this);
  },
}
</script>
Copy the code

Now we can retrieve the rule object in formItem via this.form.rules; After having the rule object, you can set the specific verification method;

  • SetRules: set specific events to be monitored and trigger verification;
  • GetRules: Obtain the verification rules corresponding to the form;
  • GetFilteredRule: filters out rules that meet requirements.
  • Validate: The specific verification process; .

The specific code is as follows:

<! -- ./src/components/form/formItem.vue --> <template> <div> <label :for="labelFor" v-if="label" :class="{'label-required': isRequired}">{{label}}</label>
    <slot></slot>
    <div v-if="isShowMes" class="message">{{message}}</div>
  </div>
</template>
<script>
import AsyncValidator from 'async-validator';
import Emitter from '@/mixins/emitter.js';
export default {
  name: 'iFormItem',
  mixins: [ Emitter ],
  inject: [ 'form' ],
  props: {
    label: { type: String, default: ' ' },
    prop: { type: String }
  },
  data() {
    return {
      isRequired: false, isShowMes: false, message: ' ', labelFor: 'input' + new Date().valueOf()
    }
  },
  mounted() {
    if (this.prop) {
      this.dispatch('iForm'.'form-add', this); // set the initialValue this.initialValue = this.fieldValue; this.setRules(); }}, // Remove the instance from the Form cache before the component is destroyedbeforeDestroy () {
    this.dispatch('iForm'.'form-remove', this);
  },
  computed: {
    fieldValue() {
      return this.form.model[this.prop]
    }
  },
  methods: {
    setRules() {
      let rules = this.getRules();
      if (rules.length) {
        rules.forEach(rule => {
          if(rule.required ! == undefined) this.isRequired = rule.required }); } this.$on('form-blur', this.onFieldBlur);
      this.$on('form-change', this.onFieldChange);
    },
    getRules() {
      let formRules = this.form.rules;
      formRules = formRules ? formRules[this.prop] : [];
      returnformRules; }, // filter out rules that meet the requirements getFilteredRule (trigger) {const rules = this.getrules ();returnrules.filter(rule => ! rule.trigger || rule.trigger.indexOf(trigger) ! = = 1); }, /** * validates form data * @param trigger validates type * @param callback */ validate(trigger, cb) {let rules = this.getFilteredRule(trigger);
      if(! rules || rules.length === 0)return true; // Use async-validator const validator = new AsyncValidator({[this.prop]: rules});let model = {[this.prop]: this.fieldValue}; 
      validator.validate(model, { firstFields: true }, errors => {
        this.isShowMes = errors ? true : false;
        this.message = errors ? errors[0].message : ' ';
        if(cb) cb(this.message); })},resetField () {
      this.message = ' ';
      this.form.model[this.prop] = this.initialValue;
    },
    onFieldBlur() {
      this.validate('blur');
    },
    onFieldChange() {
      this.validate('change');
    }
  }
}
</script>
<style>
  .label-required:before {
    content: The '*';
    color: red;
  }
  .message {
    font-size: 12px;
    color: red;
  }
</style>
Copy the code

Note: in addition to the increase of the specific verification method, there are error message display logic

With the formItem component we can use it to wrap the Input component:

  • ininputComponent through@input@blurThese two events are triggeredformItemThe component’sform-changeform-blurThe listening method of.Special attention is requiredIn:handleInputIs required to callthis.$emit('input', value),inputEntered in thevaluePassed to the instance call pageformData, the code is as follows:
<! <template> <div class=./ SRC /views/ form. vue --"home"</h3> <iForm ref="form" :model="formData" :rules="rules">
      <iFormItem label="Name:" prop="name">
        <iInput v-model="formData.name"></iInput>
      </iFormItem>
      <iFormItem label="Email:" prop="mail">
        <iInput v-model="formData.mail"></iInput>
      </iFormItem>
    </iForm>
  </div>
</template>

<script>
// @ is an alias to /src
import iForm from '@/components/form/form.vue'
import iFormItem from '@/components/form/formItem.vue'
import iInput from '@/components/input.vue'// The data in formData is bound by the test of the V-model. // Call this in the input component.$emit('input', value) passes the data to formDataexport default {
  name: 'home',
  components: { iForm, iFormItem, iInput },
  data() {
    return {
      formData: { name: ' ', mail: ' ' }
    }
  }
}
</script>
Copy the code
  • And in the componentwatchThe input of thevalueValue, assigned toinputComponents;

The implementation code is as follows:

<! -- ./src/components/input.vue --> <template> <div> <input ref="input" :type="type" :value="currentValue" @input="handleInput" @blur="handleBlur" />
  </div>
</template>
<script>
import Emitter from '@/mixins/emitter.js';
export default {
  name: 'iInput',
  mixins: [ Emitter ],
  props: {
    type: { type: String, default: 'text'},
    value: { type: String, default: ' '}
  },
  watch: {
    value(value) {
      this.currentValue = value
    }
  },
  data() {
    return { currentValue: this.value, id: this.label }
  },
  mounted () {
    if (this.$parent.labelFor) this.$refs.input.id = this.$parent.labelFor;
  },
  methods: {
    handleInput(e) {
      const value = e.target.value;
      this.currentValue = value;
      this.$emit('input', value);
      this.dispatch('iFormItem'.'form-change', value);
    },
    handleBlur() {
      this.dispatch('iFormItem'.'form-blur', this.currentValue);
    }
  }
}
</script>
Copy the code

This completes the input component, and we can now proceed to validate all forms and reset the used forms when the form component implements form submission:

<! -- ./src/components/form/form.vue --> <template> <div> <slot></slot> </div> </template> <script>export default {
  name: 'iForm'.data() {
    return { fields: [] }
  },
  props: {
    model: { type: Object },
    rules: { type: Object }
  },
  provide() {
    return { form: this }
  },
  methods: {
    resetFields() {
      this.fields.forEach(field => field.resetField())
    },
    validate(cb) {
      return new Promise(resolve => {
        let valid = true, count = 0;
        this.fields.forEach(field => {
          field.validate(' ', error => {
            if (error) valid = false;
            if (++count === this.fields.length) {
              resolve(valid);
              if (typeof cb === 'function') cb(valid); }})})})}},created() {
    this.$on('form-add', field => {
      if (field) this.fields.push(field);
    });
    this.$on('form-remove', field => {
      if (field.prop) this.fields.splice(this.fields.indexOf(field), 1);
    })
  }
}
</script>
Copy the code
  • Validate: Obtain the verification results of all forms and perform corresponding logical processing;
  • ResetFields: Resets all forms;

Now let’s go back to the original call page./ SRC /views/ form.vue and add two buttons for submitting and resetting the Form:

<! -- ./src/views/Form.vue --> <template> <div class="home"</h3> <iForm ref="form" :model="formData" :rules="rules">
      <iFormItem label="Name:" prop="name">
        <iInput v-model="formData.name"></iInput>
      </iFormItem>
      <iFormItem label="Email:" prop="mail">
        <iInput v-model="formData.mail"></iInput>
      </iFormItem>
      <button @click="handleSubmit"> submit </button> < button@click ="handleReset"Reset > < / button > < / iForm > < / div > < / template > < script > / / @ is the analias to /src
import iForm from '@/components/form/form.vue'
import iFormItem from '@/components/form/formItem.vue'
import iInput from '@/components/input.vue'

export default {
  name: 'home',
  components: { iForm, iFormItem, iInput },
  data() {
    return {
      formData: { name: ' ', mail: ' ' },
      rules: {
        name: [{ required: true, message: 'Cannot be empty', trigger: 'blur'}],
        mail: [
          { required: true, message: 'Cannot be empty', trigger: 'blur'},
          { type: 'email', message: 'Email format is not correct', trigger: 'blur'}
        ]
      }
    }
  },
  methods: {
    handleSubmit() {
      this.$refs.form.validate((valid) => {
        if (valid)  console.log('Submitted successfully');
        else console.log('Verification failed'); })},handleReset() { this.$refs.form.resetFields() }
  }
}
</script>
Copy the code

At this point, the basic functions of the Form component are complete. Although it is just a few simple Form controls, it already implements verification and prompt functions.

Example code: github.com/zhengjunxia…

The conclusion can further understand vue.js components by encapsulating them, such as provide/Inject and dispatch communication scenarios. It is of great help to the future development.