在Vue中的项目,基于VUX-UI开发,一个常见的需求:
1、金额输入框 2、弹出数字键盘 3、仅支持输入两位小数,限制最大11位数,不允许0开头
后续:与UI沟通后, 思路调整为限制输入,并减少正则替换输入值出现的闪动。后续改动如下,注意点如下:
1、处理思路
A。在用户输入的键盘事件中,对于不符合的输入,阻止默认行为和事件冒泡。
不符合输入的规则如下:
1)当前输入框中的长度大于等于配置的max
2)非数字和小数点
3)当前输入框中已存在小数点,或第一位输入小数点
B。在获取值后,对于不符合两位小数的值,用watch正则替换后,再下一次渲染(会出现先12.000到12.00的闪动)
2、阻止键盘事件在哪个阶段?
keypress。
因为keydown和keyup得到的是keyEvent中键值是原始的组合键值,需要判断不同环境和浏览器对keycode的实现不同以及是否有shift/alt等。比如在IOS中keydown,对于字符$ @,keycode都是0;中文键盘和英文键盘中的数字keycode不一致。
而kepress得到的是组合解析后的实际值,android和ios大部分表现一致。
3、Android的数字键盘中的小数点的特殊处理
调试发现,安卓的数字键盘中,小数点做了特殊处理:
1)无法捕获到keypress事件
2)keydown事件中keEvent的keycode是0,无法用于判断
3)keydown事件中keEvent的keyIdentifier === 'U+0000'
4)在keydown事件以及keyuup或其它事件中, 用preventDefault和stopPropagation阻止默认行为和事件冒泡,不能阻止input框输入小数点.
所以对这个问题处理,只能沿用之前用在watch中处理空值问题的思路。
4、最终效果
IOS中默认拉起含特殊字符的数字键盘,对于非法输入不会出现任何闪动,对于长度越界的会出现闪动
Andriod中默认拉起九宫格数字键盘,没有特殊字符,小数点会出现闪动,对于长度越界的会出现闪动
<template> <XInput :title="title" :max="currentMax" :min="currentMin" :type="type" v-model="currentValue" @on-focus="onFoucus()" @on-blur="onBlur()" :show-clear="showClear" :placeholder="placeholder" ref="xinput"> <template v-if="$slots.label" slot="label"><slot name="label"></slot></template> <template v-if="$slots.right" slot="right"><slot name="right"></slot></template> </XInput> </template> <script> export default { data() { return { currentValue: this.value, }; }, computed: { currentMax() { return (this.type === 'number') ? undefined : this.max; }, currentMin() { return (this.type === 'number') ? undefined : this.min; } }, props: { title: String, max: Number, min: Number, type: String, showClear: { type: Boolean, default: true, }, placeholder: String, value: [String, Number], filter: { type: Function, default: (value) => { let formattedValue = ''; const match = value.match(/^([1-9]\d*(\.[\d]{0,2})?|0(\.[\d]{0,2})?)[\d.]*/); if (match) { formattedValue = match[1]; } return formattedValue; }, } }, watch: { currentValue(val, oldVal) { // 调用filter过滤数据 let formattedValue = this.filter(val); if (this.type === 'number') { formattedValue = this.typeNumberFilter(formattedValue, oldVal); } if (val !== formattedValue || val === '') { setTimeout(() => { this.currentValue = formattedValue; }, 0); } this.$emit('input', formattedValue); }, value(value) { this.currentValue = value; }, }, methods: { blur() { this.$refs.xinput.blur(); }, focus() { this.$refs.xinput.focus(); }, onFoucus() { this.$emit('on-focus'); }, onBlur() { this.$emit('on-blur'); }, typeNumberFilter(val, oldVal) { const inputEle = this.$refs.xinput.$refs.input; let formattedValue = val; // TODO: 待大范围验证:Android处理连续输入..后,type=number的input框会把值修改为'',这里手动替换为上次的currentValue // 问题描述: 1.00. 不会触发值改变,1.00.不会触发值改变,1.00.【\d\.】都会把值修改为空字符串''。hack处理的条件说明如下: // 1、输入框拿到的是空值(因input=number导致输入框立即被赋予空值。点击清除按钮时,这里input输入框还是上次的值) // 2、上次输入值有效 if (inputEle.value === '' && oldVal && oldVal.match(/^(\d)[\d.]+/)) { formattedValue = oldVal; } return formattedValue; }, isBackspace(keyCode) { return keyCode === 8; }, isDot(keyCode) { return keyCode === 46 || keyCode === 190; }, isNumber(keyCode) { return (keyCode >= 48 && keyCode <= 57); }, isNotNumberKeycode(keyCode) { return !this.isBackspace(keyCode) && !this.isDot(keyCode) && !this.isNumber(keyCode); }, isDotStart(keyCode, inputVal) { return this.isDot(keyCode) && (!inputVal || inputVal === '' || /\./.test(inputVal)); }, isFinalInput(inputVal) { return inputVal.length >= this.max; } }, mounted() { if (this.type === 'number') { const inputEle = this.$refs.xinput.$refs.input; inputEle.onkeydown = (e) => { // Android小数点特殊处理 const inputVal = inputEle.value; if (e.keyIdentifier === 'U+0000' && (!inputVal || inputVal === '')) { inputEle.value = ''; } }; // eslint-disable-next-line inputEle.onkeypress = (e) => { const keyCode = e.keyCode; const inputVal = inputEle.value; if (this.isNotNumberKeycode(keyCode) || this.isDotStart(keyCode, inputVal) || this.isFinalInput(inputVal)) { e.preventDefault(); e.stopPropagation(); return false; } }; } } }; </script>
第一,首先想到额就是在VUX-UI中制定type=number。--不可行
VUX中的文档和代码说明,type=number不支持maxLength,会报错,而且没有正则替换的处理或者钩子函数,只有输入后提示校验信息。
第二,基于VUX中XInput封装,有如下问题
1)两层v-model,正则替换的值不会触发input框渲染
解决:currentValue赋值为foramttedValue,放入setTimeout(func ,0)中,让input框先渲染为正则替换前的值,再渲染为替换后的值
currentValue(val, oldVal) { // 调用filter过滤数据 let formattedValue = this.filter(val); if (this.type === 'number') { formattedValue = this.typeNumberFilter(formattedValue, oldVal); } if (val !== formattedValue || val === '') { setTimeout(() => { this.currentValue = formattedValue; }, 0); } this.$emit('input', formattedValue); },
2)数字键盘input type=number,会导致maxlength失效,无法限制长度
解决:用slice(0, max)处理
if (formattedValue.length > this.max) { formattedValue = formattedValue.slice(0, this.max); }
3)数字键盘input type=number ,连续输入小数点...导致实际值和显示值不一致
解决:用原生的 inputElement.value = oldValue处理
const inputEle = this.$children[0].$refs.input; // TODO: 待大范围验证:处理连续输入..后,type=number的input框会把值修改为''的问题;fastclick导致type=number报错 // 问题描述: 1.00. 不会触发值改变,1.00.不会触发值改变,1.00.【\d\.】都会把值修改为空字符串''。hack处理的条件说明如下: // 1、当校验后是空值,(因input=number,formattedValue为''表明 原始newVal也为'') // 2、输入框拿到的是空值(因input=number导致输入框立即被赋予空值。点击清除按钮时,这里input输入框还是上次的值) // 3、上次输入大于两位(避免最后一位无法删除的问题。最后一位删除时,oldVal.length === 1) if (formattedValue === '' && inputEle.value === '' && oldVal && oldVal.match(/^(\d)[\d.]+/)) { formattedValue = oldVal; } setTimeout(() => { inputEle.value = formattedValue; }, 0);
4)IOS中数字键盘有%$*等特殊字符
解决:用原生的 inputElement.onkeydown监听事件,非数字和退格和小数点直接return事件
mounted() { if (this.type === 'number') { const inputEle = this.$refs.xinput.$refs.input; // eslint-disable-next-line inputEle.onkeydown = (e) => { const keyCode = e.keyCode; if (!this.isBackspace(keyCode) && !this.isDot(keyCode) && !this.isNumber(keyCode)) { // 其他按键 e.preventDefault(); e.stopPropagation(); return false; } }; } }
第三,其他说明
为什么不用 type=tel?
type=tel在ios中没有小数点
第四,全部代码
<template> <XInput :title="title" :max="currentMax" :min="currentMin" :type="type" v-model="currentValue" @on-focus="onFoucus()" @on-blur="onBlur()" :show-clear="showClear" :placeholder="placeholder" ref="xinput"> <template v-if="$slots.label" slot="label"><slot name="label"></slot></template> <template v-if="$slots.right" slot="right"><slot name="right"></slot></template> </XInput> </template> <script> export default { data() { return { currentValue: this.value, }; }, computed: { currentMax() { return (this.type === 'number') ? undefined : this.max; }, currentMin() { return (this.type === 'number') ? undefined : this.min; } }, props: { title: String, max: Number, min: Number, type: String, showClear: { type: Boolean, default: true, }, placeholder: String, value: [String, Number], filter: { type: Function, default: (value) => { let formattedValue = ''; const match = value.match(/^([1-9]\d*(\.[\d]{0,2})?|0(\.[\d]{0,2})?)[\d.]*/); if (match) { formattedValue = match[1]; } return formattedValue; }, } }, watch: { currentValue(val, oldVal) { // 调用filter过滤数据 let formattedValue = this.filter(val); if (this.type === 'number') { formattedValue = this.typeNumberFilter(formattedValue, oldVal); } if (val !== formattedValue || val === '') { setTimeout(() => { this.currentValue = formattedValue; }, 0); } this.$emit('input', formattedValue); }, value(value) { this.currentValue = value; }, }, methods: { onFoucus() { this.$emit('on-focus'); }, onBlur() { this.$emit('on-blur'); }, typeNumberFilter(val, oldVal) { const inputEle = this.$refs.xinput.$refs.input; let formattedValue = val; // 由于type=number不支持maxLength,用slice模拟 if (formattedValue.length > this.max) { formattedValue = formattedValue.slice(0, this.max); } // TODO: 待大范围验证:处理连续输入..后,type=number的input框会把值修改为''的问题;fastclick导致type=number报错 // 问题描述: 1.00. 不会触发值改变,1.00.不会触发值改变,1.00.【\d\.】都会把值修改为空字符串''。hack处理的条件说明如下: // 1、当校验后是空值,(因input=number,formattedValue为''表明 原始newVal也为'') // 2、输入框拿到的是空值(因input=number导致输入框立即被赋予空值。点击清除按钮时,这里input输入框还是上次的值) // 3、上次输入大于两位(避免最后一位无法删除的问题。最后一位删除时,oldVal.length === 1) if (formattedValue === '' && inputEle.value === '' && oldVal && oldVal.match(/^(\d)[\d.]+/)) { formattedValue = oldVal; } setTimeout(() => { inputEle.value = formattedValue; }, 0); return formattedValue; }, isBackspace(keyCode) { return keyCode === 8; }, isDot(keyCode) { return keyCode === 46 || keyCode === 110 || keyCode === 190; }, isNumber(keyCode) { return (keyCode >= 48 && keyCode <= 57) || (keyCode >= 96 && keyCode <= 105); }, }, mounted() { if (this.type === 'number') { const inputEle = this.$refs.xinput.$refs.input; // eslint-disable-next-line inputEle.onkeydown = (e) => { const keyCode = e.keyCode; if (!this.isBackspace(keyCode) && !this.isDot(keyCode) && !this.isNumber(keyCode)) { // 其他按键 e.preventDefault(); e.stopPropagation(); return false; } }; } } }; </script>