angular学习之路14-表单验证

常永长
2023-12-01

1,模板驱动验证

每当表单控件中的值发生变化时,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> 为其中一个可能出现的验证错误显示一条自定义消息。比如 requiredminlength 和 forbiddenName

2,响应式表单的验证

在响应式表单中,权威数据源是其组件类。不应该通过模板上的属性来添加验证器,而应该在组件类中直接把验证器函数添加到表单控件模型上(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 样式或可访问性

3,自定义验证器

由于内置验证器无法适用于所有应用场景,有时候你还是得创建自定义验证器。

考虑前面的例子中的 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),那时候这个表单就会使用它输出的最后一个值作为验证结果。

4,自定义验证器添加进响应式表单

在响应式表单组件中,添加自定义验证器相当简单。你所要做的一切就是直接把这个函数传给 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)  //内置验证器传第二个参数
});

5,自定义验证器添加到模板驱动表单

在模板驱动表单中,你不用直接访问 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 的。

6,表示控件状态的 CSS 类

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

  • .ng-valid

  • .ng-invalid

  • .ng-pending

  • .ng-pristine

  • .ng-dirty

  • .ng-untouched

  • .ng-touched

7,跨字段交叉验证

在下一节中,我们要确保英雄们不能通过填写表单来暴露他们的真实身份。要做到这一点,我们就要验证英雄的名字和他的第二人格(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>

注意,我们需要检查:

  • FormGroup 应该有一个由 identityRevealed 验证器返回的交叉验证错误对象。

  • 用户已经和表单进行过交互

添加到模板驱动表单中

首先,我们必须创建一个指令,它会包装这个验证器函数。我们使用 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 验证器提供的交叉验证错误对象。

  • 用户已经和表单进行过交互

这样就完成了这个交叉验证的例子。我们的做法是:

  • 基于两个相邻控件的值来验证表单

  • 当用户与表单交互过并且验证失败时,才显示一个描述性的错误信息。

8,异步验证

基础

就像同步验证器有 ValidatorFn 和 Validator 接口一样,异步验证器也有自己的对应物:AsyncValidatorFn 和 AsyncValidator

它们非常像,但是有下列不同:

  • 它们必须返回承诺(Promise)或可观察对象(Observable),

  • 返回的可观察对象必须是有限的,也就是说,它必须在某个时间点结束(complete)。要把无尽的可观察对象转换成有限的,可以使用 firstlasttake 或 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,并且表单的有效性也更新了。

 

 类似资料: