Wrap Vue3 form binding in Typescript, with support for anti-shake and other features.

Time:2024-1-30

Vue3 parent-child component passing values, binding form data, secondary encapsulation of UI libraries, anti-jitter, etc., I think we are all very familiar with it, this article introduces a way to use theTypescript method of unified encapsulation in the manner of the

Basic usage

Vue3 provides a simple way for form binding:v-model. Very user-friendly.v-model="name" That’s all.

Make your own components

But when we want to make a component ourselves, we have a bit of trouble:

https://staging-cn.vuejs.org/guide/components/events.html#usage-with-v-modelWrap Vue3 form binding in Typescript, with support for anti-shake and other features.https://links.jianshu.com/go?to=https%3A%2F%2Fstaging-cn.vuejs.org%2Fguide%2Fcomponents%2Fevents.html%23usage-with-v-model

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

requires us to define the props、emit、input Events, etc.

Secondary packaging of UI library components

If we want to encapsulate the UI libraries, we’re in a little bit of trouble again:

https://staging-cn.vuejs.org/guide/components/events.html#usage-with-v-modelWrap Vue3 form binding in Typescript, with support for anti-shake and other features.https://links.jianshu.com/go?to=https%3A%2F%2Fstaging-cn.vuejs.org%2Fguide%2Fcomponents%2Fevents.html%23usage-with-v-model

// <script setup>
import { computed } from 'vue'

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const value = computed({
  get() {
    return props.modelValue
  },
  set(value) {
    emit('update:modelValue', value)
  }
})
// </script>

<template>
  <el-input v-model="value" />
</template>

Since v-models can’t use component props directly, and theel-input Again, the nativevalue It’s turned intov-model form, so it is necessary to use thecomputed Doing transit makes the code a bit cumbersome.

The code is a little more complicated if you consider the anti-shake feature.

Why does the code get messier and messier? Because of the lack of timely refactoring and necessary encapsulation!

Creating a vue3 project

With the situation told, we move on to the solution.

The first step is to use the latest toolchain for vue3, create-vue, to build a toolchain that supports theTypescript of the project.
https://staging-cn.vuejs.org/guide/typescript/overview.htmlWrap Vue3 form binding in Typescript, with support for anti-shake and other features.https://links.jianshu.com/go?to=https%3A%2F%2Fstaging-cn.vuejs.org%2Fguide%2Ftypescript%2Foverview.html

First, we’ll use Typescript to encapsulate the v-model, and then we’ll use a more convenient way to implement the requirements, and we’ll see which of the two is more suitable.

Encapsulation of v-model

Let’s start with the v-model、emit Make a simple package and then add anti-shake.

Basic Packaging

  • ref-emit.ts

import { customRef } from 'vue'

/**
 * Direct input of the control without stabilization. Responsible for parent-child components interacting with form values
 * @param props props of a component
 * @param emit emit of the component
 * @param key v-model name for emit
 */
export default function emitRef<T, K extends keyof T & string>
(
  props: T,
  emit: (event: any, ...args: any[]) => void,
  key: K
) {
  return customRef<T[K]>((track: () => void, trigger: () => void) => {
    return {
      get(): T[K] {
        track()
        return props[key] // Returns the value of modelValue
      },
      set(val: T[K]) {
        trigger()
        // Set the value of modelValue via emit.
        emit(`update:${key.toString()}`, val) 
      }
    }
  })
}
  • K keyof T
    Since the property names should be in the props, use thekeyof T in a way that is constrained.

  • T[K]
    You can use T[K] as the return type.

  • key default value of
    Tried all sorts of things and while it runs, TS reports an error. Maybe I’m not opening it the right way.

  • customRef
    The reason why it is not computed is because of the subsequent addition of stabilization.
    Use emit in set to commit, and get in get to get the values of the properties in props.

  • emit (used form a nominal expression)type
    emit: (event: any, ...args: any[]) => void, various attempts were made and finally ANY was used.

This way the simple encapsulation is complete.

Support for anti-shake

The anti-shake code provided by the official website works fine for native input, but there was a small problem with el-input, so I had to modify it:

  • ref-emit-debounce.ts

import { customRef, watch } from 'vue'

/**
 * :: Anti-dithering inputs to the control, the way emit is done
 * @param props props of a component
 * @param emit emit of the component
 * @param key v-model name, default modelValue, used for emit
 * @param delay Delay time, default 500 milliseconds
 */
export default function debounceRef<T, K extends keyof T> 
(
  props: T,
  emit: (name: any, ...args: any[]) => void,
  key: K,
  delay = 500
) {
  // Timer
  let timeout: NodeJS.Timeout
  // Initialize set property values
  let _value = props[key]
  
  return customRef<T[K]>((track: () => void, trigger: () => void) => {
    // Listening for property changes in the parent component and then assigning values to ensure that properties are set in response to the parent component
    watch(() => props[key], (v1) => {
      _value = v1
      trigger()
    })

    return {
      get(): T[K] {
        track()
        return _value
      },
      set(val: T[K]) {
        _value = val // Binding value
        trigger() // Bind the input to the control, but don't submit it.
        clearTimeout(timeout) // clear the previous timeout
        // Setting new timings
        timeout = setTimeout(() => {
          emit(' update:${key.toString()} ', val) // Submit
        }, delay)
      }
    }
  })
}
  • timeout = setTimeout(() => {})
    Implement anti-shake function to delay data submission.

  • let _value = props[key]
    Define an internal variable that saves data when the user enters a character, for binding the component, and then submitting it to the parent component when it is delayed.

  • watch(() => props[key], (v1) => {})
    Listens for changes in property values and can update the display of the child component when the parent component modifies the value.
    This is because the value of the subcomponent corresponds to the internal variable _value and does not directly correspond to the value of the property of the props.

This realizes the function of anti-shake.

Methods that pass the model directly.

A form often involves multiple fields, and if each of them is used with the v-model If it is passed on in the same way as a “transit”, then there is a “transit” situation, where “transit” meansemit, whose internal code is more complex.

If the components are deeply nested, there will be multiple “transitions”, which is not straightforward and cumbersome.
In addition, if v-for traversal of form subcontrols is required, it is not convenient to deal with multiple v-model The situation.

So why not pass a form’s model object directly into the child component? That way, no matter how many layers of components are nested, the address is operated on directly, plus it’s easier to handle the case where one component corresponds to multiple fields.

Of course, there is a bit of a hassle in that you need to pass in an extra attribute to record the name of the field that the component is going to operate on.

integrated componentprops The type of theshallowReadonly, i.e., root-level read-only, so we can modify the properties of the incoming object.

base package method

  • ref-model.ts

import { computed } from 'vue'

/**
 * Direct input of the control without stabilization. Responsible for parent-child components interacting with form values.
 * @param model The props model of the component
 * @param colName Name of attribute to be used
 */
export default function modelRef<T, K extends keyof T> (model: T, colName: K) {
  
  return computed<T[K]>({
    get(): T[K] {
      // Returns the value of the specified attribute inside the model
      return model[colName]
    },
    set(val: T[K]) {
      // Assign values to specific properties inside the model
      model[colName] = val
    }
  })
}

We can also use thecomputed to do the transit, or do you useK extends keyof TDo a little restraint.

Anti-shake implementation

  • ref-model-debounce.ts

import { customRef, watch } from 'vue'

import type { IEventDebounce } from '../types/20-form-item'

/**
 * :: Modify the model's stabilization directly
 * @param model The props model of the component
 * @param colName Name of attribute to be used
 * @param events events collection, run: submit immediately; clear: clear the timer, used for Chinese character entry
 * @param delay delay time, default 500 milliseconds
 */
export default function debounceRef<T, K extends keyof T> (
  model: T,
  colName: K,
  events: IEventDebounce,
  delay = 500
) {

  // Timer
  let timeout: NodeJS.Timeout
  // Initialize set property values
  let _value: T[K] = model[colName]
    
  return customRef<T[K]>((track: () => void, trigger: () => void) => {
    // Listening for property changes in the parent component and then assigning values to ensure that properties are set in response to the parent component
    watch(() => model[colName], (v1) => {
      _value = v1
      trigger()
    })

    return {
      get(): T[K] {
        track()
        return _value
      },
      set(val: T[K]) {
        _value = val // Binding value
        trigger() // Bind the input to the control, but don't submit it.
        clearTimeout(timeout) // clear the previous timeout
        // Setting new timings
        timeout = setTimeout(() => {
          model[colName] = _value // Submitted
        }, delay)
      }
    }
  })
}

Comparison will find that the code is basically the same, just take the value, assign the value of the place is different, one uses emit, a direct to themodelThe attribute assignment of the

So can it be merged into one function? Of course you can, it’s just that the parameters aren’t well named, plus you need to make judgments, which makes it look a bit hard to read, so it’s more straightforward to make two functions.

I prefer to pass directly into themodel object, very concise.

Encapsulation of range values (multiple fields)

Start date, end date, can be divided into two controls, or you can use one control, if you use one control, it involves type conversion, field correspondence.

So we can wrap another function.

  • ref-model-range.ts

import { customRef } from 'vue'

interface IModel {
  [key: string]: any
}

/**
 * :: emit is not supported when a control corresponds to multiple fields
 * @param model model of the form
 * @param arrColName Use multiple attributes, arrays
 */
export default function range2Ref<T extends IModel, K extends keyof T>
(
  model: T,
  ...arrColName: K[]
) {

  return customRef<Array<any>>((track: () => void, trigger: () => void) => {
    return {
      get(): Array<any> {
        track()
        // Multiple fields, need to splice attribute values
        const tmp: Array<any> = []
        arrColName.forEach((col: K) => {
          // Get the values of the properties specified inside the model as an array.
          tmp.push(model[col])
        })
        return tmp
      },
      set(arrVal: Array<any>) {
        trigger()
        if (arrVal) {
          arrColName.forEach((col: K, i: number) => {
            // Split attribute assignment, number of values may be less than number of fields
            if (i < arrVal.length) {
              model[col] = arrVal[i]
            } else {
              model[col] = ''
            }
          })
        } else {
          // Clear Selection
          arrColName.forEach((col: K) => {
            model[col] = '' // undefined
          })
        }
      }
    }
  })
}
  • IModel
    Define an interface for constraining the generalized T such thatmodel[col] It won’t report an error.

The stabilization is not considered here, as it is not needed in most cases.

Usage

Once encapsulated, it’s very easy to use it inside the component, just one line.

First make a parent component and load various child components for a demo.

  • js

// Encapsulation of v-model, emit
  const emitVal = ref('')
  // Pass the object
  const person = reactive({name: 'test ', age: 111})
  // Scope, divided into two attributes
  const date = reactive({d1: '2012-10-11', d2: '2012-11-11'})
  • template

Encapsulation of emit
  <input-emit v-model="emitVal"/>
  <input-emit v-model="person.name"/>
  Model encapsulation
  <input-model :model="person" colName="name"/>
  <input-model :model="person" colName="age"/>
  Range of values for model
  <input-range :model="date" colName="d1_d2"/>

emit

We make a subcomponent:

  • 10-emit.vue

// <template>
  <! ---test emitRef -->
  <el-input v-model="val"></el-input>
// /template>

// <script lang="ts">
  import { defineComponent } from 'vue'

  import emitRef from '../../../../lib/base/ref-emit'

  export default defineComponent({
    name: 'nf-demo-base-emit',
    props: {
      modelValue: {
        type: [String, Number, Boolean, Date]
      }
    },
    emits: ['update:modelValue'],
    setup(props, context) {

      const val = emitRef(props, context.emit, 'modelValue')

      return {
        val
      }
    }
  })
// </script>

Define it.props respond in singingemit, and then just call the function.
also supportsscript setup The way:

  • 12-emit-ss.vue

<template>
  <el-input v-model="val" ></el-input>
</template>

<script setup lang="ts">
  import emitRef from '../../../../lib/base/ref-emit'

  const props = defineProps<{
    modelValue: string
  }>()

  const emit = defineEmits<{
    (e: 'update:modelValue', value: string): void
  }>()
 
  const val = emitRef(props, emit, 'modelValue')

</script>

definepropsDefinitionemitand then call theemitRef

model

We make a subcomponent

  • 20-model.vue

<template>
  <el-input v-model="val2"></el-input>
</template>

<script lang="ts">
  import { defineComponent } from 'vue'
  import type { PropType } from 'vue'
  import modelRef from '../../../../lib/base/ref-model'

  interface Person {
    name: string,
    age: 12
  }

  export default defineComponent({
    name: 'nf-base-model',
    props: {
      model: {
        type: Object as PropType<Person>
      },
      colName: {
        type: String
    },
    setup(props, context) {
      const val2 = modelRef(props.model, 'name')
      return {
        val2
      }
    }
  })
</script>

Define the props and just call it.
Although there is an additional parameter describing the name of the field, you don’t have to define and pass an emit.

range value

<template>
  <el-date-picker
    v-model="val2"
    type="daterange"
    value-format="YYYY-MM-DD"
    range-separator="-"
    start-placeholder="Start Date"
    end-placeholder="End Date"
  />
</template>

<script lang="ts">
  import { defineComponent } from 'vue'
  import type { PropType } from 'vue'

  import rangeRef from '../../../../lib/base/ref-model-range2'
 
  interface DateRange {
    d1: string,
    d2: string
  }

  export default defineComponent({
    name: 'nf-base-range',
    props: {
      model: {
        type: Object as PropType<DateRange>
      },
      colName: {
        type: [String]
      }
    },
    setup(props, context) {
      const val2 = rangeRef<DateRange>(props.model, 'd1', 'd2')
      return {
        val2
      }
    }
  })
</script>

el-date-picker component in thetype=”daterange” when the v-model is an array, and the back-end database setup, which is typically two fields, such as startDate、endDate,What needs to be submitted is also in the form of an object, so that you need to do conversions between arrays and objects.

and our encapsulatedrangeRef It’s possible to do just such a conversion.

TS’s embarrassment

You may notice that the above example doesn’t use thecolName attribute, but instead passes the character layer parameter directly.

Because TS can only do static checking, not dynamic checking, writing strings directly is a static way that TS can check.

However, the use ofcolName For attributes, it’s the dynamic way, the TS check doesn’t support dynamics, and then it just gives an error message.

It works fine, but it’s still annoying to look at the red lines, so I ended up encapsulating a loneliness.

To compare.

Comparison Programemitmodel
Typology clarificationstraitened circumstancesexplicitly
Parameters (use)anboth
efficiencyTransit is required within emitModify directly using the object address
packaging difficultyIt’s a bit of a hassle.relaxing
component using theNeed to define emitNo need to define emit
Multi-field (encapsulation)No need for separate packagingRequires separate package
Multiple fields (use)Need to write multiple v-modelsNo need to increase the number of parameters
Multiple fields (form v-for)not easy handleliable (to)

If there are subcomponents in the form that you want to traverse in a v-for fashion, it’s obviously easier to do it the model way because you don’t have to think about how many v-models you need to write for a single component.

Recommended Today

DML statements in SQL

preamble Previously we have explained DDL statements in SQL statements. Today we will continue with the DML statement of SQL. DML is the Data Manipulation Language.Used to add, delete, and change data operations on the tables in the library.。 1. Add data to the specified field(INSERT) 2. Modify data(UPDATE) 3. Delete data(DELETE) catalogs preamble I. […]