表单验证
表单验证
通过验证用户输入的准确性和完整性,来增强整体数据质量。
本文展示了在界面中如何验证用户输入,并显示有用的验证信息,先使用模板驱动表单方式,再使用响应式表单方式。
模板驱动验证
为了往模板驱动表单中添加验证机制,你要添加一些验证属性,就像原生的 HTML 表单验证器。 Angular 会用指令来匹配这些具有验证功能的指令。
每当表单控件中的值发生变化时,Angular 就会进行验证,并生成一个验证错误的列表(对应着 INVALID 状态)或者 null(对应着 VALID 状态)。
你可以通过把 ngModel
导出成局部模板变量来查看该控件的状态。 比如下面这个例子就把 NgModel
导出成了一个名叫 name
的变量:
template/hero-form-template.component.html (name)
content_copy<input 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 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。要了解更多信息,参见自定义验证器一节。
为何检查 dirty 和 touched?
你肯定不希望应用在用户还没有编辑过表单的时候就给他们显示错误提示。 对 dirty
和 touched
的检查可以避免这种问题。改变控件的值会改变控件的 dirty
(脏)状态,而当控件失去焦点时,就会改变控件的 touched
(碰过)状态。
响应式表单的验证
在响应式表单中,真正的源码都在组件类中。不应该通过模板上的属性来添加验证器,而应该在组件类中直接把验证器函数添加到表单控件模型上(FormControl
)。然后,一旦控件发生了变化,Angular 就会调用这些函数。
验证器函数
有两种验证器函数:同步验证器和异步验证器。
同步验证器
函数接受一个控件实例,然后返回一组验证错误或null
。你可以在实例化一个FormControl
时把它作为构造函数的第二个参数传进去。
注意:出于性能方面的考虑,只有在所有同步验证器都通过之后,Angular 才会运行异步验证器。当每一个异步验证器都执行完之后,才会设置这些验证错误。
内置验证器
你可以写自己的验证器,也可以使用一些 Angular 内置的验证器。
模板驱动表单中可用的那些属性型验证器(如 required
、minlength
等)对应于 Validators
类中的同名函数。要想查看内置验证器的全列表,参见 API 参考手册中的验证器部分。
要想把这个英雄表单改造成一个响应式表单,你还是用那些内置验证器,但这次改为用它们的函数形态。
reactive/hero-form-reactive.component.ts (validator functions)
content_copyngOnInit(): 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
控件设置了两个内置验证器:Validators.required
和Validators.minLength(4)
。要了解更多信息,参见本章的自定义验证器一节。
如果你到模板中找到 name 输入框,就会发现它和模板驱动的例子很相似。
reactive/hero-form-reactive.component.html (name with error msg)
content_copy<input 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
读取器。
自定义验证器
由于内置验证器无法适用于所有应用场景,有时候你还是得创建自定义验证器。
考虑前面的例子中的 forbiddenNameValidator
函数。该函数的定义看起来是这样的:
shared/forbidden-name.directive.ts (forbiddenNameValidator)
content_copy/** 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;
};
}
这个函数实际上是一个工厂,它接受一个用来检测指定名字是否已被禁用的正则表达式,并返回一个验证器函数。
在本例中,禁止的名字是“bob”; 验证器会拒绝任何带有“bob”的英雄名字。 在其他地方,只要配置的正则表达式可以匹配上,它可能拒绝“alice”或者任何其他名字。
forbiddenNameValidator
工厂函数返回配置好的验证器函数。 该函数接受一个 Angular 控制器对象,并在控制器值有效时返回 null,或无效时返回验证错误对象。 验证错误对象通常有一个名为验证秘钥(forbiddenName
)的属性。其值为一个任意词典,你可以用来插入错误信息({name}
)。
自定义异步验证器和同步验证器很像,只是它们必须返回一个稍后会输出 null 或“验证错误对象”的承诺(Promise)或可观察对象,如果是可观察对象,那么它必须在某个时间点被完成(complete),那时候这个表单就会使用它输出的最后一个值作为验证结果。(译注:HTTP 服务是自动完成的,但是某些自定义的可观察对象可能需要手动调用 complete 方法)
添加响应式表单
在响应式表单组件中,添加自定义验证器相当简单。你所要做的一切就是直接把这个函数传给 FormControl
。
reactive/hero-form-reactive.component.ts (validator functions)
content_copythis.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
提供商中,该提供商拥有一组可扩展的验证器。
shared/forbidden-name.directive.ts (providers)
content_copyproviders: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
然后该指令类实现了 Validator
接口,以便它能简单的与 Angular 表单集成在一起。这个指令的其余部分有助于你理解它们是如何协作的:
shared/forbidden-name.directive.ts (directive)
content_copy@Directive{ selector: '[appForbiddenName]', providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]})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
选择器添加到输入框上就可以激活这个验证器了。比如:
template/hero-form-template.component.html (forbidden-name-input)
content_copy<input 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 类
像 AngularJS 中一样,Angular 会自动把很多控件属性作为 CSS 类映射到控件所在的元素上。你可以使用这些类来根据表单状态给表单控件元素添加样式。目前支持下列类:
.ng-valid
这个英雄表单使用 .ng-valid
和 .ng-invalid
来设置每个表单控件的边框颜色。
forms.css (status classes)
content_copy.ng-valid[required], .ng-valid.required {
border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* red */
}
跨字段交叉验证
本节将展示如何进行跨字段验证。这里假设你已经有了创建自定义验证器所需的基础知识。
如果你以前没有创建过自定义验证器,请先阅读自定义验证器一节。
在下一节中,我们要确保英雄们不能通过填写表单来暴露他们的真实身份。要做到这一点,我们就要验证英雄的名字和他的第二人格(alterEgo)是否匹配。
添加到响应式表单
表单具有下列结构:
content_copyconst heroForm = new FormGroup{
'name': new FormControl(),
'alterEgo': new FormControl(),
'power': new FormControl()
}
注意,name 和 alterEgo 是兄弟控件。要想在单个的自定义验证器中计算这两个控件,我们就得在它们共同的祖先控件(FormGroup
)中进行验证。这样,我们就可以查询 FormGroup
的子控件,从而让我们能够比较它们的值。
要想给 FormGroup
添加验证器,就要在创建时把一个新的验证器传给它的第二个参数。
content_copyconst heroForm = new FormGroup{
'name': new FormControl(),
'alterEgo': new FormControl(),
'power': new FormControl()
}, { validators: identityRevealedValidator }
验证器的代码如下:
shared/identity-revealed.directive.ts
content_copy/** 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。否则就说明英雄的身份已经暴露了,我们必须通过返回一个错误对象来把这个表单标记为无效的。
接下来,为了提供更好的用户体验,当表单无效时,我们还要显示一个恰当的错误信息。
reactive/hero-form-template.component.html
content_copy<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
验证器返回的交叉验证错误对象。
添加到模板驱动表单中
首先,我们必须创建一个指令,它会包装这个验证器函数。我们使用 NG_VALIDATORS
令牌来把它作为验证器提供出来。如果你还不清楚为什么要这么做或者不能完全理解这种语法,请重新访问前面的小节。
shared/identity-revealed.directive.ts
content_copy@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
标签上。
template/hero-form-template.component.html
content_copy<form #heroForm="ngForm" appIdentityRevealed>
为了提供更好的用户体验,当表单无效时,我们要显示一个恰当的错误信息。
template/hero-form-template.component.html
content_copy<div *ngIf="heroForm.errors?.identityRevealed && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert alert-danger">
Name cannot match alter ego.
</div>
注意,我们需要检查:
- 该表单具有一个由
identityRevealed
验证器提供的交叉验证错误对象。
这样就完成了这个交叉验证的例子。我们的做法是:
- 基于两个相邻控件的值来验证表单
你可以运行在线例子
/
下载范例来查看完整的响应式和模板驱动表单的代码。