angular 响应式表单 input限制输入

前面通过设定FromGroupupdateOn配置,将表单的验证时机修改为了当输入框输入焦点之后,这个操作引发的问题是,输入限制不起作用了!比如有些地方我只需要输入数字,不可以输入其他字符,做这个输入限制是靠在验证器里进行replace,现在是当鼠标遗失后才去验证,也就是鼠标遗失后采取replace,很糟心啊,得想想解决办法。

既然不能用验证器限制输入,那么我们使用input方法来进行监听和替换怎么样?看方法:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 输入限制
* @param $event
*/
onInput($event) {
if (!$event) {
return;
}
const target = $event.target;
const regexp = /[^0-9]/ig;
target.value = target.value.replace(regexp, '');
}

这个方法通过使用正则表达式,将不是数字的字符都替换掉,这样应该可以满足我们的限制输入。
应用在表单上:

1
2
3
4
5
6
<input nz-input
name="code"
id="code"
(input)="onInput($event)"
formControlName="code"
>

然后运行测试,发现这种情况输入是可以限制的,但是,尽管页面上显示的是replace后的,FormControl拿到的还是格式之前的,也就是假如我输入一串数字,再输入一串字母,表单拿到的值是数字+最后一个字母,而并不是只有数字。

而且,当我切换到中文输入法的时候,更会发生错乱。为什么呢?因为输入汉字的时候,有个中间状态,也就是我不断输入并选词的状态,这个时候虽然界面上是在选词,但是input事件能监听到,还会替换掉,所以页面上就乱套了。需要避免这种情况。

解决中文输入的问题

需要解决中文输入的问题,那就要在用户继续输入的时候,不要进行替换,在输入完成的时候进行替换。这有个中间状态。

根据W3C的规范,在我们开始输入法输入词组的时候,浏览器会触发一个compositionstart事件,在输入法结束的时候,浏览器会触发compositionend事件,我们只需要维护一个变量,通过监听状态来改变这个状态,就可以知道什么时候输入完成。看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 输入完成的标记
isComposite: boolean = false;
onInput($event) {
if (!$event || this.isComposite) {
return;
}
const target = $event.target;
const regexp = /[^0-9]/ig;
target.value = target.value.replace(regexp, '');
}
onCompositionStart() {
this.isComposite = true;
}
onCompositionEnd($event) {
this.isComposite = false;
this.onInput($event);
}

相应的html:

1
2
3
4
5
6
7
8
<input nz-input
name="code"
id="code"
(compositionstart)="onCompositionStart()"
(compositionend)="onCompositionEnd($event)"
(input)="onInput($event)"
formControlName="code"
>

这样我们可以正常输入中文,并在中文输入完成后完成对文字的replace。

目前为止,表面上的输入限制是做好的,但是表单拿到的值还是有杂质的。需要进一步处理值。

处理表单值

现在当我们输入数字+其他字符的时候,使输入框遗失焦点,发现表单得到的值总是有最后一个字母保留,输入中文的话后续的全部保留。很纳闷。我们需要去掉这个尾巴。

现在我们的表单改成了输入框遗失焦点后才验证,那么我们能不能在输入框遗失焦点后对值再手动更新一下呢?

1
2
3
4
onBlur($event) {
this.onInput($event);
this.formGroup.controls['code'].setValue($event.target.value);
}

在html中加入blur:

1
2
3
4
5
6
7
8
9
<input nz-input
name="code"
id="code"
(compositionstart)="onCompositionStart()"
(compositionend)="onCompositionEnd($event)"
(input)="onInput($event)"
(blur)="onBlur($event)"
formControlName="code"
>

在页面中测试,发现确实是能用。至少,目前是满足使用的。

但是,我们几乎每个表单都要这样改,四个方法,页面上进行配置,主要是还公用一个变量isComposite,这样不仅很累,而且很容易出问题。我们需要出去为一个指令。

限制输入-指令

上面各种方法移入指令中倒也不是怎么麻烦,麻烦的是如何在指令中去修改FormGroup里面FormControl的值?

需要祭出NgControl这个类了,NgControl是一个基类,所有的控制指令都继承于它。它可以给一个Dom元素绑定一个FormControl对象。在Angular表单内部使用。还是想啥来啥,注入了这个NgControl,我们就有了在指令里面操作FormControl的能力了。

来看看NgControl,它继承了一个AbstractControlDirective,这个才是真正具有FormControl的源头。

直接来看指令:

1
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
89
90
91
92
93
94
95
96
97
98
99
100
101
import {Directive, ElementRef, HostListener, Input} from '@angular/core';
import {NgControl} from '@angular/forms';

@Directive({
selector: '[appLimitInput]'
})
export class LimitInputDirective {
/**
* 可以是字符串或自定义的正则表达式
* -- 字符串的时候就找内置的一些正则表达式
* -- 正则表达式的时候直接使用
*/
@Input() replace: string|RegExp;

// 限制输入的正则表达式
_regexp: RegExp;

isComposite: boolean = false;

/**
* 对复杂的业务传入方法
*/
@Input() replaceFn: Function;

constructor(
private el: ElementRef,
private control: NgControl,
) {
}

/**
* 选词输入开始
*/
@HostListener('compositionstart') onCompositionStart() {
this.isComposite = true;
}

/**
* 选词输入结束(确定输入或取消输入)
*/
@HostListener('compositionend', ['$event']) onCompositionEnd($event) {
this.isComposite = false;
this.limitInput($event);

}

@HostListener('change') onChange() {
this.control.control.setValue(this.el.nativeElement.value);
}

/**
* 应对输入被格式化导致不接发change事件的问题。所以这里在blur的时候也进行重新赋值
* @param $event
*/
@HostListener('blur', ['$event']) onBlur($event) {
this.control.control.setValue(this.el.nativeElement.value);
}

/**
* 正常输入
*/
@HostListener('input', ['$event']) onInput($event) {
this.limitInput($event);
}

private limitInput($event) {
if (!this.el.nativeElement.value || this.isComposite) {
return;
}
if (this.replaceFn) {
this.replaceFn(this.el);
return;
} else {
this._regexp = this.getRegexp(this.replace);
this.el.nativeElement.value = this.el.nativeElement.value.replace(this._regexp, '');
}

}

private getRegexp(str) {
if (typeof str === 'object') {
return str;
}
let regexp;
switch (str) {
case 'en':
regexp = /(^\s|[\u4e00-\u9fa5])/g;
break;
case 'number':
regexp = /[^0-9]/ig;
break;
case 'number|letter':
regexp = /[^\d|\w]/ig;
break;
case 'float':
this.replace = /[^\d|\.]/ig;
break;
}
return regexp;
}
}

在使用的时候,可以传入内置的正则替换的名字,也可以直接传入自己写的正则表达式,如果不满意还可以传入自己写的替换方法,满足各种使用。

使用的例子:

  • 使用内置正则替换:
1
2
3
4
5
6
 <input nz-input
name="code"
formControlName="code" id="code"
appLimitInput
[replace]="'number'"
>
  • 使用自定义正则表达式:
1
regexp: RegExp = /[^\d|\.]/ig;
1
2
3
4
5
6
<input nz-input
name="code"
formControlName="code" id="code"
appLimitInput
[replace]="regexp"
>
  • 使用自定义方法:
1
2
3
4
5
6
7
// 限制输入不超过八位小数
replaceFn = (el: ElementRef) => {
console.log('replaceFn');
const regexp = /\d*\.?\d{0,8}/g;
const match = el.nativeElement.value.match(regexp);
el.nativeElement.value = match ? match[0] : '';
}
1
2
3
4
5
6
<input nz-input
name="code"
formControlName="code" id="code"
appLimitInput
[replaceFn]="replaceFn"
>

整理成指令后,修改成本降低了,代码也变得可扩展,业务复杂也不惧。