Basic operation

Here is a simple development of a VInput box component. A component is like a function that handles inputs and outputs. Vue3 provides two arguments on the setup function, props, and the EMIT method under context, to handle input and output, respectively.

props

Now that the VInput is a child component, I need it to be able to accept a value passed by the parent so that it can do the following logical processing for me before returning it to the parent. So, here we need some basic parent-child communication methods, V-bind, props.

Parent component

<template> // Pass data to child components via v-bind <VInput :value="valueRef" /> </template> const valueRef = ref(") copies the codeCopy the code

In the VInput

<template> <input :value="value" type="text" /> </template> <script lang="ts"> import { defineComponent } from 'vue' export default defineComponent({ name: 'VInput', props: { value: String}, setup(props) {// other logic // to accept this value console.log(props. Value) return {}}}) </script> Copy codeCopy the code

emit

When we accept parameters in the component and do some logical processing, we need to return the value to the external, and the external needs to implement an event function to accept it. At this point I can use the emit method

Suppose we want the VInput component to return a string of limited length. In this case, we need to implement a corresponding event function to receive this value, and then VInput internal emit event, return the value as a parameter.

VInput

<template> <input :value="value" type="text" @input="onInput" ref="inputRef" /> </template> <script lang="ts"> import { defineComponent, ref } from 'vue' export default defineComponent({ name: 'VInput', props: { value: String, maxLength: Number }, setup(props, Const inputRef = ref() const limitLength = (value: string, maxLength: Number) => value.slice(0, maxLength) const Controlled = (value: string) => { inputRef.value.value = value } const onInput = (e: any) => { let value = e.target.value if (typeof props.maxLength === 'number' && props.maxLength >= 0) { value = LimitLength (value, props. MaxLength)} controlled(value) // Emit ('onInput', value)} return {onInput, InputRef}}}) </script> Copies the codeCopy the code

The parent component

<template> // Pass a function to the child component via v-on, User accepts the return value <VInput :value="valueRef" :maxLength="10" @oninput ="onInput" /> </template> <script lang="ts"> import { defineComponent, ref } from 'vue' import VInput from '@/components/VInput.vue' export default defineComponent({ name: 'Demo', components: { VInput }, setup() { const valueRef = ref('') const onInput = (value: String) => {// Accept the value returned by the child component VInput console.log(value) // Change the corresponding value valueref. value = value} return {valueRef, OnInput}}}) </script> Copy the codeCopy the code

For this type of input component, I guess you don’t want to have to go through the hassle of receiving and changing a value in a parent component, so VUE provides a V-model for faster input and output.

v-model

The Vue3 documentation shows that the usage of this directive has changed somewhat. In the past, to achieve a custom non-form component bidirectional binding, we need to use xxxx.sync syntax to achieve, now this directive has been abolished, but the unified use of v-model directive.

The parent component

The new V-Model can also support bidirectional binding of multiple data.

<template> <VBtn V-model :value="valueRef" V-model :keyword="keywordRef" /> </template> Copies the codeCopy the code

Custom non-form components

<template> <button @click="clickHandle">click</button> </template> export default defineComponent({ name: 'VBtn', props: {value: String, keyword: String}, setup(props, {emit}) {// Any) => {// omit other code // modify data emit('update:value', value) emit('update:keyword', value + '123') } return { // ... }}}) copy the codeCopy the code

This is an introduction to some of the basic communication apis in Vue3. Vue3 is usually developed using the Composition Api, so you’ll find that you can’t use this.$XXX to call a function or property on an instance. This.$parent, this.$children, this.$on, this.$emit, etc.

What about communication between components in Vue3? Let’s go through the scenarios one by one, from simple to complex.

Let’s take a look at the three form components we developed together and see how they work in practice:

<template> <ValidateForm ref="validateFormRef1" :model="state" :rules="rules"> <ValidateFormItem label=" user name" Prop ="keyword"> <ValidateInput placeholder=" required V-model :modelValue="state.keyword" /> </ValidateFormItem> <ValidateFormItem label=" password" prop="password"> <ValidateInput placeholder=" v-model:modelValue="state.password" /> </ValidateFormItem> </ValidateForm> <button class="btn btn-primary" @click="submit(0)"> submit </button> </templateCopy the code

The functionality of all components is modeled after the Element UI.

The father the son

A parent component can pass data to a child component in two ways:

  • v-bind
  • Refs gets a function inside the child component and calls the parameter directly.

Refs way

We won’t go into details about v-bind, but we’ve already covered how to use it in the basic Operations section. This section describes how Vue3 takes a subcomponent instance from ref and calls a function on it to pass a value to the subcomponent.

Child components

<template> // render the value received from the parent <div>Son: {{ valueRef }}</div> </template> <script lang="ts"> import { defineComponent, ref } from 'vue' export default defineComponent({ name: 'Son', setup() {const valueRef = ref(") const acceptValue = (value: String) => (valueref. value = value) return {acceptValue, valueRef}}}) </script> Copy codeCopy the code

The parent component

<template> <div>sonRef</div> < button@click ="sendValue">send</button> <Son ref="sonRef" /> </template> <script lang="ts"> import {defineComponent, ref } from 'vue' import Son from '@/components/Son.vue' export default defineComponent({ name: 'Demo', components: {Son}, setup() {// If ref is null, SonRef = ref() const sendValue = () => {// sonRef = ref() const sendValue = () => { Console. log(sonref.value) // By calling the son component instance's methods, AcceptValue ('123456')} return {sonRef, sendValue}}) </script> Copy code to itCopy the code

This.$refs, this.$children this.$refs, this. The method is the same, but Vue3 does not have the this black box.

As you can see, all variables and methods returned by Setup can be retrieved from a subcomponent instance retrieved from ref, as well as other internal attributes. Take a look at the official documentation for the description of the Vue composite API.

Summary of REF method

Advantages:

  1. A parent component can get data to be passed quickly to a child component that is determined to exist
  2. The transmission parameters are not limited, and the transmission mode is flexible

Disadvantages:

  1. The subcomponent obtained by ref must be determined to exist (if it is not determined to exist, such as a subcomponent on a slot,v-ifSubcomponents of control)
  2. The child component also needs to implement methods that accept parameters

The father passes on further offspring

In general, there are two ways to pass values to the depth level:

  • provide / inject
  • vuex

provide / inject

When you see the word “deep”, the provide/inject option in Vue2 is definitely the first thing that comes to mind. Yes, the same logic applies to VUE3, where the two options become two methods.

Provide allows us to pass a piece of data to all descendants of the current component. All descendants can inject this data or not to accept it.

Actual Application Scenarios

There are two main application scenarios, one is when passing a parameter or a function deeply, the other is when passing a parameter to an uncertain component on the slot.

Focus on passing parameters to components on slots. Implement an outermost ValidateForm component that accepts the entire form data and validates the entire form data. It provides a slot inside for some uncertain components. There is also a ValidateFormItem component that accepts a field name to know exactly which field needs to be validated.

Component-based development requires decoupling of parameters and functions, so we design it as follows:

  • ValidateForm:model.rules, just accept the data and validation rules for the entire form
  • ValidateFormItem:prop, just accept the field name, just know which field you need to validate
<template> < validateFormRef ="validateFormRef" :model="formData" :rules="rules"> <ValidateFormItem label=" user name" prop="keyword"> <! -- field component --> </ValidateFormItem> <ValidateFormItem label=" Password "prop="password"> -- Field component --> </ValidateFormItem> </ValidateForm> </template> Copy codeCopy the code

If the ValidateFormItem component needs to validate a field with prop, it needs to retrieve the form’s data. FormData [Prop] is used to retrieve the field’s value. Where does the formData come from? First, it is impossible to pass a ValidateFormItem component every time you write one. In practice, we can’t determine how many ValidateFormItem components to write under ValidateForm. If we manually pass a copy of the form’s data for each one, it would be a lot of redundant code and cumbersome to write. So, the ValidateForm component accepts and distributes it independently.

ValidateForm

So we need ValidateForm to distribute the data down.

<template> <form> <slot></slot> </form> </template> <script lang="ts"> import { defineComponent, provide } from 'vue' export const modelKey = Symbol() export const rulesKey = Symbol() export default defineComponent({ name: 'ValidateForm', props: { model: { type: Object }, rules: { type: Provide (modelKey, props. Model) provide(rulesKey, Props. Rules) return {}}}) </script> Copy codeCopy the code

ValidateFormItem

ValidateFormItem accepts the data passed above.

<script lang="ts"> import { defineComponent, reactive, inject, provide } from 'vue' import { modelKey, rulesKey } from './ValidateForm.vue' export default defineComponent({ name: 'ValidateFormItem', props: { label: String, required: { type: Boolean, default: false }, prop: String}, setup(props) {// Accept data passed down from ValidateForm const model = inject<any>(modelKey, ref({})) const rules = inject<any>(rulesKey, Ref ({})) // Obtain the data to be verified in model and rules according to props. Prop console.log(model[props. Return {//... }}}) </script> Copy the codeCopy the code

vuex

Vuex has long been an excellent solution in the VUE ecosystem for data sharing between components at different levels. Not only does it work in parent-to-child, but it’s a great solution for parent-to-parent, or ancestor-to-descendant, progenitor to progenitor, or sibling components. Because it is a centralized state management model. Its implementation is also reactive in nature. Here is a brief mention of how it is used in Vue3.

To create astore

import { createStore } from 'vuex' export enum Mutarions { SET_COUNT = 'SET_COUNT' } export default createStore({ state:  { count: 231 }, getters: { count: state => state.count }, mutations: { [Mutarions.SET_COUNT]: (state, num: Number) => (state.count = num)}}) copy the codeCopy the code

The parent component

<template> <div>father</div> <Son ref="sonRef" /> </template> <script lang="ts"> import { defineComponent, ref } from 'vue' import Son from '@/components/Son.vue' import { useStore } from 'vuex' import { Mutarions } from '@/store/index' export default defineComponent({ name: 'Father', components: { Son }, setup() { const valueRef = ref(100) const store = useStore() store.commit(Mutarions.SET_COUNT, Valueref. value) return {}}}) </script> Copy codeCopy the code

Child components

<template> <div>Son: {{ count }}</div> </template> <script lang="ts"> import { defineComponent, computed } from 'vue' import { useStore } from 'vuex' export default defineComponent({ name: 'Son', setup() { const store = useStore() const count = computed(() => store.getters.count) return { count } } }) </script> Copy the codeCopy the code

Child the parent

There are three ways that a child can pass data to its parent:

  • v-on
  • Refs way
  • Event center

Refs way

It is also true to pass a data to the parent through a REF. The subcomponent implements a function that returns a value. The parent component calls this method after the parent component gets the child component instance through ref to get the required return value.

To look at the actual application scenario, we want the ValidateForm component to validate all of the following form items, and then return one of the validation states within the component via a function.

The parent component

<template> < validateFormRef ="validateFormRef" :model="formData" :rules="rules"> <ValidateFormItem label=" user name" prop="keyword"> <! -- field component --> </ValidateFormItem> <ValidateFormItem label=" Password "prop="password"> -- Field component --> </ValidateFormItem> </ValidateForm> </template> <script lang="ts"> import {defineComponent, ref } from 'vue' export default defineComponent({ name: 'demo', Setup () {const validateFormRef = ref() const validateFormRef = ref() (this. ValidateFormRef. The validate ()) {/ / after the success of the form validation, do the subsequent operations} return {validateFormRef}}}) < / script > duplicate codeCopy the code

ValidateForm

<template> <form> <slot></slot> </form> </template> <script lang="ts"> import { defineComponent } from 'vue' export default defineComponent({ name: 'ValidateForm', Setup () {const validate = async () => {let result = false Return result} return {validate}}) </script> Copy the codeCopy the code

This method also provides access to the internal data of child components, just as closure functions do.

Event center

Why is this kind of communication brought here? Because I think it would be very appropriate to use the event center in the next actual case. In the previous section, we left a hole in the fact that in order for the ValidateForm component to validate the entire form, it had to find a way for each ValidateFormItem to return its internal validation results to it.

There are two problems first

  1. ValidateFormThe following components are mounted through slots, so they will not passrefTo get an instance of each subform item, so you can’t get eachValidateFormItemThe validation status of the.
  2. There is a picture in the chapter above that shows the passagerefGet the component instance. You can find it. You can find it$parentAttributes, but none$childrenProperties. This is awkward. We can’t be likeVue2As in theValidateFormThrough the$childrenGet an instance of each child component.

solution

Since there is no way to get the component instance on the slot, let’s bypass it and do it through an event center. Here’s the idea:

  1. inValidateFormWhen the instance is initialized, create an event centerEmitterInstance, which registers an event, accepts a function when the event is executed, and stores it in a queue.
  2. Will thisEmitterthroughprovidePass to future generations to ensure that the event center is in a differentValidateFormComponents are all independent. In other words, if you write more than oneValidateFormTheir event centers don’t interfere with each other.
  3. inValidateFormItemThe use ofinjectThat receives its own form fieldEmitterAt mount time, executeEmitterOn the event, will own the internalvalidateFunction, pass to send toValidateFormAnd caches the method to the queue.
  4. ValidateFormWhen the checksum is executed, all the checksum functions in the queue can be executed and the checksum result can be obtained.

Specific code implementation:

Let’s implement an Emitter event center class

Import {EmitterHandles} from '@/type/utils' export class Emitter { EmitterHandles = {} // Register events on(eventName: string, eventHandle: Function) {this.events[eventName] = eventHandle} // Delete event off(eventName: String) {if (this.events[eventName]) {delete this.events[eventName]}} // Emit events (eventName: string,... rest: any[]) { if (this.events[eventName]) { this.events[eventName](... Rest)}}} copy the codeCopy the code

Once the event center is in place, let’s refine the ValidateForm code

<script lang="ts"> import { defineComponent, nextTick, provide } from 'vue' import { Emitter } from '@/utils/emitter' type ValidateFunc = () => boolean export const emitterKey  = Symbol() export const modelKey = Symbol() export const rulesKey = Symbol() export default defineComponent({ name: 'ValidateForm', props: { model: { type: Object }, rules: { type: // Provide (modelKey, props. Model) provide(rulesKey, Const Emitter = new Emitter() // Provide (emitterKey, Emitter) // Accept the validation function returned by the formItem component and store it. On ('acceptValidate', (validateFunc: Const ValidateFunc) => {validateist.push (ValidateFunc)}) ValidateFunc[] = [] const validate = () => {return validateist.map (fn =>) Fn ()).every(valid => valid)} return {validate}}}) </script> copies the codeCopy the code

Ok, now that we’ve implemented the logic for validateForm, let’s write the logic for validateFormItem

<template> <div class="form-group"> <label v-if="label" class=" col-form-label">{{ label }}</label> <slot></slot> <small  v-if="error.isError" class="invalid-feedback"> {{ error.errorMessage }} </small> </div> </template> <script lang="ts"> import { Emitter } from '@/utils/emitter' import { defineComponent, reactive, inject, onMounted, provide } from 'vue' import { emitterProviderKey, modelKey, rulesKey } from './ValidateForm.vue' export default defineComponent({ name: 'ValidateFormItem', props: { label: String, required: { type: Boolean, default: false }, prop: String }, Setup (props) {// Accept Emitter event center const Emitter = Inject <Emitter>(emitterProviderKey) const model = inject<any>(modelKey) const rules = inject<any>(rulesKey) const error = reactive({ isError: false, errorMessage: Const prop = props. Prop if (prop & model & rules && rules[prop]) {const prop = props. Prop if (prop & model & rules && rules[prop]) { const result = rules[prop].some((item: any) => { if (! item.validator(model[prop])) { console.warn(`${prop}:${item.message}`) error.isError = true error.errorMessage = item.message return true } }) return ! Result} return true} // When the component is mounted, OnMounted (() => {emitter && emitter. Emit ('acceptValidate', ValidateField)}) return {error}}}) </script> Copy codeCopy the code
  1. Register events, distribute event centers

  2. Execute the event and send the validation function

The whole process is summed up by the top-level component creating and distributing the event center and registering the event listener functions. The descendant component executes the event and then sends the message, and the top-level component reclaims the message.

Tips

Once again, when using the Emitter event center, we created and sent it in the Setup of the ValidateForm, rather than using a global event center. Like bosses this article Vue components communication mode and application scenario summary concluded that, in the form of the event bus is a fatal weakness, if there is a more common components on a page, as long as we transfer data to one of them, but every method of common components are bound to accept the data that will be a chaotic situation. However, our event bus is not a global one, but rather an event center within a single scope.

Because the event center is created inside the current component and published downward using provide, only descendants of the current component can use the event center. So, even if multiple ValidateForms are written to a surface, their validateForms are independent of each other.

<template> < validateFormRef1 :model="formData1" :rules="rules"> <ValidateFormItem label=" username" prop="keyword"> <! -- field component --> </ValidateFormItem> <ValidateFormItem label=" Password "prop="password"> </ValidateFormItem> </ValidateForm> </ValidateForm ref="validateFormRef2" :model="formData2" :rules="rules"> <ValidateFormItem label=" prop "="keyword"> <! -- field component --> </ValidateFormItem> <ValidateFormItem label=" Password "prop="password"> -- Field component --> </ValidateFormItem> </ValidateForm> </template> Copy codeCopy the code

Summary of Incident Center

Advantages:

  1. Can be solvedVue3You can’t usethis.$childrenThe problem of
  2. It can be used flexibly and is not restricted by the component level
  3. This type of communication is not constrained by the framework

Disadvantages:

  1. You need to control the scope of the event center
  2. You need to control the specification of event names

Event center advanced

Vue’s feature API is more granular in Vue3’s Composition API. We can modify the event center with a custom requirement.

Reactive and Ref can be introduced to help maintain a responsive data inside the event center, so that the corresponding view can be updated when the event center conducts certain communication behaviors. You can also use computed to compute attributes.

import { reactive, ref, Computed} from 'vue' export class Emitter {reactive({}) private events: EmitterHandles = ref({}) // Records the number of events in the current event center. Private eventLength = computed(() => object.keys (events.value).length) // Omit some code} Copy codeCopy the code

Watch is added to realize the function of data monitoring to perform certain logical behaviors. I think the Composition API and the React Hooks API are very powerful because they allow us to use function functions as building blocks to assemble any application we want.

The deep offspring communicate to the top, and the brothers communicate

Descendants can pass values to ancestors, or sibling components can pass values using vuEX or event center. Sibling levels, or adjacent levels, can use ref,$parent, etc.