Angular 表单验证

宗政元青
2023-12-01

模板驱动验证

使用模板驱动验证需要依赖于原生的HTML表单验证器 Angular 会用指令来匹配具有验证功能的这些属性。

原生的HTMl验证器主要分两种

  1. 通过语义类型来进行定义
  2. 通过验证相关的属性来进行定义

语义类型

Input typeConstraint descriptionAssociated violation
<input type="URL">The value must be an absolute URL, as defined in the URL Living Standard.TypeMismatch constraint violation
<input type="email">The value must be a syntactically valid email address, which generally has the format username@hostname.tld.TypeMismatch constraint violation

 验证相关属性

AttributeInput types supporting the attributePossible valuesConstraint descriptionAssociated violation
patterntext, search, url, tel, email, passwordA JavaScript regular expression (compiled with the ECMAScript 5 global, ignoreCase, and multiline flags disabled)The value must match the pattern.patternMismatch constraint violation
minrange, numberA valid numberThe value must be greater than or equal to the value.rangeUnderflow constraint violation
date, month, weekA valid date
datetime, datetime-local, timeA valid date and time
maxrange, numberA valid numberThe value must be less than or equal to the valuerangeOverflow constraint violation
date, month, weekA valid date
datetime, datetime-local, timeA valid date and time
requiredtext, search, url, tel, email, password, date, datetime, datetime-local, month, week, time, number, checkbox, radio, file; also on the <select> and <textarea> elementsnone as it is a Boolean attribute: its presence means true, its absence means falseThere must be a value (if set).valueMissing constraint violation
stepdateAn integer number of daysUnless the step is set to the any literal, the value must be min + an integral multiple of the step.stepMismatch constraint violation
monthAn integer number of months
weekAn integer number of weeks
datetime, datetime-local, timeAn integer number of seconds
range, numberAn integer
minlengthtext, search, url, tel, email, password; also on the <textarea> elementAn integer lengthThe number of characters (code points) must not be less than the value of the attribute, if non-empty. All newlines are normalized to a single character (as opposed to CRLF pairs) for <textarea>.tooShort constraint violation
maxlengthtext, search, url, tel, email, password; also on the <textarea> elementAn integer lengthThe number of characters (code points) must not exceed the value of the attribute.tooLong constraint violation

 

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

可以通过吧ngModel导出成局部模板变量来查看控件的状态,比如像下面这样

<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>

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

响应式表单的验证

响应式表单控制的源头在组件类,就不能通过模板上的属性来添加验证器了,而是直接在组件类中直接把验证器函数添加到表单控件模型(FormControl)上。当控件发生变化的时候就会调用这些函数。

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)
  });

}
<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 读取器。
  • required 属性仍然存在,虽然验证不再需要它,但你仍然要在模板中保留它,以支持 CSS 样式或可访问性。 

从验证过程上看有两种验证器函数:同步验证器和异步验证器。

  • 同步验证器函数接受一个控件实例,然后返回一组验证错误或null,可以在实例化FormControl的时候把函数作为构造函数的第二个参数传递进去。
  • 异步验证器函数同样也接收一个控件实例,并返回一个承诺(Promise)或可观察对象(Observable),最终函数会返回一组验证错误或者null,可以在实例化FormControl的时候把她当做构造函数的第三个函数传递进去。

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

从验证器来源来看也有两种验证器:内置验证器和自定义验证器

  • 内置验证器与表单中使用的验证器类似,Validators都对应实现了其同名函数,具体如下 详见API
class Validators {
  static min(min: number): ValidatorFn
  static max(max: number): ValidatorFn
  static required(control: AbstractControl): ValidationErrors | null
  static requiredTrue(control: AbstractControl): ValidationErrors | null
  static email(control: AbstractControl): ValidationErrors | null
  static minLength(minLength: number): ValidatorFn
  static maxLength(maxLength: number): ValidatorFn
  static pattern(pattern: string | RegExp): ValidatorFn
  static nullValidator(control: AbstractControl): ValidationErrors | null
  static compose(validators: ValidatorFn[]): ValidatorFn | null
  static composeAsync(validators: AsyncValidatorFn[]): AsyncValidatorFn | null
}  
  • 自定义验证器:DEMO 如下,写在一起就能达到即给模板使用又给组件类使用的目的。
// 声明成指令给模板验证用 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;
  }
}
// 定义函数给验证方法用
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;
  };
}

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

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

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

表示控件状态的CSS类

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

.ng-valid

.ng-invalid

.ng-pending

.ng-pristine

.ng-dirty

.ng-untouched

.ng-touched

.ng-valid[required], .ng-valid.required  {
  border-left: 5px solid #42A948; /* green */
}


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

跨字段交叉验证

除了单独的控件验证之外,有时候还需要多控件的联合验证,这个时候就需要用到跨字段的交叉验证方式。先粘代码

  • 这个身份验证器实现了 ValidatorFn 接口。它接收一个 Angular 表单控件对象作为参数,当表单有效时,它返回一个 null,否则返回 ValidationErrors 对象。
  • 我们先通过调用 FormGroupget 方法来获取子控件。然后,简单地比较一下 namealterEgo 控件的值。
// 先定义指令给模板验证使用,指令调用导出的验证方法
@Directive({
  selector: '[appIdentityRevealed]',
  providers: [{ provide: NG_VALIDATORS, useExisting: IdentityRevealedValidatorDirective, multi: true }]
})
export class IdentityRevealedValidatorDirective implements Validator {
  validate(control: AbstractControl): ValidationErrors {
    return identityRevealedValidator(control)
  }
}
// 下面的方法给指令使用也可以给组件类使用
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;
};

 响应式表单验证:需要在创建FormGroup时把一个新的验证器传给他的第二个参数.

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

 模板驱动表单验证:我们要把该指令添加到 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>

异步验证器

就像同步验证器有 ValidatorFnValidator 接口一样,异步验证器也有自己对应的接口:AsyncValidatorFnAsyncValidator

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

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

  • 返回的可观察对象必须是有限的,也就是说,它必须在某个时间点结束(complete)。要把无尽的可观察对象转换成有限的,可以使用 firstlasttaketakeUntil 等过滤型管道对其进行处理。

注意!异步验证总是会在同步验证之后执行,并且只有当同步验证成功了之后才会执行。如果更基本的验证方法已经失败了,那么这能让表单避免进行可能会很昂贵的异步验证过程,比如 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(() => of(null))
    );
  }
}
// HeroesService 负责向英雄数据库发起一个 HTTP 请求
interface HeroesService {
  isAlterEgoTaken: (alterEgo: string) => Observable<boolean>;
}

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

isAlterEgoTaken() 方法会发出一个 HTTP 请求,并返回一个 Observable<boolean> 型结果。我们通过 map 操作符把响应对象串起来,并把它转换成一个有效性结果。 与往常一样,如果表单有效则返回 null,否则返回 ValidationErrors。我们还是用 catchError 操作符来确保对任何潜在错误都进行了处理。

这里,我们决定将 isAlterEgoTaken() 中的错误视为成功验证。你也可以将其视为失败,并返回 ValidationError 对象。

一段时间之后,可观察对象完成了,异步验证也就结束了。这时候 pending 标志就改成了 false,并且表单的有效性也更新了。

性能上的注意事项

默认情况下,每当表单值变化之后,都会执行所有验证器。对于同步验证器,没有什么会显著影响应用性能的地方。不过,异步验证器通常会执行某种 HTTP 请求来对控件进行验证。如果在每次按键之后都发出 HTTP 请求会给后端 API 带来沉重的负担,应该尽量避免。

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

对于模板驱动表单:

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

对于响应式表单:

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

 

 类似资料: