First, the previous words

Form submission is a common scenario on both PC and mobile, so is friendly and interactive form validation.

Recently, WHEN I was developing mobile terminal pages, I also met the requirements of forms. Unfortunately, the UI library in the technology selection does not conform to the business scenario, so ~~~~

I embarked on a do-it-yourself and hopefully well-fed path…..

But when I thought it through, it turned out to be a lot easier than I thought! Now let me write down my thoughts.

Second, the train of thought

Interaction design

Since our product manager didn’t say anything about form interaction (which is the hardest part), I became a god and could do whatever I wanted ^ _ ^

Therefore, I designed the following requirements based on my experience and business scenarios:

  1. Validates the entire form
  2. Part of a validable form
  3. Reset the form and clear the validation result
  4. Validation is triggered when the value of a form field changes or at the end of input.

Implement the design

According to the business scenario, in the process of implementing Form validation, I have encapsulated three components: Form, FormItem, and Input. Its structure should look like this:

Here’s how I assigned them:

1. Form component —Form

The Form component doesn’t focus on the UI. It focuses on logical implementation, such as verifying (partial) forms, resetting forms, and so on.

The reason for putting these functions in the Form component is to wrap the Form as a whole, just like the UI Form.

If you put these functions in the FormItem component, you break up the form. Furthermore, if we were to validate the entire form or reset the form, we would need to operate on FormItem after FormItem, which would make our code hard to read.

2. Form item component —FormItem

The form item component focuses on the UI and presents the form item label and validation information

3. The input box

In fact, the “input field” refers to the parts that can interact with the user, such as SELECT, input, radio, textArea, etc. Depending on the business scenario, I encapsulate the input.

Three, implementation,

Set the scene ~~~

The above scenario contains two validation requirements:

  1. Click Submit to validate the entire form
  2. Click to obtain the SMS verification code and verify whether the correct mobile phone number is entered. This is a service scenario for verifying a single field.

Step by step, form validation is now implemented.

Now my directory structure looks like this:

In the beginning, I wrote:

<template>
  <div>
    <li-form ref="form"> <li-form-item> <li-input></li-input> </li-form-item> <! </li-form> </div> </template>export default {
  name: 'app'.data() {
    return {
      form: {
        name: ' ', // Name phone:' ', // Mobile phone number email:' '// address:' '// Validation code}}}, Components: {[form. name]: Form, [formitem. name]: FormItem, [input. name]: Input}} </script>Copy the code

We use forms to store user input, but the current form is just a basic structure with no validation capability.

So, what is the first step to implement form validation??

I don’t know what the first step you thought of, the first step I thought of was implementing the validation tool, right

step 1: Implements the verification tool

The verification tool is used to implement the verification function

To implement the validation tool, I need to think about two questions:

  1. Verify the input and output of the tool
  2. Defining validation rules

1. Verify the input and output of the tool

A validation tool is usually a function that has input and output.

The validation tool is intended to validate user input, so its input (parameters) should be the user’s input and the validation rule. Since the user input can be validated with multiple limits, the validation rule should be an array.

The output of the verification tool should be the verification result. The verification result can be in the following ways:

  1. Promise.reject() / Promise.resolve():PromiseMethod to indicate whether the checksum passed.
  2. Booleanvalue

I chose the second option. The disadvantage of the first approach is that when multiple forms are combined into one large form, failure to verify the first form (promise.reject ()) blocks validation of subsequent forms, which is not suitable for business scenarios.

2. Define verification rules

For user input, we should specify from what Angle the user input is appropriate, such as whether it is mandatory, limit length, minimum length, maximum length, type (number or string), etc. Sometimes we also need to perform dynamic verification based on specific requirements. So, I define validation rules from the following perspectives:

  1. If required
  2. The length of the input
  3. Minimum (maximum) input length
  4. type
  5. Regular check rule
  6. Custom check

Suppose we now restrict the user’s name to: mandatory, with a length of 1-10; If the user does not enter, it prompts “Please enter your name”; If the user enters more than 10 characters, “Please enter 1-10 characters” is prompted.

/ / form:
const form = {
  name: ' './ / name
  phone: ' './ / cell phone number
  email: ' '  / / email
}
// Form rules:
const rules = {
  name: [{required: true.message: 'Please enter your name' },
    { min: 1.max: 10.message: 'Please enter 1-10 characters'}}]Copy the code

The following method takes the user’s input and returns the verification information. Note: this is not the verification result.

// validator.js
/* rules = [{required: true, message: 'Please enter your name '}, {min: 1, Max: 10, message:' Please enter 1-10 characters '}] */
export const validator = (value, rules) = > {
  const result = [] // Save the verification information
  rules.forEach(rule= > {
    let msg = ' '
    const {
      len = 0.// Field length
      max = 0.// Maximum length
      min = 0.// Minimum length
      message = ' '.// Verify copy
      pattern = ' '.// Regular expression verification
      type = 'string'./ / type
      required = false.// Mandatory
      validator         // Custom functions
    } = rule
    if(required && ! value) { msg = message ||'Please enter'
    }
    // typeValidator: validator type
    if (type === 'string' && typeValidator(value) && value) {
      const length = String(value).length || 0
      // lengthValidator: Specifies the length of the verification
      msg = lengthValidator(length, min, max, len, message)
    }
    if (pattern) {
      const isReg = typeValidator(pattern, 'RegExp')
      if(! isReg) { msg ='The regular check rule is incorrect'
      }
      if(! pattern.test(value)) { msg = message ||'Please enter the correct value'}}if (validator && typeof validator === 'function') {
      msg = validator(value, rules)
    }
    if (msg) {
      result.push(msg)
    }
  })
  return result
}
// typeValidator: type verification function ~~~
const baseTypes = ['string'.'number'.'boolean']
const typeValidator = (value, type = 'string') = > {
  if (baseTypes.includes(type)) {
    const valueType = typeof value
    return baseTypes.includes(valueType)
  } else if (type === 'array') {
    return Array.isArray(value)
  } else if (type === 'email') {
    const reg = / ^ [a - zA - Z0-9 _. -] + @ [a zA - Z0-9 -] + (\. [a zA - Z0-9 -] +) * \. [a zA - Z0-9] {2, 6} $/
    return reg.test(value)
  } else if (type === 'RegExp') {
    return value instanceof RegExp}}// lengthValidator: specifies the length verification function
const lengthValidator = (length, min, max, len, message) = > {
  if(len && len ! == length) {return message || ` please enter${len}Characters `
  }
  if (min && length < min) {
    return message || 'At least enter${min}Characters `
  }
  if (max && length > max) {
    return message || 'maximum input${max}Characters `
  }
  if (min && max && (length < min || length > max)) {
    return message || ` please enter${min} ~ ${max}Characters `
  }
  return ' '
}
Copy the code

If the user has no input, the above method will return the array:

validator(form.name, rules.name)
// [' Please enter your name ', 'Please enter 1-10 characters ']
Copy the code

Step 2The postman delivers letters

The Form validator is a Form that validates the Form. The Form validator is a Form that validates the Form. The Form validator is a Form that validates the Form. We can pass it to the Form component as a prop.

So, to start, the Form component looks like this:

<! -- form.vue --> <template> <div> <slot></slot> </div> </template> <script> import { validator } from'./validator'
export default {
  name: 'li-form',
  props: {
    data: {
      type: Object,
      default: () => ({})
    },
    rules: {
      type: the Object, the default: (a) = > ({})}}, the methods: {/ / check the entire formvalidateFields() {/ /... }, } } </script>Copy the code

1. Verify the form

The Form component validateFields method validates the entire Form. In this case, we return the validation result based on the validation information

Implementing validation of the entire form requires two considerations:

  1. Verify each field, obtain the verification information of each field, and then return the verification result according to the verification information.
  2. FormItemDisplay verification Information

We’ve already implemented the first problem by calling the Validator for each field:

// form.vue
validateFields () {
  let hasError = false
  const ruleKeys = Object.keys(this.rules)
  ruleKeys.forEach(ruleKey= > {
    const value = this.data[ruleKey]
    const keyResult = this.validateField(value, ruleKey)
    if(! hasError) { hasError = keyResult.length >0}})return hasError
}
validateField (value, prop) {
  const rules = this.rules[prop]
  let keyResult = validator(value, rules)
  return keyResult
}
Copy the code

The logic of the above code is shown below:

By traversing each field (key), we can connect the form with the verification rule, so that we can get the value of each field and the corresponding verification rule, and finally call the verification tool function

The second problem is that the FormItem displays validation information, just like the mailman delivers a letter, we need to deliver a particular letter to a particular recipient, in this case the Form component is the mailman, so now we need to associate the letter with the recipient, give the validation information to the FormItem, and display it

So how do we relate the letter to the recipient?

We can also bind the key to the FormItem and use the key as the unique identifier of the FormItem:

<template>
  <div>
    <li-form :data="form" :rules="rules" ref="form">
      <li-form-item prop="name">
        <li-input></li-input>
      </li-form-item>
    </li-form>
  </div>
</template>
export default {
  name: 'app'.data() {
    return {
      form: {
        name: ' '// name}, rules: {name: [{required:true, message: 'Please enter your name' },
          { min: 1, max: 10, message: 'Please enter 1-10 characters'}
        ]
      }
  }
}
</script>
Copy the code

As shown, we bind the FormItem to the validation information:

Here, we bind the key to the FormItem using the ref attribute. By adding the ref attribute to a FormItem, we can get the FormItem instance and manipulate its properties and methods.

So, once we have the checksum, we can manipulate the FormItem instance through ref to display the checksum.

Here’s a wrapper around FormItem:

<! -- form-item.vue --> <template> <div :ref="prop">
    <div>
      <slot name="label">
        <span>{{ label }}</span>
      </slot>
    </div>
    <div>
      <slot></slot>
      <span>{{ msg }}</span>
    </div>
  </div>
</template>
<script>
export default {
  name: 'li-form-item',
  props: {
    prop: {
      type: String,
      default: ' '}},data () {
    return{error: [] // Check information :['Please enter your name'.'Please enter 1-10 characters']
    }
  },
  computed: {
    msg () {
      returnThis. Error [0] // display the first}}} </script>Copy the code

So what’s the structure of the FormItem check information, that’s up to you, right

Now let’s go back and rewrite validateFields

// form.vue
validateFields () {
  // ...
  const ruleKeys = Object.keys(this.rules)
  ruleKeys.forEach(ruleKey= > {
    const value = this.data[ruleKey]
    const keyResult = this.validateField(value, ruleKey)
    // ...
  }
  // ...
},
validateField (value, prop) {
  const [vNode] = this.$children.filter(vnode= > prop in vnode.$refs)
  const rules = this.rules[prop]
  let keyResult = []
  if (vNode && rules) {
    keyResult = validator(value, rules)
    vNode.error = keyResult
  }
  return keyResult
}
Copy the code

When clicking submit on the page, we can just write down the following code to achieve the verification of the entire form!

this.$refs.form.validateFields()
Copy the code

At this point we have implemented the validation of the entire form

And if you’re careful, you might notice that validateField is just a way to validate a field.

The validateField method uses the key to get the FormItem instance and the validation information, but it returns the validation information, which is an array. Here, TO be consistent with the validateFields return structure, I also wrote an additional method to validate a field

// form.vue

// Verify form fields
// @params value Specifies the form field value
// @params label Form field name
validateFieldValue (value, lable) {
  let hasError = false
  const keyResult = this.validateField(value, lable)
  hasError = keyResult.length > 0
  return hasError
}
Copy the code

ValidateFieldValue can only validate one field, and its full function should be to validate multiple fields, but I don’t have this business scenario, so I’ll leave it out and fix it later.

So when you click the get SMS verification code button on the page, say:

this.$refs.form.validateFieldValue(this.form.phone, 'phone')
Copy the code

This validates individual fields.

2. Reset the form

The reset form does two things:

  1. Clear the form values
  2. Remove check result

To clear the form value, you need to clear the form passed by props, which communicates with the parent component

Removing the validation result is similar to validating the entire form, except that you do not need to execute a validator

So, reset the form:

// form.vue
// Reset the form
resetFields () {
  let obj = {}
  Object.keys(this.data).forEach(key= >{ obj = { ... obj, [key]:null}})this.validateFields(true)
  this.$emit('change', obj)
},
// Verify the entire form
validateFields (reset = false) {
  // ...
  const ruleKeys = Object.keys(this.rules)
  ruleKeys.forEach(ruleKey= > {
    const value = this.data[ruleKey]
    const keyResult = this.validateField(value, ruleKey, reset)
  }
  // ...
},
validateField (value, prop, reset = false) {
  // ...
  let keyResult = []
  if (vNode && rules) {
    if(! reset) { keyResult = validator(value, rules) } vNode.error = keyResult }return keyResult
},
Copy the code

Step 3: the letter

The form verification function is almost complete, but there is still one verification function that is not implemented, which is to start the verification when the input field value changes or loses focus, so let’s rewrite the verification rule first:

rules: {
  name: [{required: true.message: 'Please enter your name'.trigger: 'blur' },
    { min: 1.max: 10.message: 'Please enter 1-10 characters'.trigger: 'change'}}]Copy the code

Trigger indicates whether the checksum is triggered when the value changes or is out of focus.

How does that happen

Here are two ideas:

  1. inInputWrite another validation method inside the component
  2. The triggerFormComponent methods

The core of the first approach is key:

  1. According to thekeyFirstly, the verification rule of the field is obtained
  2. inInputComponent callsvalidatorMethod to obtain verification information
  3. Manipulate the parent component instance or use events to makeFormItemDisplay verification Information

The idea is as follows

But there are problems with this approach:

  1. Validation may be performed several times, such as after the last input field is typed and clicking Submit, which will startFormThe global checksum ofInputOut of focus check.
  2. InputAs an inputUIIt is not necessary to have logical functions, which makesInputAnd parent components,FormItemComponent coupling is too high
  3. Finally, and most importantly, as a deep sloth, I don’t want to write the same code with the same functionality in two places twice.

If you use events, you have to listen for the Input blur or change event in the parent component, which is too flexible.

So in the end, I used the second method, which triggered the Form component

To implement the second approach, we need to solve two problems:

  1. inInputCan I getFormComponent instance
  2. Because it’s calledvalidateFieldValue (value, lable)Method, so we also need to know the fieldskey

It’s like going to the post office to mail a letter after you’ve written it.

So now the question is how do we know where the post office is?

After I read Vue API to double 叒 yi, I suddenly had an Epiphany and found Provide/inject can solve my problem

This pair of options needs to be used together to allow an ancestor component to inject a dependency into all of its descendants, regardless of how deep the component hierarchy is, and remain in effect as long as the upstream and downstream relationship is established.

Provide provides the ability to inject dependencies into future generations by injecting the Form instance into the Input and by using Provide ^ _ ^

For now, we’ll inject instances into posterity using Provide in the Form component.

// form.vue
export default {
  provide () {
    return {
      liForm: this
    }
  }
}
Copy the code

Then inject the key into the descendant using Provide in the FormItem component:

// form-item.vue
export default {
  props: {
    prop: {
      type: String,
      default: ' '}}provide () {
    return {
      formItem: this.prop
    }
  }
}
Copy the code

Finally we use inject in the Input to receive dependencies:

<! -- input.vue --> <template> <div> <input :value="value"
      @blur="$blur"
      @change="$change"
      @input="$input"
    >
  </div>
</template>
<script>
export default {
  name: 'li-input',
  model: {
    prop: 'value',
    event: 'change'
  },
  inject: {
    liForm: {
      default: ' '
    },
    formItem: {
      default: ' '
    }
  },
  props: {
    value: [String, Number]
  },
  methods: {
    $blur (e) {
      this.$emit('blur', e)
      const value = e.target.value
      this.triggerValidate(value, 'blur')},$change (e) {
      const value = e.target.value
      this.$emit('change', value)
    },
    $input (e) {
      const value = e.target.value
      this.$emit('input', value)
      this.$emit('change', value)
      this.triggerValidate(value, 'change')
    },
    triggerValidate (value, triggerTime) {
      if(! (this.liForm && this.formItem))return
      const trigger = this.liForm.getTriggerTimer(this.formItem, triggerTime)
      if (trigger === triggerTime) {
        this.liForm.validateField(value, this.formItem)
      }
    }
  }
}
</script>
Copy the code

Note: Inject a default value for liForm and formItem, otherwise an error will be reported

Liform. getTriggerTimer is used to get the validation time defined in the field validation rule:

// form.vue
getTriggerTimer (lable, triggerTime) {
  const rules = this.rules[lable]
  const ruleItem = rules.find(item= > item.trigger === triggerTime)
  const { trigger = ' ' } = ruleItem || {}
  return trigger
}
Copy the code

At this point, form validation is almost complete

The last

The beginning is going to attach the source code, and write a Demo, but there is no time ah, will be added later. Besides, this code is rough and needs to be improved. However, I write this article to record my thinking, and then I hope that you who read this article can arouse your thinking, so that we can have a collision of ideas.

Here’s a summary:

  1. FormComponent to achieve verification function,FormItemDisplay verification information,InputFor user input.
  2. Through each fieldkeyValues will beForm,FormItemandInputComponents are linked together. throughkeyValue,FormoperationFormItemInstance,FormItemDisplay verification information.
  3. throughProvide/injectandkeyValue,InputoperationFormComponent for out-of-focus or value change check.