使用模板驱动验证需要依赖于原生的HTML表单验证器 Angular 会用指令来匹配具有验证功能的这些属性。
原生的HTMl验证器主要分两种
Input type Constraint description Associated 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
Attribute Input types supporting the attribute Possible values Constraint description Associated violation pattern
text
,search
,url
,tel
,password
A JavaScript regular expression (compiled with the ECMAScript 5 global
,ignoreCase
, andmultiline
flags disabled)The value must match the pattern. patternMismatch
constraint violationmin
range
,number
A valid number The value must be greater than or equal to the value. rangeUnderflow
constraint violationdate
,month
,week
A valid date datetime
,datetime-local
,time
A valid date and time max
range
,number
A valid number The value must be less than or equal to the value rangeOverflow
constraint violationdate
,month
,week
A valid date datetime
,datetime-local
,time
A valid date and time required
text
,search
,url
,tel
,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 false There must be a value (if set). valueMissing
constraint violationstep
date
An integer number of days Unless the step is set to the any
literal, the value must be min + an integral multiple of the step.stepMismatch
constraint violationmonth
An integer number of months week
An integer number of weeks datetime
,datetime-local
,time
An integer number of seconds range
,number
An integer minlength
text
,search
,url
,tel
,password
; also on the<textarea>
elementAn integer length The 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 violationmaxlength
text
,search
,url
,tel
,password
; also on the<textarea>
elementAn integer length The 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
实例的属性映射出去,让你能在模板中检查控件的状态,比如 valid
和 dirty
。要了解完整的控件属性,参见 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 样式或可访问性。 从验证过程上看有两种验证器函数:同步验证器和异步验证器。
出于性能方面的考虑,只有在所有同步验证器都通过之后,Angular 才会运行异步验证器。当每一个异步验证器都执行完之后,才会设置这些验证错误。
从验证器来源来看也有两种验证器:内置验证器和自定义验证器
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
}
// 声明成指令给模板验证用 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 方法)
Angular 会自动把很多控件属性作为 CSS 类映射到控件所在的元素上。你可以使用这些类来根据表单状态给表单控件元素添加样式。目前支持下列类:
| .ng-valid[required], .ng-valid.required {
|
除了单独的控件验证之外,有时候还需要多控件的联合验证,这个时候就需要用到跨字段的交叉验证方式。先粘代码
ValidatorFn
接口。它接收一个 Angular 表单控件对象作为参数,当表单有效时,它返回一个 null,否则返回 ValidationErrors
对象。FormGroup
的 get 方法来获取子控件。然后,简单地比较一下 name
和 alterEgo
控件的值。// 先定义指令给模板验证使用,指令调用导出的验证方法
@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>
就像同步验证器有 ValidatorFn
和 Validator
接口一样,异步验证器也有自己对应的接口:AsyncValidatorFn
和 AsyncValidator
。
它们非常像,但是有下列不同:
它们必须返回承诺(Promise)或可观察对象(Observable),
返回的可观察对象必须是有限的,也就是说,它必须在某个时间点结束(complete)。要把无尽的可观察对象转换成有限的,可以使用 first
、last
、take
或 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(() => of(null))
);
}
}
// HeroesService 负责向英雄数据库发起一个 HTTP 请求
interface HeroesService {
isAlterEgoTaken: (alterEgo: string) => Observable<boolean>;
}
当验证开始的时候,UniqueAlterEgoValidator
把任务委托给 HeroesService
的 isAlterEgoTaken()
方法,并传入当前控件的值。这时候,该控件会被标记为 pending
状态,直到 validate()
方法所返回的可观察对象完成(complete)了。
isAlterEgoTaken()
方法会发出一个 HTTP 请求,并返回一个 Observable<boolean>
型结果。我们通过 map
操作符把响应对象串起来,并把它转换成一个有效性结果。 与往常一样,如果表单有效则返回 null
,否则返回 ValidationErrors
。我们还是用 catchError
操作符来确保对任何潜在错误都进行了处理。
这里,我们决定将 isAlterEgoTaken()
中的错误视为成功验证。你也可以将其视为失败,并返回 ValidationError
对象。
一段时间之后,可观察对象完成了,异步验证也就结束了。这时候 pending
标志就改成了 false
,并且表单的有效性也更新了。
默认情况下,每当表单值变化之后,都会执行所有验证器。对于同步验证器,没有什么会显著影响应用性能的地方。不过,异步验证器通常会执行某种 HTTP 请求来对控件进行验证。如果在每次按键之后都发出 HTTP 请求会给后端 API 带来沉重的负担,应该尽量避免。
我们可以把 updateOn
属性从 change
(默认值)改成 submit
或 blur
来推迟表单验证的更新时机。
对于模板驱动表单:
<input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}">
对于响应式表单:
new FormControl('', {updateOn: 'blur'});