填写这份《一分钟调查》,帮我们(开发组)做得更好!去填写Home

验证表单输入

Validating form input

通过验证用户输入的准确性和完整性,可以提高整体的数据质量。该页面显示了如何从 UI 验证用户输入,以及如何在响应式表单和模板驱动表单中显示有用的验证消息。

You can improve overall data quality by validating user input for accuracy and completeness. This page shows how to validate user input from the UI and display useful validation messages, in both reactive and template-driven forms.

先决条件

Prerequisites

在阅读表单验证之前,你应该对这些内容有一个基本的了解。

Before reading about form validation, you should have a basic understanding of the following.

要获取这里用讲解表单验证的响应式表单和模板驱动表单的完整示例代码。请运行现场演练 / 下载范例

Get the complete example code for the reactive and template-driven forms used here to illustrate form validation. Run the现场演练 / 下载范例.

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

Validating input in template-driven forms

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

To add validation to a template-driven form, you add the same validation attributes as you would with native HTML form validation. Angular uses directives to match these attributes with validator functions in the framework.

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

Every time the value of a form control changes, Angular runs validation and generates either a list of validation errors that results in an INVALID status, or null, which results in a VALID status.

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

You can then inspect the control's state by exporting ngModel to a local template variable. The following example exports NgModel into a variable called name:

<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>
template/hero-form-template.component.html (name)
      
      <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>
    

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

Notice the following features illustrated by the example.

  • <input> 元素带有一些 HTML 验证属性:requiredminlength。它还带有一个自定义的验证器指令 forbiddenName。要了解更多信息,参见自定义验证器一节。

    The <input> element carries the HTML validation attributes: required and minlength. It also carries a custom validator directive, forbiddenName. For more information, see the Custom validators section.

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

    #name="ngModel" exports NgModel into a local variable called name. NgModel mirrors many of the properties of its underlying FormControl instance, so you can use this in the template to check for control states such as valid and dirty. For a full list of control properties, see the AbstractControl API reference.

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

      The *ngIf on the <div> element reveals a set of nested message divs but only if the name is invalid and the control is either dirty or touched.

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

      Each nested <div> can present a custom message for one of the possible validation errors. There are messages for required, minlength, and forbiddenName.

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

To prevent the validator from displaying errors before the user has a chance to edit the form, you should check for either the dirty or touched states in a control.

  • 当用户在被监视的字段中修改该值时,控件就会被标记为 dirty(脏)。

    When the user changes the value in the watched field, the control is marked as "dirty".

  • 当用户的表单控件失去焦点时,该控件就会被标记为 touched(已接触)。

    When the user blurs the form control element, the control is marked as "touched".

在响应式表单中验证输入

Validating input in reactive forms

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

In a reactive form, the source of truth is the component class. Instead of adding validators through attributes in the template, you add validator functions directly to the form control model in the component class. Angular then calls these functions whenever the value of the control changes.

验证器(Validator)函数

Validator functions

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

Validator functions can be either synchronous or asynchronous.

  • 同步验证器:这些同步函数接受一个控件实例,然后返回一组验证错误或 null。你可以在实例化一个 FormControl 时把它作为构造函数的第二个参数传进去。

    Sync validators: Synchronous functions that take a control instance and immediately return either a set of validation errors or null. You can pass these in as the second argument when you instantiate a FormControl.

  • 异步验证器 :这些异步函数接受一个控件实例并返回一个 Promise 或 Observable,它稍后会发出一组验证错误或 null。在实例化 FormControl 时,可以把它们作为第三个参数传入。

    Async validators: Asynchronous functions that take a control instance and return a Promise or Observable that later emits a set of validation errors or null. You can pass these in as the third argument when you instantiate a FormControl.

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

For performance reasons, Angular only runs async validators if all sync validators pass. Each must complete before errors are set.

内置验证器函数

Built-in validator functions

你可以选择编写自己的验证器函数,也可以使用 Angular 的一些内置验证器。

You can choose to write your own validator functions, or you can use some of Angular's built-in validators.

在模板驱动表单中用作属性的那些内置验证器,比如 requiredminlength,也都可以作为 Validators 类中的函数使用。有关内置验证器的完整列表,参见 API 参考手册中的验证器部分。

The same built-in validators that are available as attributes in template-driven forms, such as required and minlength, are all available to use as functions from the Validators class. For a full list of built-in validators, see the Validators API reference.

要想把这个英雄表单改造成一个响应式表单,你还是要用那些内置验证器,但这次改为用它们的函数形态。参见下面的例子。

To update the hero form to be a reactive form, you can use some of the same built-in validators—this time, in function form, as in the following example.

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'); }
reactive/hero-form-reactive.component.ts (validator functions)
      
      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 控件设置了两个内置验证器 - Validators.requiredValidators.minLength(4) 以及一个自定义验证器 forbiddenNameValidator。(欲知详情,请参阅下面的自定义验证器部分。)

In this example, the name control sets up two built-in validators—Validators.required and Validators.minLength(4)—and one custom validator, forbiddenNameValidator. (For more details see custom validators below.)

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

All of these validators are synchronous, so they are passed as the second argument. Notice that you can support multiple validators by passing the functions in as an array.

这个例子还添加了一些 getter 方法。在响应式表单中,你通常会通过它所属的控件组(FormGroup)的 get 方法来访问表单控件,但有时候为模板定义一些 getter 作为简短形式。

This example also adds a few getter methods. In a reactive form, you can always access any form control through the get method on its parent group, but sometimes it's useful to define getters as shorthand for the template.

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

If you look at the template for the name input again, it is fairly similar to the template-driven example.

<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>
reactive/hero-form-reactive.component.html (name with error msg)
      
      <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 读取器(getter)。

This form differs from the template-driven version in that it no longer exports any directives. Instead, it uses the name getter defined in the component class.

请注意,required 属性仍然出现在模板中。虽然它对于验证来说不是必须的,但为了无障碍性,还是应该保留它。

Notice that the required attribute is still present in the template. Although it's not necessary for validation, it should be retained to for accessibility purposes.

定义自定义验证器

Defining custom validators

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

The built-in validators don't always match the exact use case of your application, so you sometimes need to create a custom validator.

考虑前面的响应式式表单中forbiddenNameValidator 函数。该函数的定义如下。

Consider the forbiddenNameValidator function from previous reactive-form examples. Here's what the definition of that function looks like.

/** 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; }; }
shared/forbidden-name.directive.ts (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;
  };
}
    

这个函数实际上是一个工厂,它接受一个用来检测指定名字是否已被禁用的正则表达式,并返回一个验证器函数。

The function is actually a factory that takes a regular expression to detect a specific forbidden name and returns a validator function.

在本例中,禁止的名字是“bob”; 验证器会拒绝任何带有“bob”的英雄名字。 在其它地方,只要配置的正则表达式可以匹配上,它可能拒绝“alice”或者任何其它名字。

In this sample, the forbidden name is "bob", so the validator will reject any hero name containing "bob". Elsewhere it could reject "alice" or any name that the configuring regular expression matches.

forbiddenNameValidator 工厂函数返回配置好的验证器函数。 该函数接受一个 Angular 控制器对象,并在控制器值有效时返回 null,或无效时返回验证错误对象。 验证错误对象通常有一个名为验证秘钥(forbiddenName)的属性。其值为一个任意词典,你可以用来插入错误信息({name})。

The forbiddenNameValidator factory returns the configured validator function. That function takes an Angular control object and returns either null if the control value is valid or a validation error object. The validation error object typically has a property whose name is the validation key, 'forbiddenName', and whose value is an arbitrary dictionary of values that you could insert into an error message, {name}.

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

Custom async validators are similar to sync validators, but they must instead return a Promise or observable that later emits null or a validation error object. In the case of an observable, the observable must complete, at which point the form uses the last value emitted for validation.

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

Adding custom validators to reactive forms

在响应式表单中,通过直接把该函数传给 FormControl 来添加自定义验证器。

In reactive forms, add a custom validator by passing the function directly to the 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) });
reactive/hero-form-reactive.component.ts (validator functions)
      
      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)
});
    

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

Adding custom validators to template-driven forms

在模板驱动表单中,要为模板添加一个指令,该指令包含了 validator 函数。例如,对应的 ForbiddenValidatorDirective 用作 forbiddenNameValidator 的包装器。

In template-driven forms, add a directive to the template, where the directive wraps the validator function. For example, the corresponding ForbiddenValidatorDirective serves as a wrapper around the forbiddenNameValidator.

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

Angular recognizes the directive's role in the validation process because the directive registers itself with the NG_VALIDATORS provider, as shown in the following example. NG_VALIDATORS is a predefined provider with an extensible collection of validators.

providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
shared/forbidden-name.directive.ts (providers)
      
      providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
    

然后该指令类实现了 Validator 接口,以便它能简单的与 Angular 表单集成在一起。这个指令的其余部分有助于你理解它们是如何协作的:

The directive class then implements the Validator interface, so that it can easily integrate with Angular forms. Here is the rest of the directive to help you get an idea of how it all comes together.

@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; } }
shared/forbidden-name.directive.ts (directive)
      
      @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 选择器添加到输入框上就可以激活这个验证器了。比如:

Once the ForbiddenValidatorDirective is ready, you can add its selector, appForbiddenName, to any input element to activate it. For example:

<input id="name" name="name" class="form-control" required minlength="4" appForbiddenName="bob" [(ngModel)]="hero.name" #name="ngModel" >
template/hero-form-template.component.html (forbidden-name-input)
      
      <input id="name" name="name" class="form-control"
      required minlength="4" appForbiddenName="bob"
      [(ngModel)]="hero.name" #name="ngModel" >
    

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

Notice that the custom validation directive is instantiated with useExisting rather than useClass. The registered validator must be this instance of the ForbiddenValidatorDirective—the instance in the form with its forbiddenName property bound to “bob".

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

If you were to replace useExisting with useClass, then you’d be registering a new class instance, one that doesn’t have a forbiddenName.

表示控件状态的 CSS 类

Control status CSS classes

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

Angular automatically mirrors many control properties onto the form control element as CSS classes. You can use these classes to style form control elements according to the state of the form. The following classes are currently supported.

  • .ng-valid
  • .ng-invalid
  • .ng-pending
  • .ng-pristine
  • .ng-dirty
  • .ng-untouched
  • .ng-touched

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

In the following example, the hero form uses the .ng-valid and .ng-invalid classes to set the color of each form control's border.

.ng-valid[required], .ng-valid.required { border-left: 5px solid #42A948; /* green */ } .ng-invalid:not(form) { border-left: 5px solid #a94442; /* red */ }
forms.css (status classes)
      
      .ng-valid[required], .ng-valid.required  {
  border-left: 5px solid #42A948; /* green */
}

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

跨字段交叉验证

Cross-field validation

跨字段交叉验证器是一种自定义验证器,可以对表单中不同字段的值进行比较,并针对它们的组合进行接受或拒绝。例如,你可能有一个提供互不兼容选项的表单,以便让用户选择 A 或 B,而不能两者都选。某些字段值也可能依赖于其它值;用户可能只有当选择了 A 之后才能选择 B。

A cross-field validator is a custom validator that compares the values of different fields in a form and accepts or rejects them in combination. For example, you might have a form that offers mutually incompatible options, so that if the user can choose A or B, but not both. Some field values might also depend on others; a user might be allowed to choose B only if A is also chosen.

下列交叉验证的例子说明了如何进行如下操作:

The following cross validation examples show how to do the following:

  • 根据两个兄弟控件的值验证响应式表单或模板驱动表单的输入,

    Validate reactive or template-based form input based on the values of two sibling controls,

  • 当用户与表单交互过,且验证失败后,就会显示描述性的错误信息。

    Show a descriptive error message after the user interacted with the form and the validation failed.

这些例子使用了交叉验证,以确保英雄们不会通过填写 Hero 表单来暴露自己的真实身份。验证器会通过检查英雄的名字和第二人格是否匹配来做到这一点。

The examples use cross-validation to ensure that heroes do not reveal their true identities by filling out the Hero Form. The validators do this by checking that the hero names and alter egos do not match.

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

Adding cross-validation to reactive forms

该表单具有以下结构:

The form has the following structure:

const heroForm = new FormGroup({ 'name': new FormControl(), 'alterEgo': new FormControl(), 'power': new FormControl() });
      
      const heroForm = new FormGroup({
  'name': new FormControl(),
  'alterEgo': new FormControl(),
  'power': new FormControl()
});
    

注意,namealterEgo 是兄弟控件。要想在单个自定义验证器中计算这两个控件,你就必须在它们共同的祖先控件中执行验证: FormGroup。你可以在 FormGroup 中查询它的子控件,从而让你能比较它们的值。

Notice that the name and alterEgo are sibling controls. To evaluate both controls in a single custom validator, you must perform the validation in a common ancestor control: the FormGroup. You query the FormGroup for its child controls so that you can compare their values.

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

To add a validator to the FormGroup, pass the new validator in as the second argument on creation.

const heroForm = new FormGroup({ 'name': new FormControl(), 'alterEgo': new FormControl(), 'power': new FormControl() }, { validators: identityRevealedValidator });
      
      const heroForm = new FormGroup({
  'name': new FormControl(),
  'alterEgo': new FormControl(),
  'power': new FormControl()
}, { validators: identityRevealedValidator });
    

验证器的代码如下。

The validator code is as follows.

/** 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; };
shared/identity-revealed.directive.ts
      
      /** 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;
};
    

这个 identity 验证器实现了 ValidatorFn 接口。它接收一个 Angular 表单控件对象作为参数,当表单有效时,它返回一个 null,否则返回 ValidationErrors 对象。

The identity validator implements the ValidatorFn interface. It takes an Angular control object as an argument and returns either null if the form is valid, or ValidationErrors otherwise.

该验证器通过调用 FormGroupget 方法来检索这些子控件,然后比较 namealterEgo 控件的值。

The validator retrieves the child controls by calling the FormGroup's get method, then compares the values of the name and alterEgo controls.

如果值不匹配,则 hero 的身份保持秘密,两者都有效,且 validator 返回 null。如果匹配,就说明英雄的身份已经暴露了,验证器必须通过返回一个错误对象来把这个表单标记为无效的。

If the values do not match, the hero's identity remains secret, both are valid, and the validator returns null. If they do match, the hero's identity is revealed and the validator must mark the form as invalid by returning an error object.

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

To provide better user experience, the template shows an appropriate error message when the form is invalid.

<div *ngIf="heroForm.errors?.identityRevealed && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert alert-danger"> Name cannot match alter ego. </div>
reactive/hero-form-template.component.html
      
      <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 就会显示错误,但只有当该用户已经与表单进行过交互的时候才显示。

This *ngIf displays the error if the FormGroup has the cross validation error returned by the identityRevealed validator, but only if the user has finished interacting with the form.

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

Adding cross-validation to template-driven forms

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

For a template-driven form, you must create a directive to wrap the validator function. You provide that directive as the validator using the NG_VALIDATORS token, as shown in the following example.

@Directive({ selector: '[appIdentityRevealed]', providers: [{ provide: NG_VALIDATORS, useExisting: IdentityRevealedValidatorDirective, multi: true }] }) export class IdentityRevealedValidatorDirective implements Validator { validate(control: AbstractControl): ValidationErrors { return identityRevealedValidator(control) } }
shared/identity-revealed.directive.ts
      
      @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 标签上。

You must add the new directive to the HTML template. Because the validator must be registered at the highest level in the form, the following template puts the directive on the form tag.

<form #heroForm="ngForm" appIdentityRevealed>
template/hero-form-template.component.html
      
      <form #heroForm="ngForm" appIdentityRevealed>
    

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

To provide better user experience, we show an appropriate error message when the form is invalid.

<div *ngIf="heroForm.errors?.identityRevealed && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert alert-danger"> Name cannot match alter ego. </div>
template/hero-form-template.component.html
      
      <div *ngIf="heroForm.errors?.identityRevealed && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert alert-danger">
    Name cannot match alter ego.
</div>
    

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

This is the same in both template-driven and reactive forms.

创建异步验证器

Creating asynchronous validators

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

Asynchronous validators implement the AsyncValidatorFn and AsyncValidator interfaces. These are very similar to their synchronous counterparts, with the following differences.

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

    The validate() functions must return a Promise or an observable,

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

    The observable returned must be finite, meaning it must complete at some point. To convert an infinite observable into a finite one, pipe the observable through a filtering operator such as first, last, take, or takeUntil.

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

Asynchronous validation happens after the synchronous validation, and is performed only if the synchronous validation is successful. This check allows forms to avoid potentially expensive async validation processes (such as an HTTP request) if the more basic validation methods have already found invalid input.

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

After asynchronous validation begins, the form control enters a pending state. You can inspect the control's pending property and use it to give visual feedback about the ongoing validation operation.

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

A common UI pattern is to show a spinner while the async validation is being performed. The following example shows how to achieve this in a template-driven form.

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

实现自定义异步验证器

Implementing a custom async validator

在下面的例子中,异步验证器可以确保英雄们选择了一个尚未采用的第二人格。新英雄不断涌现,老英雄也会离开,所以无法提前找到可用的人格列表。为了验证潜在的第二人格条目,验证器必须启动一个异步操作来查询包含所有在编英雄的中央数据库。

In the following example, an async validator ensures that heroes pick an alter ego that is not already taken. New heroes are constantly enlisting and old heroes are leaving the service, so the list of available alter egos cannot be retrieved ahead of time. To validate the potential alter ego entry, the validator must initiate an asynchronous operation to consult a central database of all currently enlisted heroes.

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

The following code create the validator class, UniqueAlterEgoValidator, which implements the AsyncValidator interface.

@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(() => of(null)) ); } }
      
      @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(() => of(null))
    );
  }
}
    

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

The constructor injects the HeroesService, which defines the following interface.

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

在真实的应用中,HeroesService 会负责向英雄数据库发起一个 HTTP 请求,以检查该第二人格是否可用。 从该验证器的视角看,此服务的具体实现无关紧要,所以这个例子仅仅针对 HeroesService 接口来写实现代码。

In a real world application, the HeroesService would be responsible for making an HTTP request to the hero database to check if the alter ego is available. From the validator's point of view, the actual implementation of the service is not important, so the example can just code against the HeroesService interface.

当验证开始的时候,UniqueAlterEgoValidator 把任务委托给 HeroesServiceisAlterEgoTaken() 方法,并传入当前控件的值。这时候,该控件会被标记为 pending 状态,直到 validate() 方法所返回的可观察对象完成(complete)了。

As the validation begins, the UniqueAlterEgoValidator delegates to the HeroesService isAlterEgoTaken() method with the current control value. At this point the control is marked as pending and remains in this state until the observable chain returned from the validate() method completes.

isAlterEgoTaken() 方法会调度一个 HTTP 请求来检查第二人格是否可用,并返回 Observable<boolean> 作为结果。validate() 方法通过 map 操作符来对响应对象进行管道化处理,并把它转换成验证结果。

The isAlterEgoTaken() method dispatches an HTTP request that checks if the alter ego is available, and returns Observable<boolean> as the result. The validate() method pipes the response through the map operator and transforms it into a validation result.

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

The method then, like any validator, returns null if the form is valid, and ValidationErrors if it is not. This validator handles any potential errors with the catchError operator. In this case, the validator treats the isAlterEgoTaken() error as a successful validation, because failure to make a validation request does not necessarily mean that the alter ego is invalid. You could handle the error differently and return the ValidationError object instead.

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

After some time passes, the observable chain completes and the asynchronous validation is done. The pending flag is set to false, and the form validity is updated.

优化异步验证器的性能

Optimizing performance of async validators

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

By default, all validators run after every form value change. With synchronous validators, this does not normally have a noticeable impact on application performance. Async validators, however, commonly perform some kind of HTTP request to validate the control. Dispatching an HTTP request after every keystroke could put a strain on the backend API, and should be avoided if possible.

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

You can delay updating the form validity by changing the updateOn property from change (default) to submit or blur.

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

With template-driven forms, set the property in the template.

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

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

With reactive forms, set the property in the FormControl instance.

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