每当表单控件中的值发生变化时,Angular 就会进行验证,并生成一个验证错误的列表(对应着 INVALID 状态)或者 null(对应着 VALID 状态)。
你可以通过把 ngModel
导出成局部模板变量来查看该控件的状态。 比如下面这个例子就把 NgModel
导出成了一个名叫 name
的变量:
<input id="name" name="name" class="form-control"
required minlength="4" appForbiddenName="bob" //required minlength 都是内置验证函数 appForbiddenName是自定义验证函数
[(ngModel)]="hero.name" #name="ngModel" >//#name="ngModel" 此处把ngmodel导出为一个模板变量,此变量可以查询表单变化的相关状态
<div *ngIf="name.invalid && (name.dirty || name.touched)" //通过上述把ngmodel导出为模板变量来访问表单相关状态
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>
请注意以下几点:
<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
在响应式表单中,权威数据源是其组件类。不应该通过模板上的属性来添加验证器,而应该在组件类中直接把验证器函数添加到表单控件模型上(FormControl
)。然后,一旦控件发生了变化,Angular 就会调用这些函数。
有两种验证器函数:同步验证器和异步验证器。
同步验证器函数接受一个控件实例,然后返回一组验证错误或 null
。你可以在实例化一个 FormControl
时把它作为构造函数的第二个参数传进去。
异步验证器函数接受一个控件实例,并返回一个承诺(Promise)或可观察对象(Observable),它们稍后会发出一组验证错误或者 null
。你可以在实例化一个 FormControl
时把它作为构造函数的第三个参数传进去。
注意:出于性能方面的考虑,只有在所有同步验证器都通过之后,Angular 才会运行异步验证器。当每一个异步验证器都执行完之后,才会设置这些验证错误。//因为异步验证器主要用来验证http请求以及数据库读取数据,当同步验证器没有通过的时候就不再去验证异步验证器了,提升应用的性能。
要想把这个英雄表单改造成一个响应式表单的验证方式,用内置验证器,但这次改为用它们的函数形态:
ngOnInit(): void {
this.heroForm = new FormGroup({
'name': new FormControl(this.hero.name, [
Validators.required,//required是内置验证器,在fromgoup的构造函数里面设置验证
Validators.minLength(4),//minLength是内置验证器,在fromgoup的构造函数里面设置验证
forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.//forbiddenNameValidator是自定义验证器,在fromgoup的构造函数里面设置验证
]),
'alterEgo': new FormControl(this.hero.alterEgo),
'power': new FormControl(this.hero.power, Validators.required)
});
}
get name() { return this.heroForm.get('name'); }//模板通过这些getter访问表单控件
get power() { return this.heroForm.get('power'); }//模板通过这些getter访问表单控件
注意
name
控件设置了两个内置验证器:Validators.required
和 Validators.minLength(4)
。要了解更多信息,参见本章的自定义验证器一节。
由于这些验证器都是同步验证器,因此你要把它们作为第二个参数传进去。
可以通过把这些函数放进一个数组后传进去,可以支持多重验证器。
这个例子添加了一些 getter 方法。在响应式表单中,你通常会通过它所属的控件组(FormGroup)的 get
方法来访问表单控件,但有时候为模板定义一些 getter 作为简短形式。
如果你到模板中找到 name 输入框,就会发现它和模板驱动的例子很相似。:
<input id="name" class="form-control"
formControlName="name" required >//此处的formControlName对应组件中的name
<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
读取器。
required
属性仍然存在,虽然验证不再需要它,但你仍然要在模板中保留它,以支持 CSS 样式或可访问性
由于内置验证器无法适用于所有应用场景,有时候你还是得创建自定义验证器。
考虑前面的例子中的 forbiddenNameValidator
函数。该函数的定义看起来是这样的:
/** A hero's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} | null => {
const forbidden = nameRe.test(control.value);
return forbidden ? {'forbiddenName': {value: control.value}} : null;
};
}
这个函数实际上是一个工厂,它接受一个用来检测指定名字是否已被禁用的正则表达式,并返回一个验证器函数。
定义异步验证器和同步验证器很像,只是它们必须返回一个稍后会输出 null 或“验证错误对象”的承诺(Promise)或可观察对象,如果是可观察对象,那么它必须在某个时间点被完成(complete),那时候这个表单就会使用它输出的最后一个值作为验证结果。
在响应式表单组件中,添加自定义验证器相当简单。你所要做的一切就是直接把这个函数传给 FormControl
。
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) //内置验证器传第二个参数
});
在模板驱动表单中,你不用直接访问 FormControl
实例。所以不能像响应式表单中那样把验证器传进去,而应该在模板中添加一个指令。
ForbiddenValidatorDirective
指令相当于 forbiddenNameValidator
的包装器。
Angular 在验证过程中能识别出指令的作用,是因为指令把自己注册成了 NG_VALIDATORS
提供商,该提供商拥有一组可扩展的验证器。
providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}] //指令
然后该指令类实现了 Validator
接口,以便它能简单的与 Angular 表单集成在一起。这个指令的其余部分有助于你理解它们是如何协作的:
@Directive({
selector: '[appForbiddenName]',
providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}] //useExisting 而不是useClass
})
export class ForbiddenValidatorDirective implements Validator {
@Input('appForbiddenName') forbiddenName: string;
validate(control: AbstractControl): {[key: string]: any} | null {
return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
: null;
}
}
一旦 ForbiddenValidatorDirective
写好了,你只要把 forbiddenName
选择器添加到输入框上就可以激活这个验证器了。比如:
<input id="name" name="name" class="form-control"
required minlength="4" appForbiddenName="bob" //c此处把自定义验证器添加进驱动式表单
[(ngModel)]="hero.name" #name="ngModel" >
你可能注意到了自定义验证器指令是用 useExisting
而不是 useClass
来实例化的。注册的验证器必须是这个 ForbiddenValidatorDirective
实例本身,也就是表单中 forbiddenName
属性被绑定到了"bob"的那个。如果用 useClass
来代替 useExisting
,就会注册一个新的类实例,而它是没有 forbiddenName
的。
像 AngularJS 中一样,Angular 会自动把很多控件属性作为 CSS 类映射到控件所在的元素上。你可以使用这些类来根据表单状态给表单控件元素添加样式。目前支持下列类:
.ng-valid
.ng-invalid
.ng-pending
.ng-pristine
.ng-dirty
.ng-untouched
.ng-touched
在下一节中,我们要确保英雄们不能通过填写表单来暴露他们的真实身份。要做到这一点,我们就要验证英雄的名字和他的第二人格(alterEgo)是否匹配。
const heroForm = new FormGroup({
'name': new FormControl(),
'alterEgo': new FormControl(),
'power': new FormControl()
});
注意,name 和 alterEgo 是兄弟控件。要想在单个的自定义验证器中计算这两个控件,我们就得在它们共同的祖先控件(FormGroup
)中进行验证。这样,我们就可以查询 FormGroup
的子控件,从而让我们能够比较它们的值。
要想给 FormGroup
添加验证器,就要在创建时把一个新的验证器传给它的第二个参数。
const heroForm = new FormGroup({
'name': new FormControl(),
'alterEgo': new FormControl(),
'power': new FormControl()
}, { validators: identityRevealedValidator });
验证器的代码如下:
/** A hero's name can't match the hero's alter ego */
export const identityRevealedValidator: ValidatorFn = (control: FormGroup): ValidationErrors | null => {
const name = control.get('name');
const alterEgo = control.get('alterEgo');
return name && alterEgo && name.value === alterEgo.value ? { 'identityRevealed': true } : null;
};
这个身份验证器实现了 ValidatorFn
接口。它接收一个 Angular 表单控件对象作为参数,当表单有效时,它返回一个 null,否则返回 ValidationErrors
对象。
我们先通过调用 FormGroup
的 get 方法来获取子控件。然后,简单地比较一下 name
和 alterEgo
控件的值。
如果这两个值不一样,那么英雄的身份就应该继续保密,我们可以安全的返回 null。否则就说明英雄的身份已经暴露了,我们必须通过返回一个错误对象来把这个表单标记为无效的。
接下来,为了提供更好的用户体验,当表单无效时,我们还要显示一个恰当的错误信息。
<div *ngIf="heroForm.errors?.identityRevealed && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert alert-danger">
Name cannot match alter ego.
</div>
注意,我们需要检查:
首先,我们必须创建一个指令,它会包装这个验证器函数。我们使用 NG_VALIDATORS
令牌来把它作为验证器提供出来。如果你还不清楚为什么要这么做或者不能完全理解这种语法,请重新访问前面的小节。
@Directive({
selector: '[appIdentityRevealed]',
providers: [{ provide: NG_VALIDATORS, useExisting: IdentityRevealedValidatorDirective, multi: true }]
})
export class IdentityRevealedValidatorDirective implements Validator {
validate(control: AbstractControl): ValidationErrors {
return identityRevealedValidator(control)
}
}
接下来,我们要把该指令添加到 HTML 模板中。由于验证器必须注册在表单的最高层,所以我们要把该指令放在 form
标签上。
<form #heroForm="ngForm" appIdentityRevealed>
为了提供更好的用户体验,当表单无效时,我们要显示一个恰当的错误信息
<div *ngIf="heroForm.errors?.identityRevealed && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert alert-danger">
Name cannot match alter ego.
</div>
注意,我们需要检查:
该表单具有一个由 identityRevealed
验证器提供的交叉验证错误对象。
用户已经和表单进行过交互。
这样就完成了这个交叉验证的例子。我们的做法是:
基于两个相邻控件的值来验证表单
当用户与表单交互过并且验证失败时,才显示一个描述性的错误信息。
就像同步验证器有 ValidatorFn
和 Validator
接口一样,异步验证器也有自己的对应物:AsyncValidatorFn
和 AsyncValidator
。
它们非常像,但是有下列不同:
它们必须返回承诺(Promise)或可观察对象(Observable),
返回的可观察对象必须是有限的,也就是说,它必须在某个时间点结束(complete)。要把无尽的可观察对象转换成有限的,可以使用 first
、last
、take
或 takeUntil
等过滤型管道对其进行处理。
注意!异步验证总是会在同步验证之后执行,并且只有当同步验证成功了之后才会执行。如果更基本的验证方法已经失败了,那么这能让表单避免进行可能会很昂贵的异步验证过程,比如 HTTP 请求。
在异步验证器开始之后,表单控件会进入 pending
状态。你可以监视该控件的 pending
属性,利用它来给用户一些视觉反馈,表明正在进行验证。
常见的 UI 处理模式是在执行异步验证时显示一个旋转指示标(spinner)。下面的例子展示了在模板驱动表单中该怎么做
<input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator>
<app-spinner *ngIf="model.pending"></app-spinner>
在下一节中,会异步执行一个验证,以确保英雄选取了一个还没有人选过的第二人格。新的英雄不断招募,而老的英雄不断离开。这意味着我们没法提前拿到一个可用的第二人格列表。
要验证潜在的第二人格,我们需要咨询一个存有全部已招募英雄的中央数据库。而这个过程是异步的,我们需要一个特殊的验证器。
我们先创建一个验证器类。
@Injectable({ providedIn: 'root' })
export class UniqueAlterEgoValidator implements AsyncValidator {
constructor(private heroesService: HeroesService) {}
validate(
ctrl: AbstractControl
): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
return this.heroesService.isAlterEgoTaken(ctrl.value).pipe(
map(isTaken => (isTaken ? { uniqueAlterEgo: true } : null)),
catchError(() => null)
);
}
}
如你所见,UniqueAlterEgoValidator
类实现了 AsyncValidator
接口。在其构造函数中,我们注入了一个HeroesService
,其接口如下:
interface HeroesService {
isAlterEgoTaken: (alterEgo: string) => Observable<boolean>;
}
在真实的应用中,HeroesService
负责向英雄数据库发起一个 HTTP 请求,以检查该第二人格是否可用。 从该验证器的视角看,此服务的具体实现无关紧要,所以我们仅仅针对 HeroesService
接口来写实现代码。
当验证开始的时候,UniqueAlterEgoValidator
把任务委托给 HeroesService
的 isAlterEgoTaken()
方法,并传入当前控件的值。这时候,该控件会被标记为 pending
状态,直到 validate()
方法所返回的可观察对象完成(complete)了。
isAlterEgoTaken()
方法会发出一个 HTTP 请求,以检查该第二人格是否可用,并返回一个 Observable<boolean>
型结果。我们通过 map
操作符把响应对象串起来,并把它转换成一个有效性结果。 与往常一样,如果表单有效则返回 null
,否则返回 ValidationErrors
。我们还是用 catchError
操作符来确保对任何潜在错误都进行了处理。
这里,我们决定将 isAlterEgoTaken()
中的错误视为成功验证,因为如果没能发起验证请求,未必代表这个第二人格是无效的。你也可以将其视为失败,并返回 ValidationError
对象。
一段时间之后,可观察对象完成了,异步验证也就结束了。这时候 pending
标志就改成了 false
,并且表单的有效性也更新了。