Angular
也有两种验证表单的形式:
- 使用属性验证,用于模板驱动表单;
- 使用验证器函数进行验证,用于响应式表单。
验证器(Validator)函数
验证器函数可以是同步函数,也可以是异步函数。
- 同步验证器:接受控件实例,然后返回验证错误信息或
null
。在实例化一个FormControl
时把它作为构造函数的第二个参数传进去; - 异步验证器 :接受实例并返回一个
Promise
或Observable
,稍后会发出一组验证错误或null
。也是在实例化FormControl
时,作为第三个参数传入。
内置验证器函数
Angular
内置了一些基础功能的验证器,在日常开发中可以直接使用:
我们来简单使用一下内置的验证器:
// reactive-forms.component.ts import {Validators} from '@angular/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>
效果是这样的:
可以看出:必填验证一开始都没有通过,验证顺序是跟添加验证器的顺序一致。
上一节最后我们不是介绍过 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>
上面的dirty
是 AbstractControl
类的属性,用于判定控件是否修改过控件的值。
AbstractControl
类还有其他监控用户操作控件的属性:
pristine: boolean
如果用户尚未修改UI
中的值,则该控件是pristine
(原始状态)的。touched: boolean
一旦用户在控件上触发了blur
事件,则会将其标记为touched
。untouched: boolean
如果用户尚未在控件上触发过blur
事件,则该控件为untouched
。
定义自定义验证器
内置的验证器并不是总能适用于我们的需求,因此需要创建自定义验证器。
创建自定义验证器,我们需要遵从 Angular
中的创建规则:
- 验证器函数的返回值必须是
ValidatorFn
类型;ValidatorFn
实际上是一个接口,里面只有一个方法,将一个表单控件传入,返回验证错误信息(ValidationErrors
)或者null
ValidationErrors
是一个key: value
的对象
搞清楚了规则,我们的验证器函数至少应该是这样的:
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 '@angular/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>
跨字段交叉验证
// 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
方法可以接收第二个参数,我们将验证器添加到里面:// 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>
异步验证器
有的时候,用户填写的内容需要从服务器上的数据进行匹配,从而得出验证结果。这个时候,就需要使用异步验证器。
比如用户注册时候,需要验证用户手机号码是否已经使用。我们也将通过这个需求来介绍异步验证器。
// 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>
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>
总结
这一节我们介绍了表单验证大部分知识,其他未提到的在使用时查找官方文档即可。
- 本节主要介绍了响应式表单的验证及模板驱动表单的验证;
- 他们都可以使用内置验证器、自定义验证器、交叉验证器、异步验证器;
- 创建自定义验证器需要遵从
Angular
相关规则,实现对应接口以及返回ValidationErrors
或者null
; - 响应式表单都异步验证器通过服务实现,模版驱动表单的自定义验证器是通过指令实现。
最新评论