preface

The recent development of mobile projects directly on vuE3, the new feature composition API really brings a new development experience. By using these features, developers can manage highly coupled states and methods together and encapsulate highly reusable logic separately, as appropriate, which can help improve the robustness of the overall code architecture.

Nowadays, every newly launched mobile terminal project basically contains registration and login module. In this practice, some experience is summarized for the form control in the login and registration, and the common code is extracted to improve the maintainability and development efficiency of the code.

Now take a look at the pictures provided by the art students.

The sign-up page

The login page

Forget password page

Change password Page

By looking at the above product images, it is clear that the core component of the entire login and registration module is the input input box. As long as the input box component development is complete, other pages directly reference to the line.

Input box completed the development of only the static page display, we also want to design a set of general data verification scheme applied to each page form controls.

Input box component

From the above analysis, the input box component is the core content of the entire login registration module. Let’s first take a look at what UI shapes the input box component has.

  • Form a

On the left side is the text +86, and in the middle is the input box. On the right side, if data is detected in the input box, the cross icon is displayed, and if no data is detected, the empty hide icon is displayed.

  • Form the second

There is only one input field on the left and copy on the right. The content of the copy may be the verification code, or it may be the countdown copy displayed after clicking the verification code.

  • Form three

There is still only one input box on the left. On the right side, if the input box is detected to have content, the cross icon is displayed, and if the content is empty, the icon is hidden.

layout

Based on the above observation, we can design the input component into three parts: left, center and right. The left side may be copywriting, or it may be empty. In the middle is an input box. It could be copywriting or it could be a cross icon.

The template content is as follows:

<template> <div class="input"> <! - the left side, lt is on the left side of the content - > < span class = "left - the text" > {{lt}} < / span > <! - - - > < input class = "content" v - bind = "$attrs" : value = "value" @ input = "onChange" / > <! <div v-if="rt == 'timer'" class="right-section"> {{timerdata.content}} <! - may be a 'captcha', may also be a countdown -- > < / div > < div v - else - if = "rt = = 'close'" class = "right - section" > < van - icon name = "close" / > <! </div> </div> </template>Copy the code

The layout sets the left, center, and right parent to display:flex, and all three child elements to display:inline-block, so that the left and right can adjust their width according to their content, while the middle input is set to Flex :1 to fill the remaining width.

Such a layout is feasible in theory, but problems have been found in practice.

The renderings of Demo are as follows:

As the right side continues to increase in width, the middle input will spill out to the right because of the default width, which is not what we want.

The solution to this problem is as simple as setting the width of the intermediate input to 0, which is what we want.

v-model

External pages refer to the component structure encapsulated above as follows:

<InputForm lt="+86" <! +86--> rt="close" <! Placeholder =" Please enter your mobile number "/>Copy the code

The external page creates a form data form_data as follows, but wants to be able to create two-way data binding between the form_data data and the value of the subcomponent input box in the form of v-Model.

Const form_data = reactive({number_number: ", password: ", ppassword: ", // duplicate password: ", captcha: ", // verify code})Copy the code

It is very easy to implement v-Model in VUE3, using V-Model :xx in the parent component to complete the binding, where xx corresponds to the state name of the child component to be bound, as shown below.

<InputForm lt="+86" <! +86--> rt="close" <! Placeholder ="form_data.password" /> placeholder=" form_data.password"Copy the code

Next, the child component first declares the value property to be bound and listens for the onInput event in the input box. The code is as follows:

<template>
    <div class="input">
        ...
            <input class="content" v-bind="$attrs" :value="value" @input="onChange" />  
        ...
    </div>  
</template>
export default defineComponent({
  props: {
    lt:String,
    rt: String,
    value: String
  },
  setup(props, context) {
    const onChange = (e:KeyboardEvent) => {
      const value = (e.target as HTMLInputElement).value;
       context.emit("update:value",value);
    };
    return {
       onChange
    }
  }
 })
Copy the code

The onInput callback uses context.emit(“update:value”,value) to return the obtained value.

In the preceding part of update:value, update: is fixed, followed by the name of the state to establish bidirectional binding. In this way, v-Model binding is easily completed.

Data validation

In general, whenever a form control (such as an input field) is involved on the page, data validation is performed against the corresponding value. If we follow the original method, when the user clicks the button,js accepts the response and obtains the value of each form item in turn for one-by-one verification.

This approach is certainly functional, but not efficient or streamlined. Because many pages have to be checked, a lot of check logic is written repeatedly.

Next, we designed a universal verification scheme to encapsulate the reusable logic code and quickly apply it to each page to improve development efficiency.

Using the registration page as an example, the template code is as follows. Create four input box components: mobile number, mobile verification code, password and confirm password. Put a registration button at the end.(For clarity, the code below removes all TS types.)

<Form ref="form" :rules="rules"> <InputForm lt="+86" rt="close" v-model:value="form_data.number_number" Placeholder =" propName="number_number" /> <InputForm rt="timer" v-model:value="form_data.captcha" Placeholder ="form_data.password"; placeholder :value="form_data.password"; Placeholder ="form_data.ppassword" type="password" propName="password" /> < int form rt="close" Placeholder =" id "type=" id" propName="ppassword" /> <Button text=" @sub="onSubmmit" /> <! -- register button --> </Form>Copy the code

After borrowing Form practices from other good frameworks, we first added a component Form to the outermost layer, and then added an attribute, propName, to each input box component. This property is used in conjunction with Rules, which are manually defined validation rules that, when passed to the Form component, the child component (the input box component) can get its own validation rules via the propName property.

The whole idea can be connected from the beginning. First, the front-end developer defines the validation rules for the current page and passes them to the Form component. The Form component receives the validation rules and distributes them to each of its child components (input box components). The subcomponent can get the validation rules for the value of the input box to do the corresponding data verification.

When the user clicks the register button, the click event gets an instance of the Form component and runs its validate method, at which point the Form component does a round of data validation for each of its subcomponents. Once all validations are successful, the validate method returns true. If there is a failed validation, the validate method returns false and displays an error message.

The registration page logic is as follows:

export default defineComponent({ components: {InputForm, // Button, // registration Button Form, //Form component}, setup(props) {const form_data =... ; // Omit const rules =... ; Const Form = ref(null); // Const Form = ref(null); const onSubmmit = ()=>{ if (! form.value || ! form.value.validate()) { return false; } return {form, rules, onSubmmit, form_data}; }});Copy the code

Define a variable form that is used to get an instance of the form.

The user clicks the register button to trigger the onSubmmit function, because the form is a variable created using ref, and to get the value you call.value. Running the form.value.validate() function causes each of the subcomponents under the form to start performing validation logic, returning true if all passes, and false if one fails.

From the above analysis, we can see that the Form control only exposes a function, called by the validate function, to know whether the validation is passed. So how does Validate know what rules to use? So we need to design a set of validation rules, pass it to the Form component, and its internal validate function can use the rules to perform the validation.

Design rules

Rules is an object. For example, the registration page above defines rules as follows:

Const rules = {number_number:[{type: 'required', MSG: 'required'} "phone"], captcha:[{type: 'required', MSG: 'required'], captcha:[{type: 'required', MSG: 'required'], captcha:[{type: 'required', MSG: 'required'], captcha:[{type: 'required', MSG: 'required'] }, password: [{type: 'required', MSG: 'Please enter password ',}, {type: 'minLength', params: 6, MSG: 'Password must contain at least 6 characters ',},], ppassword:[{type: 'custome', callback() { if (form_data.password !== form_data.ppassword) { return { flag: false, msg: 'do not match the password input twice,};} return {flag: true,};}},]}Copy the code

The rules we define is an object in the form of a key-value pair. The key corresponds to the propName of each input box component in the template, and the value is an array of rules that the input box component follows.

Now take a closer look at the composition of the values under each object. The reason the values are organized as arrays is to add multiple rules to the input field. Rules correspond to two forms, one is an object and the other is a string.

Strings are easy to understand, like the number_number attribute above, which corresponds to the string phone. The meaning of this rule is that the value of the input field should follow the rules of the mobile phone number. Of course, if the string is filled in email, it should be used as a mailbox to verify.

If a rule is an object, it contains the following properties:

{type:'minLength',params:6}, callback () {type:'minLength',params:6};Copy the code

Type is the verification type. Required indicates that it is required. If the user does not fill it in, clicking the register button to submit will prompt an error message defined by MSG.

In addition, you can use minLength or maxLength to limit the length of the value.

Finally, type can be filled with CUSTome, leaving the developer to define the validation logic function callback for the input box. This function requires that an object with a flag attribute be returned. The flag attribute is a Boolean value that tells the verification system whether the verification succeeded or failed.

Form Form

Once rules are defined and passed to the Form component, the Form component needs to distribute the validation logic to its child components. Make each of its child components responsible for generating its own validation function.

<! <div class="form"> <slot></slot> </div> </template> <script lang="ts"> import {ref, provide } from "vue"; export default defineComponent({ name: "Form", props:{ rules:Object }, setup(props) { ... / / omit dojo.provide (" rules ", props. Rules); Const validate = ()=>{return {validate}}}) </script>Copy the code

As you can see from the above structure, the Form component template provides a slot in the logic code to pass validation rules to descendants using provide, exposing a validate function to the outside.

The child component generates the check function

This time back to the InputForm, the core component of the login registration module, we are now going to add validation logic to the input box component.

Import {inject, onMounted} from "vue"; . setup(props, context) { const rules = inject("rules"); const rule = rules[props.propName]; Const useValidate = () => {const validateFn = getValidate(rule); Const execValidate = () => {return validateFn(props. Value); // Execute the check function and return the check result}; onMounted(() => { const Listener = inject('collectValidate'); if (Listener) { Listener(execValidate); }}); }; useValidate(); // Initialize the validation logic... }Copy the code

Rules structure is similar to the following. Use inject and propName to get the rule rule that the Form distributes to the input box to execute.

{captcha:[{type: 'required', MSG: 'Verification code cannot be empty'}], password:[{type: 'required', MSG: 'Please enter password ',}]}Copy the code

We pass the rule to the getValidate function (which we’ll talk about later) to get validateFn. The validateFn function passes in the value of the input box and returns the validation result. Here we wrap validateFn in a layer and give execValidate to external use.

In the above code we also see the logic for the onMounted package. After the component is mounted, use inject to get a function Listener passed from the Form component, and pass the validation function execValidate as the parameter to execute.

Let’s go back to the Form component in the code below and see what a Listener is.

setup(props) { const list = ref([]); // Define an array const listener = (fn) => {list.value.push(fn); }; provide("collectValidate", listener); Const validate = (propName) => {const array = list.value.map((fn) => {return fn(); }); const one = array.find((item) => { return item.flag === false; }); If (one && one.msg) {Alert(one.msg); // The return false error message is displayed. } else { return true; }}; .Copy the code

As you can see above, the Form component distributes the listener function. The child component gets the listener function from the onMounted lifecycle hook, and passes the function execValidate defined in the child component as a parameter.

This ensures that each child component passes its own validation function to the List collection in the Form component once it is mounted. The Form component’s validate method simply iterates through the list, executing the validation function for each of the child components in turn. If both tests pass, the external page returns true. If one fails, an error message is displayed, indicating false.

By the time we get here, the whole validation process has gone through. The Form first distributes the validation rules to the child component, and the child component obtains the validation rules to generate its own validation functions, and returns the validation functions to the Form after it is mounted. At this point, the Form component exposes the validate function for all Form controls.

The next final step is to look at how a subcomponent can generate its own validation functions using rules.

check

First write a Validate class that manages the validation logic. The code looks like this. We can continue to extend the methods to meet new requirements, such as adding email or maxLength methods.

Class Validate {constructor() {} required(data) {// const MSG = 'this information is required '; / / the default error message if (data = = null | | (typeof data = = = 'string' && data. The trim () = = = ")) {return {flag: false. MSG}} return {flag:true}} // Call (data) {const MSG = 'please enter the correct phone number '; // Default error message const flag = /^1[3456789]\d{9}$/.test(data); Return {MSG, flag}} // minLength(data, {params}) {let minLength = params; If (data == null) {return {flag:false, If (data.trim().length >= minLength) {return {flag:true}; } else {return {flag:false, MSG: 'The minimum length of data cannot be less than ${minLength} bit'}}}}Copy the code

Of all the methods defined by the Validate class, the first parameter, data, is the value to be checked, and the second parameter is the rule in each rule defined on the page. For example, {type: ‘minLength’, params: 6, MSG: ‘Password length cannot be less than 6 ‘}.

{flag:true, MSG :””} for each method in the Validate class. In the result, flag indicates whether the verification is successful, and MSG indicates the error message.

The Validate class provides a variety of validation methods. Next, use a singleton pattern to generate an instance of this class and apply the instance object to a real validation scenario.

 const getInstance = (function(){
    let _instance;
    return function(){
         if(_instance == null){
           _instance = new Validate();
         }
         return _instance;
      }
 })()
Copy the code

Call getInstance to get the singleton’s Validate instance object.

The input box component returns the validation function required by the component by passing a rule to the getValidate function. Let’s see how the getValidate function generates the validation function from the rule. The code looks like this:

*/ export const getValidate = (rule) => {const ob = getInstance(); Const fn_list = []; Rule. ForEach ((item) => {if (typeof item === 'string') {if (typeof item === 'string') { Fn_list. push({fn: ob[item],}); } else if (isRuleType(item)) {// object type fn_list.push({// if item.type is custome custom type, callback is used directly. Otherwise get from ob instance... item, fn: item.type === 'custome' ? item.callback : ob[item.type], }); }}); Const execuate = (value) => {let flag = true, MSG = "; for (let i = 0; i < fn_list.length; i++) { const item = fn_list[i]; const result = item.fn.apply(ob, [value, item]); //item.fn corresponds to the validation method defined by the Validate class if (! Result.flag) {// The verification failed flag = false; msg = item.msg ? item.msg : result.msg; // Whether to use the default error message or the user-defined message break; } } return { flag, msg, }; }; return execuate; };Copy the code

The data structure of rule is similar to the following code. When the rule is passed to the getValidate function, it determines whether the rule is an object or a string, and then stores the validation function corresponding to its type from the OB instance into the fn_list.

[{type: 'required', MSG: "Please enter the phone number"}, "phone"]Copy the code

The getValidate function eventually returns the Execuate function, which is also the validation function for the input box component. The input box value is available in the input box component if passed to the Execuate method call. The method internally iterates through the previously cached list of validators fn_list, passing values to each validator and running them to get the validation result of the input box component on the current value and return it.

The above verification logic has also been passed. Next whether the development of the login page, forget the password or change the password of the page, only need to use the Form component and input box InputForm component organization page structure, and write a page rules verification rules can be. All the remaining verification details and interactive actions are all handed over to Form and InputForm internal processing, which will greatly improve the development efficiency.

The final result