在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>