手动实现一个输入限制指令
3 分钟阅读
手动实现一个输入限制指令
"输入框只能输入数字"是表单开发里的高频需求。最常见的做法是在 oninput 里用正则过滤非法字符,但这个方案在中文输入法下会翻车。本文分析问题产生的原因,并用一个 Vue 自定义指令给出可靠的实现。
1. 问题:oninput 方案在输入法下失效
最初的实现很朴素:监听 input 事件,把非数字字符替换掉。但在"只能输入数字"的输入框里用中文输入法打字时,出现了一个诡异的现象:输入框里显示的内容是对的,v-model 绑定的值却再也不更新了——之后不管输入什么,绑定的数据都拿不到正确的值。
原因在于我们手动修改 el.value 是"绕过事件系统"的:
- 输入法组合(composition)期间,浏览器对
input事件的触发有自己的节奏 - 我们在
oninput里直接改写el.value,这个赋值操作不会触发新的input事件 - Vue 的
v-model依赖input事件来同步数据,事件没有派发,数据层和视图层就此脱节
所以正确的做法是:过滤完值之后,手动把 input 事件补发出去,让 v-model 的同步机制照常工作。
2. 指令实现
把过滤逻辑封装成一个自定义指令,并通过修饰符支持浮点数场景:
javascript
function onInput(el, ele, binding, vnode) {
// 用闭包实现"只保留第一个小数点":
// 每次调用 flag 自增,只有第一次返回 '.',之后都返回空串
function regHandle() {
let flag = 0
return () => (flag++ === 0) ? '.' : ''
}
function handle() {
let val = ele.value
// modifiers 为修饰符对象,传入 .float 则按浮点数规则处理
if (binding.modifiers.float) {
// 只保留数字和小数点
val = val.replace(/[^\d.]/g, '')
// 不允许出现多个小数点,只保留第一个
val = val.replace(/\./g, regHandle())
// 不允许以小数点开头,自动补 0
val = val.replace(/^\./g, '0.')
if (binding.value) {
// 指令的值用于指定期望保留的小数位数,只允许正整数
let points = +binding.value
if (!isNaN(points)) {
if (!Number.isInteger(points) || points < 0) {
points = 0
}
const reg = new RegExp(`^(\\d+)\\.(\\d\{${points}}).*$`)
if (points === 0) {
// 保留 0 位小数:小数点及之后的内容全部去掉
val = val.replace(reg, '$1')
} else {
// 通过正则捕获保留小数点后指定位数
val = val.replace(reg, '$1.$2')
}
}
}
} else {
// 整数场景:只保留数字
val = ele.value.replace(/[^\d]/g, '')
}
ele.value = val
// 关键步骤:手动派发 input 事件,保证 v-model 的响应式同步
// 组件内 input 和原生 input 的事件机制不同,需要区分处理
if (vnode.componentInstance) {
vnode.componentInstance.$emit('input', ele.value)
} else {
vnode.elm.dispatchEvent(new CustomEvent('input', ele.value))
}
}
return handle
}
const numberLimit = {
bind(el, binding, vnode) {
// 指令可能直接挂在 input 上,也可能挂在包裹了 input 的组件上
const ele = el.tagName === 'INPUT' ? el : el.querySelector('input')
ele.addEventListener('input', onInput(el, ele, binding, vnode), false)
}
}3. 使用方式
html
<!-- 只能输入整数 -->
<input v-model="age" v-number-limit>
<!-- 只能输入数字,最多两位小数 -->
<input v-model="price" v-number-limit.float="2">4. 实现要点回顾
- 指令挂点的兼容:
bind里先判断el.tagName,兼容直接挂在<input>和挂在封装组件上两种用法 - 闭包保留第一个小数点:
regHandle每次replace都返回一个全新的计数函数,利用String.replace对每次匹配调用回调的特性,实现"只留第一个" - 手动补发事件是核心:没有这一步,
v-model就会和视图脱节——这也是整个方案区别于普通oninput过滤的关键
在这个基础上还可以扩展更多规则,例如最大值/最小值限制、只允许正数/负数、手机号分段格式化等,思路都是一致的:过滤值 → 写回视图 → 补发事件同步数据。
相关文章
前后篇
← NEWERVue3+webpack实现组件远程加载OLDER →聊一聊京东的面试(金融组&物流组) 评论