This article introduces the encapsulation of form components from 0 to 1, using TypeScript + Vue3.0 technology. If you are not familiar with these two blog posts, check them outJuejin. Cn/post / 690313… 和 Juejin. Cn/post / 690818…

The result:

bootstrap

Let’s go straight to the elegant styles of Bootstrap. First install bootstrap in the project installed via vue-CLI. Vue-cli initialization project method juejin.cn/post/690818… There is an introduction, which will not be repeated here.

npm install bootstrap@next --save
Copy the code

vetur

In order to use syntax hints in the template template of the vue file, we need to modify the settings.json configuration of the vetur. New “vetur. Experimental. TemplateInterpolationService” : true

V – model principle

Vue2.0

In Vue2.0, for tags with input events (including <input>, <select>, or <textarea>), we can directly use the v-model for two-way data binding, that is, when the user modifies the content of the form, The data bound by the V-Model can be modified automatically.

<template>
    <div>
        <input v-model="msg"/>
        <p>{{msg}}</p>
        <button @click="change">change</button>
    </div>
</template>
<script>
  export default {
      name: "count".data(){
          return {
              msg: ""}},methods: {
          change(){
              this.msg = "Hello Vue"; }}}</script>
Copy the code

The V-Model is essentially a syntactical sugar that listens for user input events to update data and does some special processing for extreme scenarios. Internally, the V-Model uses different properties for different input elements and throws different events:

  • Text and Textarea elements use value property and input events;
  • Checkbox and radio use checked Property and change events;
  • The SELECT field takes value as prop and change as an event.

Translation:

<input v-model="msg"/>/ / equivalent to the<input :value="msg" @input="msg=$event.target.value"/>
Copy the code
<input type="checkbox" value="Java" id="Java" v-model="msg">/ / equivalent to the<input type="checkbox" value="Java" id="Java" :checked="msg" @change="changeInput">// The changeInput function is handled differently according to the type of MSGCopy the code
<select v-model="msg">
    <option value="dog">dog</option>
    <option value="cat">cat</option>
    <option value="Hello Vue">Hello Vue</option>
</select>/ / equivalent.<select :value="msg" @change="changeSelect">
    <option value="dog">dog</option>
    <option value="cat">cat</option>
    <option value="Hello Vue">Hello Vue</option>
</select>
Copy the code

Now that we know the nature of the V-Model, we can define the V-Model for our custom components. As follows:

<myComponent v-model="msg"></myComponent>/ / equivalent to the<myComponent :value="msg" @input="msg=arguments[0]"></myComponent>
Copy the code

In myComponent, a variable is usually defined to receive changes in value; And modifying this variable when going to $emit triggers the input event that the parent component subscribles to.

<template>
    <div>
        {{newVal}}
        <button @click="change">Click on the</button>
    </div>
</template>
<script>
export default {
    name: "my-component".props: {
        value: {
            type: String.default: "Vue"}},watch: {
        value(val){
            this.newVal = val; }},data(){
        return {
            newVal: this.value
        }
    },
    methods: {
        change(){
            this.newVal = "I am.";
            this.$emit("input".this.newVal); }}}</script>
Copy the code

Vue3.0

The initial value is passed in using modelValue and the Update :modelValue event is emitted through emit. See the examples:

//ValidateInput.vue
<template>
    <input :value="inputRef.val" @input="updateValue">
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue';
export default defineComponent({
    props: {
        modelValue: String
    },
    setup(props, context){
        const inputRef = reactive({
            val: props.modelValue || ""
        });
        / / to monitor props. ModelValue
        watch(() = > props.modelValue, (newVal) = > {
            inputRef.val = newVal as string
        })
        const updateValue = (e: KeyboardEvent) = >{
            const targetValue = (e.target as HTMLInputElement).value;
            inputRef.val = targetValue;
            context.emit('update:modelValue', targetValue);
        }
        return{
            inputRef,
            updateValue
        }
    }
})
</script>
Copy the code

Use it in the parent component:

<ValidateInput v-model="msg"/>
Copy the code

Then the bound MSG will change with the change of the input content, which can realize two-way data binding.

Check mechanism

The verification of the form is to judge whether the input content is legal when the user enters it, and to prompt the error message under the form in real time when the input content is illegal. Add a new attribute rules to the component defined above to pass the user’s rules for the current form element; Then, in blur, the rules are iterated to verify the input. The following uses mailbox input box verification as an example.

<template>
    <div class="validate-input-container pb-3">
        <input type="text" 
        class="form-control" :class="{'is-invalid': inputRef.error}"
        :value="inputRef.val" @input="updateValue" @blur="validateInput">
        <span v-if="inputRef.error" class="invalid-feedback">{{inputRef.message}}</span>
    </div>
</template>
<script lang="ts">
const emailReg = / ^ [a - zA - Z0-9.! # $% & * + / = '? ^ _ ` {|} ~ -] + @ [a zA - Z0-9 -] + (? :\.[a-zA-Z0-9-]+)*$/
import { defineComponent, reactive, PropType } from 'vue';
interface RuleProp{
    type: 'required' | 'email' | 'custom'; message: string; validator? :() = > boolean;
}
export type RulesProp = RuleProp[];
export default defineComponent({
    props: {
        modelValue: String.rules: Array as PropType<RulesProp>
    },
    setup(props, context){
        const inputRef = reactive({
            val: props.modelValue || "".error: false.message: ""
        });
        const updateValue = (e: KeyboardEvent) = >{
            const targetValue = (e.target as HTMLInputElement).value;
            inputRef.val = targetValue;
            context.emit('update:modelValue', targetValue);
        };
        const validateInput = () = > {
            if(props.rules){
                const allPassed = props.rules.every((rule: RuleProp) = > {
                    let passed = true;
                    inputRef.message = rule.message;
                    switch(rule.type){
                        case 'required': passed = (inputRef.val.trim() ! = =' ')
                            break;
                        case 'email':
                            passed = emailReg.test(inputRef.val)
                            break;
                        case 'custom':
                            passed = rule.validator ? rule.validator() : true
                            break;
                        default: 
                            break;
                    }
                    returnpassed }) inputRef.error = ! allPassed; }return true
        }
        return{
            inputRef,
            updateValue,
            validateInput
        }
    }
})
</script>
Copy the code

So you can bind the placeholder property to the input tag when you use it. Here are two steps: (1) In the InvalidateInput component, set inheritAttrs: false to indicate that you don’t want the component’s root element to inherit attributes. Bind $attrs to the component

export default defineComponent({
   / / new
    inheritAttrs: false.setup(props, context){... }Copy the code
<input type="text" 
        class="form-control" :class="{'is-invalid': inputRef.error}"
        :value="inputRef.val" @input="updateValue" @blur="validateInput"
        v-bind="$attrs">
Copy the code

More details can be found at cn.vuejs.org/v2/guide/co…

PropType

Add a Vue3.0 knowledge point PropType. We know that the props of a component can be used to define the parameters that the component can pass. Format such as

props: {
	rules: Array
}
Copy the code

You need to use a constructor after the rules variable for its type. To take advantage of TypeScript smart hints, we usually define the data type of each item in the array. In this case, we use TypeScript interface and type. To combine the defined type with the constructor, Vue3.0 provides propTypes to combine constructors with specific types. Use method as above:

interface RuleProp{
    type: 'required' | 'email' | 'custom'; message: string; validator? :() = > boolean;
}
export type RulesProp = RuleProp[];
export default defineComponent({
    props: {
        modelValue: String.rules: Array as PropType<RulesProp>
    }
})
Copy the code

ValidForm encapsulation

Expose the Submit form submission event in ValidateForm.vue; Slot is also used to customize the content of each item in the Form and the style of the submit button. Finally, we listen for the click event of the outermost div, so that when the user clicks submit, the form will be submitted.

//ValidateForm.vue
<template>
  <form class="validate-form-container">
    <slot name="default"></slot>
    <div class="submit-area" @click.prevent="submitForm">
      <slot name="submit">
        <button type="submit" class="btn btn-primary">submit</button>
      </slot>
    </div>
  </form>
</template>
<script lang="ts">
import {defineComponent} from 'vue'
export default defineComponent({
    emits: ['submit'].setup(props, context){
        const submitForm = function(){
            console.log("click");
            context.emit("submit".false)}return{
            submitForm
        }
    }
})
</script>
Copy the code

Component communication

We know that the parent component can pass data to the child component via props; A child component can pass data to its parent through emit events. Or $parent, $children to get the component information of the response. In Vue2.0, parent-child communication using slots can only be achieved through EventBus, the event stack; In Vue3.0, mitt, a plug-in, is recommended for publishing subscriptions to events. Install mitt first

npm install mitt --save
Copy the code

In order to access the ValidateInput validation methods for each child of the ValidateForm component. We need to subscribe to the Form-validate event in the ValidateForm component, and the callback is used to collect validation methods for each child component. In the child component, the component needs to publish the form-validate event and pass in its own validation function at initialization time. When the user clicks the Submit button, the ValidateForm component triggers the ValidateInput method for each child component. Thus the form verification is realized.

There are two other points that need to be noted :(1) the parent component should subscribe first and then the child component should publish so that the parent component can subscribe. So in ValidateForm we can subscribe to the form-validate event in setup; In ValidateInput, issue the form-validate event when onMounted. (2) The final aftermath should also be done well. We need to unsubscribe events while ValidateInput onUnmounted and empty the array of child component events.

//ValidateForm.vue
<script lang="ts">
import {defineComponent, onUnmounted} from 'vue'
/ / import mitt
import mitt from 'mitt'
export const emitter = mitt();
type ValidateFunc = () = > boolean;
export default defineComponent({
    emits: ['submit'].setup(props, context){
    	// The callback function used to store form validation
        let funcArr: ValidateFunc[] = []
        const submitForm = function(){
            const result = funcArr.map(func= > func()).every(result= > result);
            context.emit("submit", result)
        }
        const callback = function(fn? : ValidateFunc){
            if(fn){ funcArr.push(fn); }}/ / subscription form - validate
        emitter.on("form-validate", callback);
        // Clean up when uninstalling components
        onUnmounted(() = >{
             // Unsubscribe from form-validate
            emitter.off("form-validate", callback);
            funcArr = [];
        })
        return{
            submitForm
        }
    }
})
</script>
Copy the code
<script lang="ts">
/ / import mitt
import {emitter} from './ValidateForm.vue'
export default defineComponent({
    setup(props, context){...// Notice the timing of the lifecycle, publish the form-validate event
        onMounted(() = >{
            emitter.emit("form-validate", validateInput); })... } }) </script>Copy the code

The source code

Github address: github.com/YY88Xu/vali…

Thank you

If this article helps you, please remember to like oh, it will be my motivation to keep writing