验证表单输入

在模板驱动表单中验证输入

为了往模板驱动表单中添加验证机制,你要添加一些验证属性,就像原生的 HTML 表单验证器一样。 Angular 会用指令来匹配这些具有验证功能的指令。

每当表单控件中的值发生变化时,Angular 就会进行验证,并生成一个验证错误的列表(对应着 INVALID 状态)或者 null(对应着 VALID 状态)。

你可以通过把 ngModel 导出成局部模板变量来查看该控件的状态。 比如下面这个例子就把 NgModel 导出成了一个名叫 name 的变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<input type="text" id="name" name="name" class="form-control"
required minlength="4" appForbiddenName="bob"
[(ngModel)]="hero.name" #name="ngModel">

<div *ngIf="name.invalid && (name.dirty || name.touched)"
class="alert">

<div *ngIf="name.errors?.['required']">
Name is required.
</div>
<div *ngIf="name.errors?.['minlength']">
Name must be at least 4 characters long.
</div>
<div *ngIf="name.errors?.['forbiddenName']">
Name cannot be Bob.
</div>

</div>

注意这个例子讲解的如下特性。

  • <input> 元素带有一些 HTML 验证属性:required 和 minlength。它还带有一个自定义的验证器指令 forbiddenName。欲知详情,参阅自定义验证器一节。

  • #name=”ngModel” 把 NgModel 导出成了一个名叫 name 的局部变量。NgModel 把自己控制的 FormControl 实例的属性映射出去,让你能在模板中检查控件的状态,比如 valid 和 dirty。要了解完整的控件属性,参阅 API 参考手册中的AbstractControl。

    • <div> 元素的 *ngIf 展示了一组嵌套的消息 div,但是只在有“name”错误和控制器为 dirty 或者 touched 时才出现。

    • 每个嵌套的 <div> 为其中一个可能出现的验证错误显示一条自定义消息。比如 required、minlength 和 forbiddenName。

为防止验证程序在用户有机会编辑表单之前就显示错误,你应该检查控件的 dirty 状态或 touched 状态。

  • 当用户在被监视的字段中修改该值时,控件就会被标记为 dirty(脏)
  • 当用户的表单控件失去焦点时,该控件就会被标记为 touched(已接触)

在响应式表单中验证输入

在响应式表单中,事实之源是其组件类。不应该通过模板上的属性来添加验证器,而应该在组件类中直接把验证器函数添加到表单控件模型上(FormControl)。然后,一旦控件发生了变化,Angular 就会调用这些函数。

验证器(Validator)函数

验证器函数可以是同步函数,也可以是异步函数。

验证器类型 详细信息
同步验证器 这些同步函数接受一个控件实例,然后返回一组验证错误或 null。可以在实例化一个 FormControl 时把它作为构造函数的第二个参数传进去。
异步验证器 这些异步函数接受一个控件实例并返回一个 Promise 或 Observable,它稍后会发出一组验证错误或 null。在实例化 FormControl 时,可以把它们作为第三个参数传入。

出于性能方面的考虑,只有在所有同步验证器都通过之后,Angular 才会运行异步验证器。当每一个异步验证器都执行完之后,才会设置这些验证错误。

内置验证器函数

在模板驱动表单中用作属性的那些内置验证器,比如 required 和 minlength,也都可以作为 Validators 类中的函数使用。

  • min(min:number) 此验证器要求控件的值大于或等于指定的数字。 它只有函数形式,没有指令形式。
  • max(max: number) 此验证器要求控件的值小于等于指定的数字。 它只有函数形式,没有指令形式。
  • required 此验证器要求控件具有非空值。
  • requiredTrue 此验证器要求控件的值为真。它通常用来验证检查框。
  • email 此验证器要求控件的值能通过 email 格式验证。
  • minLength 此验证器要求控件值的长度大于等于所指定的最小长度。当使用 HTML5 的 minlength 属性时,此验证器也会生效。
  • maxLength 此验证器要求控件值的长度小于等于所指定的最大长度。当使用 HTML5 的 maxlength 属性时,此验证器也会生效。
  • pattern 此验证器要求控件的值匹配某个正则表达式。当使用 HTML5 的 pattern 属性时,它也会生效。
  • nullValidator 此验证器什么也不做。
  • compose 把多个验证器合并成一个函数,它会返回指定控件的各个错误映射表的并集。
  • composeAsync 把多个异步验证器合并成一个函数,它会返回指定控件的各个错误映射表的并集。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ngOnInit(): void {
this.heroForm = new FormGroup({
name: new FormControl(this.hero.name, [
Validators.required,
Validators.minLength(4),
forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
]),
alterEgo: new FormControl(this.hero.alterEgo),
power: new FormControl(this.hero.power, Validators.required)
});

}

get name() { return this.heroForm.get('name'); }

get power() { return this.heroForm.get('power'); }

所有这些验证器都是同步的,所以它们作为第二个参数传递。注意,你可以通过把这些函数放到一个数组中传入来支持多个验证器。

如果你到模板中找到 name 输入框,就会发现它和模板驱动的例子很相似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<input type="text" id="name" class="form-control"
formControlName="name" required>

<div *ngIf="name.invalid && (name.dirty || name.touched)"
class="alert alert-danger">

<div *ngIf="name.errors?.['required']">
Name is required.
</div>
<div *ngIf="name.errors?.['minlength']">
Name must be at least 4 characters long.
</div>
<div *ngIf="name.errors?.['forbiddenName']">
Name cannot be Bob.
</div>
</div>

这个表单与模板驱动的版本不同,它不再导出任何指令。相反,它使用组件类中定义的 name 读取器(getter)。

自定义验证器

内置的验证器并不是总能精确匹配应用中的用例,因此有时你需要创建一个自定义验证器。

1
2
3
4
5
6
7
/** A hero's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const forbidden = nameRe.test(control.value);
return forbidden ? {forbiddenName: {value: control.value}} : null;
};
}

forbiddenNameValidator 工厂函数返回配置好的验证器函数。 该函数接受一个 Angular control 对象,并在control值有效时返回 null,或无效时返回验证错误信息。 验证错误信息通常有一个名为 验证器名称(forbiddenName)的属性。其值为 K-V形式的字典,你可以插入错误信息。

自定义异步验证器和同步验证器很像,只是它们必须返回一个稍后会输出 null 或“验证错误对象”的承诺(Promise)或可观察对象,如果是可观察对象,那么它必须在某个时间点被完成(complete),那时候这个表单就会使用它输出的最后一个值作为验证结果。

把自定义验证器添加到响应式表单中

1
2
3
4
5
6
7
8
9
this.heroForm = new FormGroup({
name: new FormControl(this.hero.name, [
Validators.required,
Validators.minLength(4),
forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
]),
alterEgo: new FormControl(this.hero.alterEgo),
power: new FormControl(this.hero.power, Validators.required)
});

为模板驱动表单中添加自定义验证器

在模板驱动表单中,要为模板添加一个指令,该指令包含了一个 validator 函数。比如,ForbiddenValidatorDirective 指令中 包含了 forbiddenNameValidator 验证函数。

Angular 在验证过程中会识别出该指令的作用,因为该指令把自己注册成了 NG_VALIDATORS 提供者,如下例所示。NG_VALIDATORS 是一个带有可扩展验证器集合的预定义提供者。

1
2
3
4
5
6
7
8
9
10
11
@Directive({
selector: '[appForbiddenName]',
providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
})
export class ForbiddenValidatorDirective implements Validator {
@Input('appForbiddenName') forbiddenName = '';

validate(control: AbstractControl): ValidationErrors | null {
return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control): null;
}
}

一旦 ForbiddenValidatorDirective 写好了,你只要把 选择器 forbiddenName 添加到输入框上就可以激活这个验证器了。比如:

1
2
3
<input type="text" id="name" name="name" class="form-control"
required minlength="4" appForbiddenName="bob"
[(ngModel)]="hero.name" #name="ngModel">

注意,自定义验证指令是用 useExisting 而不是 useClass 来实例化的。注册的验证程序必须是 ForbiddenValidatorDirective 实例本身 - 表单中的实例,也就是表单中 forbiddenName 属性被绑定到了”bob”的那个。

如果用 useClass 来代替 useExisting,就会注册一个新的类实例,而它是没有 forbiddenName 的。

表示控件状态的 CSS 类

Angular 会自动把很多控件属性作为 CSS 类映射到控件所在的元素上。你可以使用这些类来根据表单状态给表单控件元素添加样式。目前支持下列类:

  • .ng-valid

  • .ng-invalid

  • .ng-pending

  • .ng-pristine

  • .ng-dirty

  • .ng-untouched

  • .ng-touched

  • .ng-submitted (只对 form 元素添加)

在下面的例子中,这个英雄表单使用 .ng-valid 和 .ng-invalid 来设置每个表单控件的边框颜色。

1
2
3
4
5
6
7
.ng-valid[required], .ng-valid.required  {
border-left: 5px solid #42A948; /* green */
}

.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* red */
}

跨字段交叉验证

跨字段交叉验证器是一种自定义验证器,可以对表单中不同字段的值进行比较,并针对它们的组合进行接受或拒绝。

为响应式表单添加交叉验证

该表单具有以下结构:

1
2
3
4
5
const heroForm = new FormGroup({
'name': new FormControl(),
'alterEgo': new FormControl(),
'power': new FormControl()
});

name 和 alterEgo 是兄弟控件。要想在单个自定义验证器中计算这两个控件,你就必须在它们共同的祖先控件中执行验证:FormGroup。

要想给 FormGroup 添加验证器,就要在创建时把一个新的验证器传给它的第二个参数。

1
2
3
4
5
const heroForm = new FormGroup({
'name': new FormControl(),
'alterEgo': new FormControl(),
'power': new FormControl()
}, { validators: identityRevealedValidator });

验证器的代码如下。

1
2
3
4
5
6
7
/** A hero's name can't match the hero's alter ego */
export const identityRevealedValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const name = control.get('name');
const alterEgo = control.get('alterEgo');

return name && alterEgo && name.value === alterEgo.value ? { identityRevealed: true } : null;
};

该验证器通过调用 FormGroup 的 get 方法来检索这些子控件,然后比较 name 和 alterEgo 控件的值。

为了提供更好的用户体验,当表单无效时,模板还会显示一条恰当的错误信息。

1
2
3
<div *ngIf="heroForm.errors?.['identityRevealed'] && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert alert-danger">
Name cannot match alter ego.
</div>

如果 FormGroup 中有一个由 identityRevealed 验证器返回的交叉验证错误,*ngIf 就会显示错误,但只有当该用户已经与表单进行过交互的时候才显示。

为模板驱动表单添加交叉验证

对于模板驱动表单,你必须创建一个指令来包装验证器函数。你可以使用NG_VALIDATORS 令牌来把该指令提供为验证器,如下例所示。

1
2
3
4
5
6
7
8
9
@Directive({
selector: '[appIdentityRevealed]',
providers: [{ provide: NG_VALIDATORS, useExisting: IdentityRevealedValidatorDirective, multi: true }]
})
export class IdentityRevealedValidatorDirective implements Validator {
validate(control: AbstractControl): ValidationErrors | null {
return identityRevealedValidator(control);
}
}

你必须把这个新指令添加到 HTML 模板中。由于验证器必须注册在表单的最高层,因此下列模板会把该指令放在 form 标签上。

1
<form #heroForm="ngForm" appIdentityRevealed>

为了提供更好的用户体验,当表单无效时,我们要显示一个恰当的错误信息。

1
2
3
<div *ngIf="heroForm.errors?.['identityRevealed'] && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert">
Name cannot match alter ego.
</div>

这在模板驱动表单和响应式表单中都是一样的。

创建异步验证器

异步验证器实现了 AsyncValidatorFn 和 AsyncValidator 接口。它们与其同步版本非常相似,但有以下不同之处。

validate() 函数必须返回一个 Promise 或可观察对象,

返回的可观察对象必须是有尽的,这意味着它必须在某个时刻完成(complete)。要把无尽的可观察对象转换成有尽的,可以在管道中加入过滤操作符,比如 first、last、take 或 takeUntil。

异步验证在同步验证完成后才会发生,并且只有在同步验证成功时才会执行。如果更基本的验证方法已经发现了无效输入,那么这种检查顺序就可以让表单避免使用昂贵的异步验证流程(比如 HTTP 请求)。

异步验证开始之后,表单控件就会进入 pending 状态。可以检查控件的 pending 属性,并用它来给出对验证中的视觉反馈。

一种常见的 UI 模式是在执行异步验证时显示 Spinner(转轮)。下面的例子展示了如何在模板驱动表单中实现这一点。

1
2
<input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator>
<app-spinner *ngIf="model.pending"></app-spinner>

实现自定义异步验证器

下面的代码创建了一个验证器类 UniqueAlterEgoValidator,它实现了 AsyncValidator 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
@Injectable({ providedIn: 'root' })
export class UniqueAlterEgoValidator implements AsyncValidator {
constructor(private heroesService: HeroesService) {}

validate(
control: AbstractControl
): Observable<ValidationErrors | null> {
return this.heroesService.isAlterEgoTaken(control.value).pipe(
map(isTaken => (isTaken ? { uniqueAlterEgo: true } : null)),
catchError(() => of(null))
);
}
}

构造函数中注入了 HeroesService,它定义了如下接口。

1
2
3
interface HeroesService {
isAlterEgoTaken: (alterEgo: string) => Observable<boolean>;
}

与任何验证器一样,如果表单有效,该方法返回 null,如果无效,则返回 ValidationErrors。这个验证器使用 catchError 操作符来处理任何潜在的错误。在这个例子中,验证器将 isAlterEgoTaken() 错误视为成功的验证,因为未能发出验证请求并不一定意味着这个第二人格无效。你也可以用不同的方式处理这种错误,比如返回 ValidationError 对象。

一段时间过后,这条可观察对象链完成,异步验证也就完成了。pending 标志位也设置为 false,该表单的有效性也已更新。

将异步验证器添加到响应式表单

要以响应式表单使用异步验证器,请首先将验证器注入组件类的构造函数。

1
constructor(private alterEgoValidator: UniqueAlterEgoValidator) {}

然后,将验证器函数直接传递给 FormControl 以应用它。

1
2
3
4
const alterEgoControl = new FormControl('', {
asyncValidators: [this.alterEgoValidator.validate.bind(this.alterEgoValidator)],
updateOn: 'blur'
});

将异步验证器添加到模板驱动表单

要在模板驱动表单中使用异步验证器,请创建一个新指令并在其上注册 NG_ASYNC_VALIDATORS 提供者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Directive({
selector: '[appUniqueAlterEgo]',
providers: [
{
provide: NG_ASYNC_VALIDATORS,
useExisting: forwardRef(() => UniqueAlterEgoValidatorDirective),
multi: true
}
]
})
export class UniqueAlterEgoValidatorDirective implements AsyncValidator {
constructor(private validator: UniqueAlterEgoValidator) {}

validate(
control: AbstractControl
): Observable<ValidationErrors | null> {
return this.validator.validate(control);
}
}

然后,与使用同步验证器一样,将指令的选择器添加到输入以激活它。

1
2
3
4
5
6
7
<input type="text"
id="alterEgo"
name="alterEgo"
#alterEgo="ngModel"
[(ngModel)]="hero.alterEgo"
[ngModelOptions]="{ updateOn: 'blur' }"
appUniqueAlterEgo>

默认情况下,所有验证程序在每次表单值更改后都会运行。对于同步验证器,这通常不会对应用性能产生明显的影响。但是,异步验证器通常会执行某种 HTTP 请求来验证控件。每次按键后调度一次 HTTP 请求都会给后端 API 带来压力,应该尽可能避免。

你可以把 updateOn 属性从 change(默认值)改成 submit 或 blur 来推迟表单验证的更新时机。

使用模板驱动表单时,可以在模板中设置该属性。

1
<input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}">

使用响应式表单时,可以在 FormControl 实例中设置该属性。

1
new FormControl('', {updateOn: 'blur'});