基于 ElementUI 封装的 NumberInput
最近在做项目的时候遇到 input
只能输入整数, 不能输入小数的需求. 想到以前同事封装过这个组件, 可以拿来一用. 结果发现组件只支持正整数, 不支持负数, 看来还需要修改一下了
国际惯例: 源码 (opens new window)奉上
# 同事封装的组件
<template>
<el-input v-model="model" v-bind="$attrs" @input="_input" v-on="listeners">
<slot v-for="(value, key) in $slots" :name="key" :slot="key"></slot>
</el-input>
</template>
2
3
4
5
<script>
export default {
inheritAttrs: false,
name: 'NumberInput',
props: {
value: [Number, String],
type: {
// integer 整数, decimal 小数
type: String,
default: 'decimal'
},
places: {
//保留小数位数
type: [Number, String],
default: 2 //默认两位小数
},
Event: {
type: String,
default: 'input'
}
},
data() {
return {
model: this.value
}
},
watch: {
value() {
this.model = this.value
}
},
methods: {
_input(val) {
const numReg = /^[0-9]$/
let v
if (this.type === 'integer') {
// 验证 整数
v = val.replace(/./g, function(e) {
if (numReg.test(parseInt(e))) {
return e
} else {
return ''
}
})
this.$nextTick(() => {
this.model = v
this.$emit('input', v)
})
return
}
if (this.type === 'decimal') {
// 验证 只留小数点和数字
v = val.replace(/./g, function(e) {
if (e === '.' || numReg.test(parseInt(e))) {
return e
} else {
return ''
}
})
}
//小数位数
if (v.indexOf('.') > -1) {
let l, r
// 阻止输入两个小数点
if (v.split('.').length > 2) {
v = v.slice(0, v.length - 1)
}
// 截取小数点
l = v.split('.')[0]
r = v.split('.')[1] || ''
if (r.length > +this.places) {
r = r.slice(0, +this.places)
}
v = l + '.' + r
}
this.$nextTick(() => {
this.model = v
this.$emit('input', v)
})
}
},
computed: {
listeners() {
return Object.assign({}, this.$listeners, { [this.Event]: this._input })
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# 分析组件
首先 html
部分没啥好说的, 都是基本操作, 这里就不再赘述了
js
部分主要看一下 _input
中的代码:
首先定义了一个验证数字的正则, 其实 /^\d+$/
就可以了.
先看验证整数的逻辑:
if (this.type === 'integer') {
// 验证 整数
// 依次替换 val 中的每个值
v = val.replace(/./g, function(e) {
// 如果是数字, 就返回本身
if (numReg.test(parseInt(e))) {
return e
// 否则返回空
} else {
return ''
}
})
this.$nextTick(() => {
// 修改绑定值
this.model = v
// emit 值
this.$emit('input', v)
})
return
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这里一切正常, 就是无法判断负整数, 所以我们需要修改一下正则, 并且还需要增加一个参数来判断是正数还是负数
再看验证小数的逻辑:
if (this.type === 'decimal') {
// 验证 只留小数点和数字
// 依次替换 val 中的每个值
v = val.replace(/./g, function(e) {
// 如果是小数点和数字, 就返回本身
// 所以只输入一个小数点, 而不输入数字也会验证通过的
// 注: 这里会返回多个小数点
if (e === '.' || numReg.test(parseInt(e))) {
return e
} else {
return ''
}
})
}
//小数位数
// 判断是否有小数点
if (v.indexOf('.') > -1) {
let l, r
// 阻止输入两个小数点
if (v.split('.').length > 2) {
// 如果输入两个小数点, 就截断最后一个字符
// 注: 这里有一个问题
// 我们正常输入都是从左到右, 当输入第二个小数点后, 截断第二个是正常的
// 但用户有可能会移动光标到第一个小数点前面输入小数点
// 这样截断最后一个字符其实是数字
// 而且现在有两个小数点了
v = v.slice(0, v.length - 1)
}
// 截取小数点
// 用小数点分隔, 判断小数位数是否超过预定值
// 其实这里变相的处理了两个小数点的问题
// 因为分隔后, 其实数组中有 3 个值, 这里只取了前两个
// 那必然会导致数据丢失
l = v.split('.')[0]
r = v.split('.')[1] || ''
if (r.length > +this.places) {
r = r.slice(0, +this.places)
}
v = l + '.' + r
}
this.$nextTick(() => {
this.model = v
this.$emit('input', v)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
小数验证一下就出来这么多问题了:
- 小数点后没有数字也会验证通过
- 在第一个小数点前面再输入一个小数点会导致数据丢失
关于数据丢失的例子:
输入 | 处理 | 输出 |
---|---|---|
123.45 | 验证通过 | 123.45 |
12.3.45 | 进入阻止输入两个小数点的逻辑, 截断最后一个字符 5 | 12.3.4 |
继续进入截取小数点的逻辑, 只取数组前两位, 并验证小数位数是否超过预定值 | 12.3 |
# 改造组件
# 增加一个参数, 来判断正负数
props: {
type: {
// integer 整数, decimal 小数
type: String,
default: 'decimal'
},
signed: {
// plus 正数, minus: 负数, all: 全部
type: String,
default: 'plus'
},
places: {
// 保留小数位数
type: [Number, String],
default: 2 // 默认两位小数
}
},
computed: {
regs() {
return {
// 注:
// 如果是负数的话, 用户首先会输入 -, 这个要验证通过, 所以数字后面的量词为 *
// 这也就有一个问题了, 用户只输入一个 - 就提交, 也能验证通过, 目前暂时没有好办法处理
'integer-plus': /^\d+$/,
'integer-minus': /^-\d*$/,
'integer-all': /^-?\d*$/,
// 这里是在正则中使用变量的方式
// \ 要写两次
'decimal-plus': new RegExp(`^\\d+(\\.\\d{0,${this.places}})?$`),
'decimal-minus': new RegExp(`^-(\\d*|\\d+(\\.\\d{0,${this.places}})?)$`),
'decimal-all': new RegExp(`^-?(\\d*|\\d+(\\.\\d{0,${this.places}})?)$`)
}
},
regName() {
return `${this.type}-${this.signed}`
},
reg() {
return this.regs[this.regName]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 验证输入
思路: 感觉同事验证整数和小数的逻辑太麻烦, 其实可以根据不同情况生成不同正则, 如果验证通过就返回, 否则就截断最后一个字符再返回. 但这样也犯了同事处理阻止输入两个小数点的问题: 如果在字符串中间输入非法字符, 会截断最后一个合法字符, 而留下这个非法字符.
其实, 如果本次输入验证不通过, 那我就返回上一次输入的. 这就需要记录本次输入和上次输入的值, 需要请出 watch
大神了, 并且需要一个变量记录当前的输入值
data() {
return {
model: this.value,
temporary: this.value // 这里给临时变量赋值为 value 初始值, 防止用户首次输入非法字符后, old 为空, 清空输入
}
},
watch: {
value(n) {
this.temporary = n
},
temporary(n, o) {
// 注:
// n 有值才判断, 防止删除的时候无法删除最后一个字符
if (n.length && !this.reg.test(n)) {
// 如果验证未通过
// 当前输入赋值为上一次的合法输入
n = o
// 临时变量也赋值为上一次
this.temporary = o
// 由于这里临时变量改变了, 会再进入 watch, 并且会验证通过, 所以这里不需要 赋值和 emit 操作
return
}
this.model = n
this.$emit('input', n)
}
},
methods: {
_input(val) {
this.temporary = val
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
至此就算正式改造完成了
# 问题
# -
或 .
问题(未解决)
可以输入 -
的时候, 只输入一个 -
也可以验证通过
可以输入 .
的时候, .
后面没有数字也可以验证通过
这两种情况暂时没有好的解决方案.
# 初始值为不合法字符串(已解决)
如果初始值就是一个不合法的字符串, 会进入无限循环中
可以在 created
对 value
判断一下, 如果不合法就给 temporary
和 model
赋值为空.
if (!this.reg.test(this.value)) {
// 这里要同时修改这两个值, 防止 temporary 的 n 和 o 与实际不符
this.model = ''
this.temporary = ''
}
2
3
4
5
这种虽然会改变输入值, 但你传入的就是一个非法值, 总比陷入无限循环而卡死页面强吧
# 默认值为 undefined 会造成页面卡顿甚至卡死(已解决)
当在表格中大量使用该组件并且默认值为 undefined
时, 渲染需要很长时间, 甚至卡死浏览器
如图:
这个表格默认查最近 4
天的数据, 由于是动态表格, 所以由后端返回, prop
是当日的时间戳.
表头数据:
data: [
{
children: [
{
prop: '1588953600000',
label: '09日',
},
{
prop: '1589040000000',
label: '10日',
},
{
prop: '1589126400000',
label: '11日',
},
{
prop: '1589212800000',
label: '12日',
},
],
prop: '1589262808249',
label: '2020年05月',
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
表格数据:
后端目前的做法是当日如果有数据就返回当日 key
和 value
, 否则就没有当日这个字段
data: [
{
id: '1611934516379702',
'1588953600000': 10,
name: '11'
},
{
id: '1611934516379703',
name: '22'
},
]
2
3
4
5
6
7
8
9
10
11
第一条 09
日有数据, 就返回了, 其余日都没数据, 就没有返回; 第二条都没有数据, 所以都没有返回
这样取 10 - 12
日的数据就是 undefined
, 当页面中有大约 150
个该组件并且都是 undefined
的时候, 渲染就非常卡顿了, 数据量再大就会卡死页面了
为什么发现是 undefined
引起的呢? 因为还有一个类似的页面并不卡顿, 差别就在那个页面后端默认返回的是 0
, 而不是 undefined
.
一开始想的是让后端也默认返回 0
, 但以后出现类似情况都需要后端处理, 作为一个成熟的组件, 咱要从自身解决这个问题
说说我的看法: 当前的问题是 undefined
引起的, 为什么会这样呢? 其实是因为咱们接收 value
值规定了只接收 [Number, String]
, 但传入的是 undefined
, 但 vue
却没有报类型错误, 应该是内部对这种情况做了处理, 导致占用了大量资源, 导致渲染卡顿. 所以咱们只要给一个类型是 Number
或 String
的默认值, 问题就解决了
props: {
value: {
type: [Number, String],
default: '' // '' 或者 0 看需求吧
}
}
2
3
4
5
6
# 传入的 value 为 Number 类型时无法验证(已解决)
复现路径: 不通过输入框修改 value
, 而是通过代码将 value
值改变为 Number
类型
以前都是通过输入框来验证组件是否可以控制输入, 这种方式会将 Number
转化为 String
, 导致一直没有测出这个 bug
由于 Number
没有 length
属性, 并且 RegExp
只能验证字符串(验证数值有问题), 所以 n.length && !this.reg.test(n)
这个判断就进不去了, 从而导致无法验证
所以我们需要先转化为 String
再验证
watch: {
temporary(n, o) {
n = n + ''
// 判断 n.length 和 n 是一样的效果, 这里就判断 n 了
if (n && !this.reg.test(n)) {
n = o
this.temporary = o
return
}
this.model = n
this.$emit('input', n)
}
},
created() {
const val = this.value + ''
if (val && !this.reg.test(val)) {
// 这里 temporary 改变后会触发 watch, 那里会给 model 赋值, 所以这里不用赋值了
this.temporary = ''
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 扩展
既然现在是通过传入参数来生成正则表达式, 那何不封装一个接收正则表达式的组件, 这样不就可以让使用者自定义 input
值了吗?
RegInput.vue
<template>
<el-input v-model="model" v-bind="$attrs" @input="_input" v-on="$listeners">
<slot v-for="(value, key) in $slots" :name="key" :slot="key"></slot>
</el-input>
</template>
2
3
4
5
<script>
export default {
inheritAttrs: false,
name: 'RegInput',
props: {
value: {
type: [Number, String],
default: ''
},
reg: {
type: RegExp,
default: /./
}
},
data() {
return {
model: this.value,
temporary: this.value
}
},
watch: {
value(n) {
this.temporary = n
},
temporary(n, o) {
n = n + ''
if (this.test(n)) {
n = o
this.temporary = o
return
}
this.model = n
this.$emit('input', n)
}
},
methods: {
_input(val) {
this.temporary = val
},
test(val) {
return val && !this.reg.test(val)
}
},
created() {
const val = this.value + ''
if (this.test(val)) {
this.temporary = ''
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
验证发现中文(/^[\u4E00-\u9FA5]*$/
)和数字(/^-?(\d*|\d+(\.\d*)?)$/
)可以正常输入, 但手机号(/^1[3456789]\d{9}$/
)等却无法输入
原因是因为这一类有最少位数, 而我们输入只能一个一个输入, 第一次输入一个, 验证不通过, 返回上一个结果(空值), 如此反复, 永远也无法输入了
对于这一类正则, 目前想到两种方法:
- 不让用户输入, 只能粘贴.
- 修改正则表达式, 一位也需要验证通过.
两种办法其实都不算好办法, 但目前没有找到合适的解决方案, 等以后找到了再说吧