用Typescript 的方式封装Vue3的表单绑定,支持防抖等功能。
Vue3 的父子组件传值、绑定表单数据、UI库的二次封装、防抖等,想来大家都很熟悉了,本篇介绍一种使用 Typescript 的方式进行统一的封装的方法。
基础使用方法
Vue3对于表单的绑定提供了一种简单的方式:v-model。对于使用者来说非常方便,v-model="name"
就可以了。
自己做组件
但是当我们要自己做一个组件的时候,就有一点麻烦:
https://staging-cn.vuejs.org/guide/components/events.html#usage-with-v-model
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
需要我们定义 props、emit、input 事件等。
对UI库的组件进行二次封装
如果我们想对UI库进行封装的话,就又麻烦了一点点:
https://staging-cn.vuejs.org/guide/components/events.html#usage-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>
由于 v-model 不可以直接用组件的 props,而 el-input 又把原生的 value 变成了 v-model 的形式,所以需要使用 computed 做中转,这样代码就显得有点繁琐。
如果考虑防抖功能的话,代码会更复杂一些。
代码为啥会越写越乱?因为没有及时进行重构和必要的封装!
建立 vue3 项目
情况讲述完毕,我们开始介绍解决方案。
首先采用 vue3 的最新工具链:create-vue, 建立一个支持 Typescript 的项目。
https://staging-cn.vuejs.org/guide/typescript/overview.html
先用 Typescript 的方式封装一下 v-model,然后再采用一种更方便的方式实现需求,二者可以对照看看哪种更适合。
v-model 的封装
我们先对 v-model、emit 做一个简单的封装,然后再加上防抖的功能。
基本封装方式
- ref-emit.ts
import { customRef } from 'vue'
/**
* 控件的直接输入,不需要防抖。负责父子组件交互表单值
* @param props 组件的 props
* @param emit 组件的 emit
* @param key v-model 的名称,用于 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] // 返回 modelValue 的值
},
set(val: T[K]) {
trigger()
// 通过 emit 设置 modelValue 的值
emit(`update:${key.toString()}`, val)
}
}
})
}
K keyof T
因为属性名称应该在 props 里面,所以使用 keyof T 的方式进行约束。T[K]
可以使用 T[K] 作为返回类型。key 的默认值
尝试了各种方式,虽然可以运行,但是TS会报错。可能是我打开的方式不对吧。customRef
为啥没有用 computed?因为后续要增加防抖功能。
在 set 里面使用 emit 进行提交,在 get 里面获取 props 里的属性值。emit 的 type
emit: (event: any, ...args: any[]) => void
,各种尝试,最后还是用了any。
这样简单的封装就完成了。
支持防抖的方式
官网提供的防抖代码,对应原生 input 是好用的,但是用在 el-input 上面就出了一点小问题,所以只好修改一下:
- ref-emit-debounce.ts
import { customRef, watch } from 'vue'
/**
* 控件的防抖输入,emit的方式
* @param props 组件的 props
* @param emit 组件的 emit
* @param key v-model的名称,默认 modelValue,用于emit
* @param delay 延迟时间,默认500毫秒
*/
export default function debounceRef<T, K extends keyof T>
(
props: T,
emit: (name: any, ...args: any[]) => void,
key: K,
delay = 500
) {
// 计时器
let timeout: NodeJS.Timeout
// 初始化设置属性值
let _value = props[key]
return customRef<T[K]>((track: () => void, trigger: () => void) => {
// 监听父组件的属性变化,然后赋值,确保响应父组件设置属性
watch(() => props[key], (v1) => {
_value = v1
trigger()
})
return {
get(): T[K] {
track()
return _value
},
set(val: T[K]) {
_value = val // 绑定值
trigger() // 输入内容绑定到控件,但是不提交
clearTimeout(timeout) // 清掉上一次的计时
// 设置新的计时
timeout = setTimeout(() => {
emit(`update:${key.toString()}`, val) // 提交
}, delay)
}
}
})
}
timeout = setTimeout(() => {})
实现防抖功能,延迟提交数据。let _value = props[key]
定义一个内部变量,在用户输入字符的时候保存数据,用于绑定组件,等延迟后再提交给父组件。watch(() => props[key], (v1) => {})
监听属性值的变化,在父组件修改值的时候,可以更新子组件的显示内容。
因为子组件的值对应的是内部变量 _value,并没有直接对应props的属性值。
这样就实现了防抖的功能。
直接传递 model 的方法。
一个表单里面往往涉及多个字段,如果每个字段都使用 v-model 的方式传递的话,就会出现“中转”的情况,这里的“中转”指的是 emit,其内部代码比较复杂。
如果组件嵌套比较深的话,就会多次“中转”,这样不够直接,也比较繁琐。
另外如果需要 v-for 遍历表单子控件的话,也不方便处理多 v-model 的情况。
所以为什么不把一个表单的 model 对象直接传入子组件呢?这样不管嵌套多少层组件,都是直接对地址进行操作,另外也方便处理一个组件对应多个字段的情况。
当然,也有一点麻烦的地方,需要多传入一个属性,记录组件要操作的字段名称。
组件的 props 的类型是 shallowReadonly,即根级只读,所以我们可以修改传入的对象的属性。
基础封装方式
- ref-model.ts
import { computed } from 'vue'
/**
* 控件的直接输入,不需要防抖。负责父子组件交互表单值。
* @param model 组件的 props 的 model
* @param colName 需要使用的属性名称
*/
export default function modelRef<T, K extends keyof T> (model: T, colName: K) {
return computed<T[K]>({
get(): T[K] {
// 返回 model 里面指定属性的值
return model[colName]
},
set(val: T[K]) {
// 给 model 里面指定属性赋值
model[colName] = val
}
})
}
我们也可以使用 computed 来做中转,还是用 K extends keyof T
做一下约束。
防抖的实现方式
- ref-model-debounce.ts
import { customRef, watch } from 'vue'
import type { IEventDebounce } from '../types/20-form-item'
/**
* 直接修改 model 的防抖
* @param model 组件的 props 的 model
* @param colName 需要使用的属性名称
* @param events 事件集合,run:立即提交;clear:清空计时,用于汉字输入
* @param delay 延迟时间,默认 500 毫秒
*/
export default function debounceRef<T, K extends keyof T> (
model: T,
colName: K,
events: IEventDebounce,
delay = 500
) {
// 计时器
let timeout: NodeJS.Timeout
// 初始化设置属性值
let _value: T[K] = model[colName]
return customRef<T[K]>((track: () => void, trigger: () => void) => {
// 监听父组件的属性变化,然后赋值,确保响应父组件设置属性
watch(() => model[colName], (v1) => {
_value = v1
trigger()
})
return {
get(): T[K] {
track()
return _value
},
set(val: T[K]) {
_value = val // 绑定值
trigger() // 输入内容绑定到控件,但是不提交
clearTimeout(timeout) // 清掉上一次的计时
// 设置新的计时
timeout = setTimeout(() => {
model[colName] = _value // 提交
}, delay)
}
}
})
}
对比一下就会发现,代码基本一样,只是取值、赋值的地方不同,一个使用 emit,一个直接给model的属性赋值。
那么能不能合并为一个函数呢?当然可以,只是参数不好起名,另外需要做判断,这样看起来就有点不易读,所以还是做两个函数直接一点。
我比较喜欢直接传入 model 对象,非常简洁。
范围取值(多字段)的封装方式
开始日期、结束日期,可以分为两个控件,也可以用一个控件,如果使用一个控件的话,就涉及到类型转换,字段对应的问题。
所以我们可以再封装一个函数。
- ref-model-range.ts
import { customRef } from 'vue'
interface IModel {
[key: string]: any
}
/**
* 一个控件对应多个字段的情况,不支持 emit
* @param model 表单的 model
* @param arrColName 使用多个属性,数组
*/
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()
// 多个字段,需要拼接属性值
const tmp: Array<any> = []
arrColName.forEach((col: K) => {
// 获取 model 里面指定的属性值,组成数组的形式
tmp.push(model[col])
})
return tmp
},
set(arrVal: Array<any>) {
trigger()
if (arrVal) {
arrColName.forEach((col: K, i: number) => {
// 拆分属性赋值,值的数量可能少于字段数量
if (i < arrVal.length) {
model[col] = arrVal[i]
} else {
model[col] = ''
}
})
} else {
// 清空选择
arrColName.forEach((col: K) => {
model[col] = '' // undefined
})
}
}
}
})
}
- IModel
定义一个接口,用于约束泛型 T,这样model[col]
就不会报错了。
这里就不考虑防抖的问题了,因为大部分情况都不需要防抖。
使用方法
封装完毕,在组件里面使用就非常方便了,只需要一行即可。
先做一个父组件,加载各种子组件做一下演示。
- js
// v-model 、 emit 的封装
const emitVal = ref('')
// 传递 对象
const person = reactive({name: '测试', age: 111})
// 范围,分为两个属性
const date = reactive({d1: '2012-10-11', d2: '2012-11-11'})
- template
emit 的封装
<input-emit v-model="emitVal"/>
<input-emit v-model="person.name"/>
model的封装
<input-model :model="person" colName="name"/>
<input-model :model="person" colName="age"/>
model 的范围取值
<input-range :model="date" colName="d1_d2"/>
emit
我们做一个子组件:
- 10-emit.vue
// <template>
<!--测试 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>
定义一下 props 和 emit,然后调用函数即可。
也支持 script setup 的方式:
- 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>
定义props,定义emit,然后调用 emitRef。
model
我们做一个子组件
- 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>
定义 props,然后调用即可。
虽然多了一个描述字段名称的参数,但是不用定义和传递 emit 了。
范围取值
<template>
<el-date-picker
v-model="val2"
type="daterange"
value-format="YYYY-MM-DD"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</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 组件在 type="daterange" 的时候,v-model 是一个数组,而后端数据库的设置,一般是两个字段,比如 startDate、endDate,需要提交的也是对象形式,这样就需要在数组和对象之间做转换。
而我们封装的 rangeRef 就可以做这样的转换。
TS 的尴尬
可能你会注意到,上面的例子没有使用 colName 属性,而是直接传递字符层的参数。
因为 TS 只能做静态检查,不能做动态检查,直接写字符串是静态的方式,TS可以检查。
但是使用 colName 属性的话,是动态的方式,TS的检查不支持动态,然后直接给出错误提示。
虽然可以正常运行,但是看着红线,还是很烦的,所以最后封装了个寂寞。
对比一下
对比项目 | emit | model |
---|---|---|
类型明确 | 困难 | 很明确 |
参数(使用) | 一个 | 两个 |
效率 | emit内部需要中转 | 直接使用对象地址修改 |
封装难度 | 有点麻烦 | 轻松 |
组件里使用 | 需要定义emit | 不需要定义emit |
多字段(封装) | 无需单独封装 | 需要单独封装 |
多字段(使用) | 需要写多个v-model | 不需要增加参数的数量 |
多字段(表单v-for) | 不好处理 | 容易 |
如果表单里的子组件,想采用 v-for 的方式遍历出来的话,显然 model 的方式更容易实现,因为不用考虑一个组件需要写几个 v-model。
源码
https://gitee.com/naturefw-code/nf-rollup-ui-controller
用Typescript 的方式封装Vue3的表单绑定,支持防抖等功能。的更多相关文章
- Servlet的5种方式实现表单提交(注册小功能)
Servlet的5种方式实现表单提交(注册小功能),后台获取表单数据 用servlet实现一个注册的小功能 ,后台获取数据. 注册页面: 注册页面代码 : <!DOCTYPE html> ...
- Vue + Element-ui实现后台管理系统(5)---封装一个Form表单组件和Table表格组件
封装一个Form表单组件和Table组件 有关后台管理系统之前写过四遍博客,看这篇之前最好先看下这四篇博客.另外这里只展示关键部分代码,项目代码放在github上: mall-manage-syste ...
- 通过jQuery的Ajax方式来提交Form表单
通过jQuery的Ajax方式来提交Form表单 $.ajax({ url:ajaxCallUrl, type:"POST", cache:true, async:false, d ...
- VUE3 之 表单元素
1. 概述 老话说的好:行动起来,原地观望是没有用的. 言归正传,今天我们来聊聊 VUE3 的 表单元素. 2. 表单元素 2.1 文本框与数据绑定 <body> <div id=& ...
- Knockout学习之表单绑定器(上)
表单绑定器 “click”绑定 Click 绑定器可以将javascript函数绑定到指定的dom元素,并且再该元素被点击时将触发绑定的函数,大多数情况下都会使用button.input和a元素,当然 ...
- 【js类库AngularJs】学习angularJs的指令(包括常见表单验证,隐藏等功能)
[js类库AngularJs]学习angularJs的指令(包括常见表单验证,隐藏等功能) AngularJS诞生于2009年,由Misko Hevery 等人创建,后为Google所收购.是一款优秀 ...
- Spirng MVC +Velocity 表单绑定命令对象
通常,表单中的数据在提交之后可以通过Spring MVC的@RequestParam注解在控制器函数的参数列表中中提取出来,但是一旦表单数据过多的话,参数列表将会变得非常长,最好的解决方案是将表单中的 ...
- Vue表单绑定(单选按钮,选择框(单选时,多选时,用 v-for 渲染的动态选项)
<!DOCTYPE html><html> <head> <meta charset="utf-8"> ...
- vue -- v-model 表单绑定
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
随机推荐
- 01 | 堆、栈、RAII:C++里该如何管理资源?(极客时间笔记)
基本概念 堆,英文是 heap,在内存管理的语境下,指的是动态分配内存的区域.这个堆跟数据结构里的堆不是一回事.这里的内存,被分配之后需要手工释放,否则,就会造成内存泄漏. C++ 标准里一个相关概念 ...
- C语言求最大公约数最小公倍数(多种方法)
前言 这个求解方式多样化,灵活变动,但是,网上没有很好的资源和很全的代码,特此练习,敲打后,总结成本片文章. 单一求解 一.最大公约数 1.穷举法(最简单求解方式) 利用除法方式用当前的数字不断去除以 ...
- linux修改中文字符集
//修改系统配置 cd /etc/profile //末尾加如下代码 export LC_ALL="zh_CN.GBK"export LANG="zh_CN.GBK&qu ...
- Lumia一键刷稳定版 Win10 arm 及其报错处理
前言 之前我发了一篇Lumia1520 刷Win10 arm双系统的文章,不过后来发现那个方法对小白来说太不友好,且系统也不稳定,所以我找到了更好的方法 刷机 我们可以利用刷机迷进行刷机,支持一键刷机 ...
- 如丝般顺滑:DDD再实践之类目树管理
在上次反思DDD实践之后,在类目树管理项目中再次实践DDD.从需求分析到建模和具体的落地,结合个人体会,都是干货.
- 创建第一个c程序
创建,组织,生成 ,生成. 1.我们先创建一个win32项目. 文件->新建->项目->Visual C++ ->Win32 输入项目名称 选择项目保存位置 很重要的一 ...
- 想法子记忆Vi/Vim常用操作及指令
本笔记有特殊目录,点击开启: 专有目录 在Linux系统中编辑文本总是离不开一位老帮手--Vi.而因为其诞生的年代有些久远,有些操作在现在看来可能有点"反直觉". 于是我决定写这样 ...
- React ant table 用 XLSX 导出excel文件
近期做了一个react ant design 的table转换成excel 的功能 总结下 首先我们会自己定义下 antdesign 的table的columns其中有可能有多语言或者是render方 ...
- .NET桌面程序集成Web网页开发的多种解决方案
系列目录 [已更新最新开发文章,点击查看详细] B/S架构的Web程序几乎占据了应用软件的绝大多数市场,但是C/S架构的WinForm.WPF客户端程序依然具有很实用的价值,如设计类软件 Au ...
- Oauth的学习以及开发自助上课签到脚本
附上源码: https://github.com/taka250/auto_checkin_skl_hdu 首先了解学习oauth的知识 ...