验证表单输入
在模板驱动表单中验证输入
为了往模板驱动表单中添加验证机制,你要添加一些验证属性,就像原生的 HTML 表单验证器一样。 Angular 会用指令来匹配这些具有验证功能的指令。
每当表单控件中的值发生变化时,Angular 就会进行验证,并生成一个验证错误的列表(对应着 INVALID 状态)或者 null(对应着 VALID 状态)。
你可以通过把 ngModel 导出成局部模板变量来查看该控件的状态。 比如下面这个例子就把 NgModel 导出成了一个名叫 name 的变量:
1 | <input type="text" id="name" name="name" class="form-control" |
注意这个例子讲解的如下特性。
<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 | ngOnInit(): void { |
所有这些验证器都是同步的,所以它们作为第二个参数传递。注意,你可以通过把这些函数放到一个数组中传入来支持多个验证器。
如果你到模板中找到 name 输入框,就会发现它和模板驱动的例子很相似。
1 | <input type="text" id="name" class="form-control" |
这个表单与模板驱动的版本不同,它不再导出任何指令。相反,它使用组件类中定义的 name 读取器(getter)。
自定义验证器
内置的验证器并不是总能精确匹配应用中的用例,因此有时你需要创建一个自定义验证器。
1 | /** A hero's name can't match the given regular expression */ |
forbiddenNameValidator 工厂函数返回配置好的验证器函数。 该函数接受一个 Angular control 对象,并在control值有效时返回 null,或无效时返回验证错误信息。 验证错误信息通常有一个名为 验证器名称(forbiddenName)的属性。其值为 K-V形式的字典,你可以插入错误信息。
自定义异步验证器和同步验证器很像,只是它们必须返回一个稍后会输出 null 或“验证错误对象”的承诺(Promise)或可观察对象,如果是可观察对象,那么它必须在某个时间点被完成(complete),那时候这个表单就会使用它输出的最后一个值作为验证结果。
把自定义验证器添加到响应式表单中
1 | this.heroForm = new FormGroup({ |
为模板驱动表单中添加自定义验证器
在模板驱动表单中,要为模板添加一个指令,该指令包含了一个 validator 函数。比如,ForbiddenValidatorDirective 指令中 包含了 forbiddenNameValidator 验证函数。
Angular 在验证过程中会识别出该指令的作用,因为该指令把自己注册成了 NG_VALIDATORS 提供者,如下例所示。NG_VALIDATORS 是一个带有可扩展验证器集合的预定义提供者。
1 | ({ |
一旦 ForbiddenValidatorDirective 写好了,你只要把 选择器 forbiddenName 添加到输入框上就可以激活这个验证器了。比如:
1 | <input type="text" id="name" name="name" class="form-control" |
注意,自定义验证指令是用 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 | .ng-valid[required], .ng-valid.required { |
跨字段交叉验证
跨字段交叉验证器是一种自定义验证器,可以对表单中不同字段的值进行比较,并针对它们的组合进行接受或拒绝。
为响应式表单添加交叉验证
该表单具有以下结构:
1 | const heroForm = new FormGroup({ |
name 和 alterEgo 是兄弟控件。要想在单个自定义验证器中计算这两个控件,你就必须在它们共同的祖先控件中执行验证:FormGroup。
要想给 FormGroup 添加验证器,就要在创建时把一个新的验证器传给它的第二个参数。
1 | const heroForm = new FormGroup({ |
验证器的代码如下。
1 | /** A hero's name can't match the hero's alter ego */ |
该验证器通过调用 FormGroup 的 get 方法来检索这些子控件,然后比较 name 和 alterEgo 控件的值。
为了提供更好的用户体验,当表单无效时,模板还会显示一条恰当的错误信息。
1 | <div *ngIf="heroForm.errors?.['identityRevealed'] && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert alert-danger"> |
如果 FormGroup 中有一个由 identityRevealed 验证器返回的交叉验证错误,*ngIf 就会显示错误,但只有当该用户已经与表单进行过交互的时候才显示。
为模板驱动表单添加交叉验证
对于模板驱动表单,你必须创建一个指令来包装验证器函数。你可以使用NG_VALIDATORS 令牌来把该指令提供为验证器,如下例所示。
1 | ({ |
你必须把这个新指令添加到 HTML 模板中。由于验证器必须注册在表单的最高层,因此下列模板会把该指令放在 form 标签上。
1 | <form #heroForm="ngForm" appIdentityRevealed> |
为了提供更好的用户体验,当表单无效时,我们要显示一个恰当的错误信息。
1 | <div *ngIf="heroForm.errors?.['identityRevealed'] && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert"> |
这在模板驱动表单和响应式表单中都是一样的。
创建异步验证器
异步验证器实现了 AsyncValidatorFn 和 AsyncValidator 接口。它们与其同步版本非常相似,但有以下不同之处。
validate() 函数必须返回一个 Promise 或可观察对象,
返回的可观察对象必须是有尽的,这意味着它必须在某个时刻完成(complete)。要把无尽的可观察对象转换成有尽的,可以在管道中加入过滤操作符,比如 first、last、take 或 takeUntil。
异步验证在同步验证完成后才会发生,并且只有在同步验证成功时才会执行。如果更基本的验证方法已经发现了无效输入,那么这种检查顺序就可以让表单避免使用昂贵的异步验证流程(比如 HTTP 请求)。
异步验证开始之后,表单控件就会进入 pending 状态。可以检查控件的 pending 属性,并用它来给出对验证中的视觉反馈。
一种常见的 UI 模式是在执行异步验证时显示 Spinner(转轮)。下面的例子展示了如何在模板驱动表单中实现这一点。
1 | <input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator> |
实现自定义异步验证器
下面的代码创建了一个验证器类 UniqueAlterEgoValidator,它实现了 AsyncValidator 接口
1 | providedIn: 'root' }) ({ |
构造函数中注入了 HeroesService,它定义了如下接口。
1 | interface HeroesService { |
与任何验证器一样,如果表单有效,该方法返回 null,如果无效,则返回 ValidationErrors。这个验证器使用 catchError 操作符来处理任何潜在的错误。在这个例子中,验证器将 isAlterEgoTaken() 错误视为成功的验证,因为未能发出验证请求并不一定意味着这个第二人格无效。你也可以用不同的方式处理这种错误,比如返回 ValidationError 对象。
一段时间过后,这条可观察对象链完成,异步验证也就完成了。pending 标志位也设置为 false,该表单的有效性也已更新。
将异步验证器添加到响应式表单
要以响应式表单使用异步验证器,请首先将验证器注入组件类的构造函数。
1 | constructor(private alterEgoValidator: UniqueAlterEgoValidator) {} |
然后,将验证器函数直接传递给 FormControl 以应用它。
1 | const alterEgoControl = new FormControl('', { |
将异步验证器添加到模板驱动表单
要在模板驱动表单中使用异步验证器,请创建一个新指令并在其上注册 NG_ASYNC_VALIDATORS 提供者。
1 | ({ |
然后,与使用同步验证器一样,将指令的选择器添加到输入以激活它。
1 | <input type="text" |
默认情况下,所有验证程序在每次表单值更改后都会运行。对于同步验证器,这通常不会对应用性能产生明显的影响。但是,异步验证器通常会执行某种 HTTP 请求来验证控件。每次按键后调度一次 HTTP 请求都会给后端 API 带来压力,应该尽可能避免。
你可以把 updateOn 属性从 change(默认值)改成 submit 或 blur 来推迟表单验证的更新时机。
使用模板驱动表单时,可以在模板中设置该属性。
1 | <input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}"> |
使用响应式表单时,可以在 FormControl 实例中设置该属性。
1 | new FormControl('', {updateOn: 'blur'}); |