通常限制一个input仅能输入指定字符我们会去监听其input事件,然后通过正则过滤掉非法字符。然而在vue中,仅仅修改input的value并不会同步到v-model上。在vue官方文档中有提到v-model其实是v-bind和v-on合起来的语法糖,如下:
<input v-bind:value="value" v-on:input="$emit('input', $event.target.value)">
由此可以看出v-model的更新其实是基于监听input事件,但由于我们直接修改input的value并不会触发input事件,所以我们只须手工去触发一下这个事件即可。按照此逻辑可以先写出一个基础版的指令,如下:
Vue.directive('inputInt', {
bind(el, binding, vnode) {
let input = vnode.elm;
input.addEventListener('input', () => {
let oldValue = input.value;
let newValue = input.value.replace(/[^\d]/g, '');
// 判断是否需要更新,避免进入死循环
if(newValue !== oldValue) {
input.value = newValue
input.dispatchEvent(new Event('input')) // 通知v-model更新
}
})
}
})
需要注意的是,我们通过手工触发input事件会再次走到input事件监听中去,如此就成了死循环,所以此处需要判断是否需要更新v-model,进而确定是否需要手工去触发input事件。
以上代码看上去是ok的,但实际使用时会遇到一个很奇怪的现象:当用中文输入法时,尝试输入中文的字符确实会被过滤掉,但v-model并没有同步,再输入数字时又正常了(在element下若输入中文, v-model将永久不会再同步更新)。这个现象暴露出一个很明显的问题,当我们尝试输入中文时,每敲一个字母就会触发一次input事件,而我们期望的是在确认输入的时候才去校验。幸运的是浏览器提供了一组事件去处理这样的情况compositionstart 、compositionend 。
MDN释义如下:
compositionstart 事件触发于一段文字的输入之前(类似于 keydown 事件,但是该事件仅在若干可见字符的输入之前,而这些可见字符的输入可能需要一连串的键盘操作、语音识别或者点击输入法的备选词)。
当文本段落的组成完成或取消时, compositionend 事件将被触发 (具有特殊字符的触发, 需要一系列键和其他输入, 如语音识别或移动中的字词建议)。
我们可以借助这组事件去限制input事件的执行,忽略掉composition内的input事件。
input.addEventListener('compositionstart', () => {
vnode.inputLocking = true;
});
input.addEventListener('compositionend', () => {
vnode.inputLocking = false;
input.dispatchEvent(new Event('input'));
});
input.addEventListener('input', () => {
if (vnode.inputLocking) {
return;
}
// ...
}
进一步思考一下,为了change事件能够同步获取到最终的值,我们在手工触发input后需要同时出发一个cahnge事件,加上input默认的change事件,一次输入可能会触发两次change事件(一个是用户输入的值,一个是修正后的值),而第一次change事件回传的值是未经处理的值,可能是不合法的,我们希望能去掉这种非法输入带来的代码负担,所以我们需要将默认的change事件屏蔽掉,统一由指令内部统一去触发。此时,我们可以在事件捕获阶段去掉默认的change事件。
一个完整的限制整数输入指令如下:
Vue.directive('inputInt', {
bind(el, binding, vnode) {
let input = vnode.elm;
if (input.tagName !== 'INPUT') {
input = input.querySelector('input');
}
if (!input) return;
input.addEventListener('compositionstart', () => {
vnode.inputLocking = true;
});
input.addEventListener('compositionend', () => {
vnode.inputLocking = false;
input.dispatchEvent(new Event('input'));
});
input.addEventListener(
'input',
e => {
e.preventDefault(); // 阻止掉默认的change事件
if (vnode.inputLocking) {
return;
}
let oldValue = input.value;
let newValue = input.value.replace(/[^\d]/g, '');
if (newValue) {
switch (binding.value) {
case 'zeroBefore':
break; // 数字随意输,不做处理,如 000013
case 'zeroCan':
newValue = Number(newValue).toString(); // 去掉开头0 正整数 + 0
break;
default:
newValue = newValue.replace(/^\b(0+)/gi, ''); // (默认)去掉开头0 正整数
}
}
// 判断是否需要更新,避免进入死循环
if (newValue !== oldValue) {
input.value = newValue;
input.dispatchEvent(new Event('input')); // 通知v-model更新 vue底层双向绑定实现的原理是基于监听input事件
input.dispatchEvent(new Event('change')); // 手动触发change事件
}
},
true, // 在捕获阶段处理,目的是赶在change事件之前阻止change事件(非法输入在触发指令之前先触发了change,需要干掉)
);
},
});
至此,以上问题都以解决。当我们想要对更多情况做控制时,只需更改value的值即可。
扩展一个可以动态指定浮点数位数的浮点数指令
Vue.directive('inputFloat', {
bind(el, binding, vnode) {
let input = vnode.elm;
if (input.tagName !== 'INPUT') {
input = input.querySelector('input');
}
if (!input) return;
input.addEventListener('compositionstart', () => {
vnode.inputLocking = true;
});
input.addEventListener('compositionend', () => {
vnode.inputLocking = false;
input.dispatchEvent(new Event('input'));
});
input.addEventListener(
'input',
e => {
e.preventDefault(); // 阻止掉默认的change事件
if (vnode.inputLocking) {
return;
}
let oldValue = input.value;
let newValue = input.value;
newValue = newValue.replace(/[^\d.]/g, '');
newValue = newValue.replace(/^\./g, '');
newValue = newValue
.replace('.', '$#$')
.replace(/\./g, '')
.replace('$#$', '.');
const decimal = Number(binding.value) || 2; // 默认两位小数
const reg = new RegExp(`^(\\-)*(\\d+)\\.(\\d{${decimal}}).*$`);
newValue = newValue.replace(reg, '$1$2.$3');
if (newValue) {
let arr = newValue.split('.');
newValue = Number(arr[0]) + (arr[1] === undefined ? '' : '.' + arr[1]); // 去掉开头多余的0
}
// 判断是否需要更新,避免进入死循环
if (newValue !== oldValue) {
input.value = newValue;
input.dispatchEvent(new Event('input')); // 通知v-model更新
}
},
true,
);
// input 事件无法处理小数点后面全是零的情况 因为无法确定用户输入的0是否真的应该清除,如3.02。放在blur中去处理
input.addEventListener('blur', () => {
let oldValue = input.value;
let newValue = input.value;
if (newValue) {
newValue = Number(newValue).toString();
}
// 判断是否需要更新,避免进入死循环
if (newValue !== oldValue) {
input.value = newValue;
input.dispatchEvent(new Event('input')); // 通知v-model更新
}
});
},
});
扩展到其他UI框架
其实扩展到其他框架很简单,整个指令对元素的依赖仅仅表现在节点的获取上,在不同的框架下更改获取input节点相应的代码即可。如在element下获取节点的代码为:
let input = vnode.elm.children[0];
在线预览 以上,tks~