Angular10教程–6.1 表单验证

Angular10教程--6.1 表单验证

通常,我们都需要对用户的表单输入做验证,以保证数据的整体质量。

Angular 也有两种验证表单的形式:

  • 使用属性验证,用于模板驱动表单;
  • 使用验证器函数进行验证,用于响应式表单。

验证器()函数

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

  • 同步验证器:接受控件实例,然后返回验证错误信息或 null。在实例化一个 FormControl 时把它作为构造函数的第二个参数传进去;
  • 异步验证器 :接受实例并返回一个 Promise 或 Observable,稍后会发出一组验证错误或 null。也是在实例化 FormControl 时,作为第三个参数传入。

内置验证器函数

Angular 内置了一些基础功能的验证器,在日常开发中可以直接使用:Angular10教程--6.1 表单验证

我们来简单使用一下内置的验证器:

// reactive-forms.component.ts
import {Validators} from '@/forms';
...
profileForm = this.fb.group({
  firstName: ['', Validators.required], // 必填
  lastName: ['',
    [Validators.required, Validators.minLength(4)]
  ], // 必填并且最小长度为4
  ...
});

// 因为我们可能多次获取表单中元素,所以先获取
get firstName() { return this.profileForm.get('firstName'); }
get lastName() { return this.profileForm.get('lastName'); }

 

<!-- reactive-forms.component.html -->
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
  <div class="form-group">
    <label>
      First Name: <input type="text" formControlName="firstName" class="form-control form-control-sm">
    </label>
    <div class="alert alert-danger">
      valid: {{firstName.valid | json}} <br>
      errors: {{firstName.errors | json}}
    </div>
  </div>
  <div class="form-group">
    <label>
      Last Name: <input type="text" formControlName="lastName" class="form-control form-control-sm">
    </label>
    <div class="alert alert-danger">
      valid: {{lastName.valid | json}} <br>
      errors: {{lastName.errors | json}}
    </div>
  </div>
</form>

效果是这样的:

Angular10教程--6.1 表单验证

可以看出:必填验证一开始都没有通过,验证顺序是跟添加验证器的顺序一致。

上一节最后我们不是介绍过 Angular 跟踪控件状态吗,那就可以根据状态去控制错误提示。

在日常开发中,当我们表单是必填项,初始化页面时,我们是不应该提示错误,并且,如果用户聚焦后并没有输入任何值的时候,也是不应该提示错误的。

<!-- reactive-forms.component.html -->
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
  <div class="form-group">
    <label>
      First Name: <input type="text" formControlName="firstName" class="form-control form-control-sm">
    </label>
    <div *ngIf="firstName.dirty && firstName.errors" class="alert alert-danger">
      <span>请填写first name</span>
    </div>
  </div>
  <div class="form-group">
    <label>
      Last Name: <input type="text" formControlName="lastName" class="form-control form-control-sm">
    </label>
    <div *ngIf="lastName.dirty && lastName.errors as errors" class="alert alert-danger">
      <span *ngIf="errors.required">请填写last name</span>
      <span *ngIf="errors.minlength">last name应该至少4个字符</span>
    </div>
  </div>
</form>

Angular10教程--6.1 表单验证

上面的dirty 是 AbstractControl 类的属性,用于判定控件是否修改过控件的值。

AbstractControl 类还有其他监控用户操作控件的属性:

  • pristine: boolean 如果用户尚未修改 UI 中的值,则该控件是 pristine(原始状态)的。
  • touched: boolean 一旦用户在控件上触发了 blur 事件,则会将其标记为 touched
  • untouched: boolean 如果用户尚未在控件上触发过 blur 事件,则该控件为 untouched

定义自定义验证器

内置的验证器并不是总能适用于我们的需求,因此需要创建自定义验证器。

创建自定义验证器,我们需要遵从 Angular 中的创建规则:

  • 验证器函数的返回值必须是 ValidatorFn 类型;ValidatorFn 实际上是一个接口,里面只有一个方法,将一个表单控件传入,返回验证错误信息( ValidationErrors )或者 null

 

Angular10教程--6.1 表单验证

  • ValidationErrors 是一个 key: value 的对象

Angular10教程--6.1 表单验证

搞清楚了规则,我们的验证器函数至少应该是这样的:

function validatorName(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const isValid: boolean;
    return isValid ? null : {validateName: 'error info'};
  };
}

按照上面的样子,我们来写一个通过正则验证字符串中不能包含特定字符的验证器函数(新建validators.ts):

// validators.ts
import {AbstractControl, ValidationErrors, ValidatorFn} from '@/forms';

// 传入一个正则以及错误提示
export function forbiddenNameValidator(reg: RegExp, errorTips: string): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const forbidden = reg.test(control.value);
    return forbidden ? {forbiddenName: {value: errorTips}} : null;
  };
}

添加验证器:

// reactive-forms.component.ts
import {forbiddenNameValidator} from '../validators';
...
lastName: ['',
  [
    Validators.required,
    Validators.minLength(4),
    forbiddenNameValidator(/bob/i, '不能包含\"bob\"')
  ]
],
...

使用自定义验证器:

<!-- reactive-forms.component.html -->
<div class="form-group">
  <label>
    Last Name: <input type="text" formControlName="lastName" class="form-control form-control-sm">
  </label>
  <div *ngIf="lastName.dirty && lastName.errors as errors" class="alert alert-danger">
    <span *ngIf="errors.required">请填写last name</span>
    <span *ngIf="errors.minlength">last name应该至少4个字符</span>
    <span *ngIf="errors.forbiddenName">{{errors.forbiddenName.value}}</span>
  </div>
</div>

Angular10教程--6.1 表单验证

跨字段交叉验证

跨字段交叉验证其实就是对一组字段进行的验证,后面的值依赖于前面的值,实质也是一个自定义验证器。最常见的使用场景就是密码相同验证。
先将密码的基础架子搭起来:
// reactive-forms.component.ts
profileForm = this.fb.group({
  ...
  newPass: this.fb.group({
    password: ['', Validators.required],
    rePassword: ['', [Validators.required]]
  }),
});
get newPass() { return this.profileForm.get('newPass'); }
get password() { return this.profileForm.get('newPass').get('password'); }
get rePassword() { return this.profileForm.get('newPass').get('rePassword'); }
<!-- reactive-forms.component.html -->
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
  ...
  <div formGroupName="newPass">
    <div class="form-group">
      <label>
        输入密码: <input type="text" formControlName="password" class="form-control form-control-sm">
      </label>
      <div *ngIf="password.dirty && password.errors" class="alert alert-danger">
        <span>请填写密码</span>
      </div>
    </div>
    <div class="form-group">
      <label>
        再次输入密码: <input type="text" formControlName="rePassword" class="form-control form-control-sm">
      </label>
      {{rePassword}}
      <div *ngIf="rePassword.dirty && rePassword.errors" class="alert alert-danger">
        <span>请再次填写密码</span>
      </div>
    </div>
  </div>
</form>
自定义密码一致验证器函数:
// validators.ts
export function equalValidator(ctrl: AbstractControl): Validators | null {
  const password = ctrl.get('password');
  const rePassword = ctrl.get('rePassword');
  return password?.value === rePassword?.value ? null : {equal: '两次密码不一致'};
}
使用跨字段交叉验证:
FormBuilder 下面的 group 方法可以接收第二个参数,我们将验证器添加到里面:

Angular10教程--6.1 表单验证

// reactive-forms.component.ts
import {equalValidator} from '../validators';
newPass: this.fb.group({
  password: ['', Validators.required],
  rePassword: ['', [Validators.required]]
}, {validators: equalValidator}),

模板中添加错误提示:

<!-- reactive-forms.component.html -->
<div *ngIf="password.dirty && rePassword.dirty && newPass.errors" class="alert alert-danger">
  <span>{{newPass.errors.equal}}</span>
</div>

Angular10教程--6.1 表单验证

异步验证器

有的时候,用户填写的内容需要从服务器上的数据进行匹配,从而得出验证结果。这个时候,就需要使用异步验证器。

比如用户注册时候,需要验证用户手机号码是否已经使用。我们也将通过这个需求来介绍异步验证器。

先搭好静态的结构:
// reactive-forms.component.ts
profileForm = this.fb.group({
  mobile: ['',
    [
      Validators.required,
      Validators.pattern(/^\d{3}$/) // 这里的正则就简写成3位数字
    ]
  ],
});
get mobile() { return this.profileForm.get('mobile'); }
<!-- reactive-forms.component.html -->
<div *ngIf="password.dirty && rePassword.dirty && newPass.errors" class="alert alert-danger">
  <span>{{newPass.errors.equal}}</span>
</div>
通常情况下,我们获取服务器数据是在一个服务里面,那将异步验证器也放在服务里。
新建一个 hasMobile 服务:
ng g s forms-study/hasMobile
在服务中实现 AsyncValidator 接口:
// has-mobile.service.ts
export class HasMobileService implements AsyncValidator{
  // 我们这里直接模拟一下,实际项目中需要拿到服务器数据进行匹配
  validate(control: AbstractControl): Observable<ValidationErrors | null> {
    const random = Math.random();
    return iif(
      () => random > 0.5,
      of({ exists: '该手机已被注册'}),
      of(null)
    );
  }
}
使用异步验证器:
// reactive-forms.component.ts
constructor(private fb: FormBuilder, private hasMobileServer: HasMobileService) { }
profileForm = this.fb.group({
  mobile: ['',
    [
      Validators.required,
      Validators.pattern(/^\d{3}$/) // 这里的正则就简写成3位数字
    ],
    this.hasMobileServer.validate // 第三个参数接收一个异步验证器
  ],
});
get mobile() { return this.profileForm.get('mobile'); }
<!-- reactive-forms.component.html -->
<div class="form-group">
  <label>
    手机号: <input type="text" formControlName="mobile" class="form-control form-control-sm">
  </label>
  <div *ngIf="mobile.dirty && mobile.errors as errors" class="alert alert-danger">
    <span *ngIf="errors.required">请填写手机号</span>
    <span *ngIf="errors.pattern">手机号格式不正确</span>
    <span *ngIf="errors.exists">{{errors.exists}}</span>
  </div>
</div>
Angular10教程--6.1 表单验证出于性能方面的考虑,只有在所有同步验证器都通过之后,Angular 才会运行异步验证器。

Angular 默认在输入框值发生变化时进行校验。如果我们的验证器中有异步验证器,每次变化都会向服务器发送请求,这是一个很消耗性能的操作。所以我们可以修改触发验证的时机为失去焦点时。
// reactive-forms.component.ts
mobile: ['', {
  validators: [
    Validators.required,
    Validators.pattern(/^\d{3}$/) // 这里的正则就简写成3位数字
  ],
  asyncValidators: this.hasMobileServer.validate,
  updateOn: 'blur'
}],

属性验证器

顾名思义,属性验证器是使用属性的形式来进行验证。同样也有内置验证器与自定义验证器。我们将在上一节的模版驱动组件中演示属性验证器。

内置验证器

同样的,我们还是限定 firstName 为必填项目,lastName 为必填且最小长度为4的验证规则。

修改模版文件,给需要验证的控件添加必要的属性并拷贝前面的错误提示:
<!-- template-forms.component.html -->
<div class="form-group" >
  <label>
    <!-- 添加规则 required: 必填  -->
    First Name: <input type="text" required [(ngModel)]="model.firstName" name="firstName" class="form-control form-control-sm">
  </label>
  <!-- 拷贝的是下面的内容 -->
  <div *ngIf="firstName.dirty && firstName.errors" class="alert alert-danger">
    <span>请填写first name</span>
  </div>
</div>
<div class="form-group">
  <label>
    <!-- 添加规则 required: 必填 minlength="4" -->
    Last Name: <input type="text" required minlength="4" [(ngModel)]="model.lastName" name="lastName" required class="form-control form-control-sm">
  </label>
  <!-- 拷贝的是下面的内容 -->
  <div *ngIf="lastName.dirty && lastName.errors as errors" class="alert alert-danger">
    <span *ngIf="errors.required">请填写last name</span>
    <span *ngIf="errors.minlength">last name应该至少4个字符</span>
  </div>
</div>
这样,很显然是会报错,因为根本就没有 firstName 、 lastName 对象。 想要跟响应式表单一样使用 FormControl 对象,我们还需要在模版控件上把 ngModel 导出成局部模板变量:
<!-- template-forms.component.html -->
...
<label>First Name:
  <input
    type="text"
    required
    [(ngModel)]="model.firstName"
    #firstName="ngModel"
    name="firstName"
    class="form-control form-control-sm">
</label>
...
<label>Last Name:
  <input 
    type="text" 
    required
    minlength="4"
    [(ngModel)]="model.lastName"
    #lastName="ngModel"
    name="lastName"
    class="form-control form-control-sm">
</label>
...
具体效果跟前面没任何差别,所以就不用图示范了。

自定义验证器

在模板驱动表单中,要使用自定义验证器,就要为模板添加一个指令,该指令包含了 validator 函数。新建 ForbiddenValidator 指令:

ng g d forms-study/ForbiddenValidator

 

同样的,需要实现 Validator 接口:

// forbidden-validator.directive.ts
import {Directive, Input} from '@angular/core';
import {AbstractControl, ValidationErrors, Validator} from '@angular/forms';
import {forbiddenNameValidator} from './validators';

@Directive({
  // 选择器修改成与输入属性一致
  selector: '[appForbiddenName]',
  // 把自己注册成了 NG_VALIDATORS 提供者,提供同步验证器。 multi表示一个令牌可以提供多个服务
  providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
})
export class ForbiddenValidatorDirective implements Validator {
  // 定义输入属性,正则表达式  
  @Input('appForbiddenName') forbiddenName: string;
  // 错误提示
  @Input('appForbiddenNameTips') forbiddenNameTips: string;
  // 实现 validate 方法
  validate(control: AbstractControl): ValidationErrors | null {
    return this.forbiddenName ?
      forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'), this.forbiddenNameTips)(control) :
      null;
  }
}

 

使用自定义验证器:

<!-- template-forms.component.html -->
...
<div class="form-group">
  <label>Last Name:
    <input type="text"
           required
           minlength="4"
           [(ngModel)]="model.lastName"
           #lastName="ngModel"
           appForbiddenName="bob"
           appForbiddenNameTips="不能包含bob"
           name="lastName"
           class="form-control form-control-sm">
  </label>
  <div *ngIf="lastName.dirty && lastName.errors as errors" class="alert alert-danger">
    ...
    <span *ngIf="errors.forbiddenName">{{errors.forbiddenName.value}}</span>
  </div>
</div>

 

这样,我们就实现了跟上面一致效果的自定义验证器。

跨字段交叉验证

我们同样使用密码的例子进行示范。修改代码:

<!-- template-forms.component.html -->
<div class="form-group">
  <label>
    输入密码: <input type="text" required [(ngModel)]="model.password" #password="ngModel" name="password" class="form-control form-control-sm">
  </label>
  <div *ngIf="password.dirty && password.errors" class="alert alert-danger">
    <span>请填写密码</span>
  </div>
</div> 
<div class="form-group">
  <label>
    再次输入密码: <input type="text" required [(ngModel)]="model.rePassword" #rePassword="ngModel" name="rePassword" class="form-control form-control-sm">
  </label>
  <div *ngIf="rePassword.dirty && rePassword.errors" class="alert alert-danger">
    <span>请填写密码</span>
  </div>
</div>

 

前面已经说过,交叉验证也是一种自定义验证器,所以,我们还是需要新建一个指令:

ng g d forms-study/PsdEqualValidator

同样需要实现 Validator 接口:

// psd-equal-validator.directive.ts
import { Directive } from '@angular/core';
import {AbstractControl, NG_VALIDATORS, ValidationErrors, Validator} from '@angular/forms';
import {equalValidator} from './validators';

@Directive({
  selector: '[appPsdEqualValidator]',
  providers: [{provide: NG_VALIDATORS, useExisting: PsdEqualValidatorDirective, multi: true}]
})
export class PsdEqualValidatorDirective implements Validator{
  validate(control: AbstractControl): ValidationErrors | null {
    return equalValidator(control);
  }
}

因为模版驱动这里没有表单嵌套一说,所以我们需要在顶级表单中使用交叉验证器:

<!-- template-forms.component.html -->
<form #profileForm="ngForm" appPsdEqualValidator (ngSubmit)="onSubmit(profileForm)">
  ...
  <div *ngIf="password.dirty && rePassword.dirty && profileForm.errors" class="alert alert-danger">
    <span>{{profileForm.errors.equal}}</span>
  </div>
</form>

异步验证器

同理,创建一个 hasMobile 指令并实现 AsyncValidator 接口:

// has-mobile-validator.directive.ts
import { Directive } from '@angular/core';
import {AbstractControl, AsyncValidator, NG_ASYNC_VALIDATORS, ValidationErrors} from '@angular/forms';
import {Observable} from 'rxjs';
import {HasMobileService} from './has-mobile.service';

@Directive({
  selector: '[appHasMobileValidator]',
  // 注意:这里是 NG_ASYNC_VALIDATORS
  providers: [{provide: NG_ASYNC_VALIDATORS, useExisting: HasMobileValidatorDirective, multi: true}]
})
export class HasMobileValidatorDirective implements AsyncValidator{
  constructor(private hasMobileServer: HasMobileService) { }
  validate(control: AbstractControl): Observable<ValidationErrors | null> {
    return this.hasMobileServer.validate(control);
  }
}

使用验证器,并设置验证时机为失去焦点时触发:

<!-- template-forms.component.html -->
<div class="form-group">
  <label>
    手机号: <input
    type="text"
    required
    pattern="\d{3}"
    [(ngModel)]="model.mobile"
    [ngModelOptions]="{updateOn: 'blur'}"
    #mobile="ngModel"
    appHasMobileValidator
    name="mobile"
    class="form-control form-control-sm">
  </label>
  <div *ngIf="mobile.dirty && mobile.errors as errors" class="alert alert-danger">
    <span *ngIf="errors.required">请填写手机号</span>
    <span *ngIf="errors.pattern">手机号格式不正确</span>
    <span *ngIf="errors.exists">{{errors.exists}}</span>
  </div>
</div>

总结

这一节我们介绍了大部分知识,其他未提到的在使用时查找官方文档即可。

  1. 本节主要介绍了响应式表单的验证及模板驱动表单的验证;
  2. 他们都可以使用内置验证器、自定义验证器、交叉验证器、异步验证器;
  3. 创建自定义验证器需要遵从 Angular 相关规则,实现对应接口以及返回 ValidationErrors 或者 null;
  4. 响应式表单都异步验证器通过服务实现,模版驱动表单的自定义验证器是通过指令实现。