表单及客户端验证¶
要在 AppComponent 和 HeroDetailComponent 的模板中使用 Bootstrap 中的 CSS 类。请把 bootstrap 的CSS 样式表文件添加到 style.css 的头部:
1 | @import url('https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css'); |
一、表单¶
1.1 模板驱动表单¶
创建初始 HTML 表单模板¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <div class="container"> <h1>Hero Form</h1> <form> <div class="form-group"> <label for="name">Name</label> <input type="text" class="form-control" id="name" required> </div> <div class="form-group"> <label for="alterEgo">Alter Ego</label> <input type="text" class="form-control" id="alterEgo"> </div> <button type="submit" class="btn btn-success">Submit</button> </form> </div> |
使用 ngModel 进行双向数据绑定¶
input 内容变动时,所绑定的对象属性也随之变动
1 2 3 4 | <input type="text" class="form-control" id="name" required [(ngModel)]="model.name" name="name"> TODO: remove this: {{model.name}} |
NgForm 指令¶
1 | <form #heroForm="ngForm"> |
heroForm 变量是一个到 NgForm 指令的引用,它代表该表单的整体;
NgForm 指令为 form 增补了一些额外特性。 它会控制那些带有 ngModel 指令和 name 属性的元素,监听他们的属性(包括其有效性)。 它还有自己的 valid 属性,这个属性只有在它包含的每个控件都有效时才是真。
通过 ngModel 跟踪修改状态与有效性验证¶
NgModel 指令不仅仅跟踪状态。它还使用特定的 Angular CSS 类来更新控件,以反映当前状态。 可以利用这些 CSS 类来修改控件的外观,显示或隐藏消息。 添加样式
1 2 3 4 5 6 7 | .ng-valid[required], .ng-valid.required { border-left: 5px solid #42A948; /* green */ } .ng-invalid:not(form) { border-left: 5px solid #a94442; /* red */ } |
修改页面
1 2 3 4 5 6 7 8 9 | <label for="name">Name</label> <input type="text" class="form-control" id="name" required [(ngModel)]="model.name" name="name" #name="ngModel"> <div [hidden]="name.valid || name.pristine" class="alert alert-danger"> Name is required </div> |
当控件是有效的 (valid) 或全新的 (pristine) 时,隐藏消息。

使用 ngSubmit 提交该表单¶
在填表完成之后,用户还应该能提交这个表单。 “Submit(提交)”按钮位于表单的底部,它自己不做任何事,要让它有用,就要把该表单的 ngSubmit 事件属性绑定到英雄表单组件的 onSubmit() 方法上:
1 2 3 | <form (ngSubmit)="onSubmit()" #heroForm="ngForm"> <button type="submit" class="btn btn-success" [disabled]="!heroForm.form.valid">Submit</button> |
就会违反“必填”规则。同时,Submit 按钮也被禁用了。
1.2响应式表单¶
这种编程风格更倾向于在非 UI 的数据模型(通常接收自服务器)之间显式的管理数据流, 并且用一个 UI 导向的表单模型来保存屏幕上 HTML 控件的状态和值。
响应式表单可以让使用响应式编程模式、测试和校验变得更容易。在响应式编程范式中,组件会负责维护数据模型的不可变性,把模型当做纯粹的原始数据源。 组件不会直接更新数据模型,而是把用户的修改提取出来,把它们转发给外部的组件或服务,外部程序才会使用这些进行处理(比如保存它们), 并且给组件返回一个新的数据模型,以反映模型状态的变化。
1.2.1简单的表单类¶
| CSS 类 | 说明 |
|---|---|
| AbstractControl | AbstractControl是这三个具体表单类的抽象基类。 并为它们提供了一些共同的行为和属性。 |
| FormControl | FormControl 用于跟踪一个单独的表单控件的值和有效性状态。它对应于一个 HTML 表单控件,比如 <input> 或 <select>。 |
| FormGroup | FormGroup用于 跟踪一组AbstractControl 的实例的值和有效性状态。 该组的属性中包含了它的子控件。 组件中的顶级表单就是一个 FormGroup。 |
| FormArray | FormArray用于跟踪 AbstractControl 实例组成的有序数组的值和有效性状态。 |
①FormControl 它允许你直接创建并管理一个 FormControl 实例¶
1 | import { FormControl } from '@angular/forms'; |
component类(创建了一个名叫 name 的 FormControl。 它将会绑定到模板中的一个 元素):
1 2 3 | export class HeroDetailComponent1 { name = new FormControl(); } |
模板文件(就要在模板中的 <input> 上加一句 [formControl]="name"):
1 2 3 4 5 | <h2>Hero Detail</h2> <h3><i>Just a FormControl</i></h3> <label class="center-block">Name: <input class="form-control" [formControl]="name"> </label> |
②FormGroup 如果有多个 FormControl,你要把它们都注册进一个父 FormGroup 中¶
1 | import { FormControl, FormGroup } from '@angular/forms'; |
component类(把 FormControl 包裹进了一个名叫 heroForm 的 FormGroup 中):
1 2 3 4 5 | export class HeroDetailComponent2 { heroForm = new FormGroup ({ name: new FormControl() }); } |
模板文件:
1 2 3 4 5 6 7 8 9 | <h2>Hero Detail</h2> <h3><i>FormControl in a FormGroup</i></h3> <form [formGroup]="heroForm"> <div class="form-group"> <label class="center-block">Name: <input class="form-control" formControlName="name"> </label> </div> </form> |
没有父 FormGroup 的时候,[formControl]="name" 也能正常工作
有了 FormGroup,name 这个 <input> 就需要再添加一个语法 formControlName=name,以便让它关联到类中正确的 FormControl 上。
这个语法告诉 Angular,查阅父 FormGroup(这里是 heroForm),然后在这个 FormGroup 中查阅一个名叫 name 的 FormControl。
1.2.2 FormBuilder¶
FormBuilder.group 是一个用来创建 FormGroup 的工厂方法,可以让你的代码更加紧凑、易读。 因为你不必写一系列重复的 new FormControl(...) 语句。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | export class HeroDetailComponent4 { heroForm: FormGroup; states = states; constructor(private fb: FormBuilder) { this.createForm(); } createForm() { this.heroForm = this.fb.group({ name: ['', Validators.required ], street: '', city: '', state: '', zip: '', power: '', sidekick: '' }); } } |
1.2.3 多级 FormGroup¶
要想更有效的管理这个表单的大小,你可以把一些相关的 FormControl 组织到多级 FormGroup 中
componenet类(用 FormBuilder 在这个名叫 heroForm 的组件中创建一个 FormGroup,并把它用作父 FormGroup。 再次使用 FormBuilder 创建一个子级 FormGroup):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | export class HeroDetailComponent5 { heroForm: FormGroup; states = states; constructor(private fb: FormBuilder) { this.createForm(); } createForm() { this.heroForm = this.fb.group({ // <-- the parent FormGroup name: ['', Validators.required ], address: this.fb.group({ // <-- the child FormGroup street: '', city: '', state: '', zip: '' }), power: '', sidekick: '' }); } } |
组件(添加一个 formGroupName 指令):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <div formGroupName="address" class="well well-lg"> <h4>Secret Lair</h4> <div class="form-group"> <label class="center-block">Street: <input class="form-control" formControlName="street"> </label> </div> <div class="form-group"> <label class="center-block">City: <input class="form-control" formControlName="city"> </label> </div> <div class="form-group"> <label class="center-block">State: <select class="form-control" formControlName="state"> <option *ngFor="let state of states" [value]="state">{{state}}</option> </select> </label> </div> <div class="form-group"> <label class="center-block">Zip Code: <input class="form-control" formControlName="zip"> </label> </div> </div> |
1.2.4 查看 FormControl 的属性¶
.get() 方法来提取表单中一个单独 FormControl 的状态
1 2 | <p>Name value: {{ heroForm.get('name').value }}</p> <p>Street value: {{ heroForm.get('address.street').value}}</p> |
1.2.5 使用 FormArray 来表示 FormGroup 数组¶
FormGroup 是一个命名对象,它的属性值是 FormControl 和其它的 FormGroup。
要使用 FormArray,就要这么做¶
①在数组中定义条目 FormControl 或 FormGroup。 ②把这个数组初始化微一组从数据模型中的数据创建的条目。 ③根据用户的需求添加或移除这些条目。
使用FormArray
1 2 3 4 5 6 | this.heroForm = this.fb.group({ name: ['', Validators.required ], secretLairs: this.fb.array([]), // <-- secretLairs as an empty FormArray power: '', sidekick: '' }); |
初始化FormArray(secretLairs):
1 2 3 4 5 6 7 | setAddresses(addresses: Address[]) { const addressFGs = addresses.map(address => this.fb.group(address)); const addressFormArray = this.fb.array(addressFGs); this.heroForm.setControl('secretLairs', addressFormArray); } //使用 FormGroup.setControl() 方法,而不是 setValue() 方法来替换前一个 FormArray。 你所要替换的是控件,而不是控件的值。 //secretLairs 数组中包含的是**FormGroup,而不是 Address |
获取 FormArray
1 2 3 | get secretLairs(): FormArray { return this.heroForm.get('secretLairs') as FormArray; }; |
显示 FormArray
①formArrayName 指令设为 "secretLairs"。 这一步为内部的表单控件建立了一个 FormArray 型的 secretLairs 作为上下文;
②这些重复条目的数据源是 FormArray.controls 而不是 FormArray 本身;
③每个被重复渲染的 FormGroup 都需要一个独一无二的 formGroupName,它必须是 FormGroup 在这个 FormArray 中的索引。 你将复用这个索引,以便为每个地址组合出一个独一无二的标签。
1 2 3 4 5 | <div formArrayName="secretLairs" class="well well-lg"> <div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" > <!-- The repeated address template --> </div> </div> |
添加到 FormArray 中
1 2 3 | addLair() { this.secretLairs.push(this.fb.group(new Address())); } |
二、客户端验证¶
2.1 模板驱动验证¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <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> |
<input> 元素带有一些 HTML 验证属性:required 和 minlength。它还带有一个自定义的验证器指令 forbiddenName。
#name="ngModel" 把 NgModel 导出成了一个名叫 name 的局部变量。NgModel 把自己控制的 FormControl 实例的属性映射出去,让你能在模板中检查控件的状态,比如 valid 和 dirty。
<div> 元素的 *ngIf 揭露了一套嵌套消息 divs,但是只在有“name”错误和控制器为 dirty 或者 touched。
每个嵌套的 <div> 为其中一个可能出现的验证错误显示一条自定义消息。比如 required、minlength 和 forbiddenName。
2.2 响应式表单的验证¶
在响应式表单中,真正的源码都在组件类中。不应该通过模板上的属性来添加验证器,而应该在组件类中直接把验证器函数添加到表单控件模型上(FormControl)。然后,一旦控件发生了变化,Angular 就会调用这些函数。
2.3 验证器函数¶
有两种验证器函数:同步验证器和异步验证器。
①同步验证器函数接受一个控件实例,然后返回一组验证错误或 null。你可以在实例化一个 FormControl 时把它作为构造函数的第二个参数传进去。
②异步验证器函数接受一个控件实例,并返回一个承诺(Promise)或可观察对象(Observable),它们稍后会发出一组验证错误或者 null。你可以在实例化一个 FormControl 时把它作为构造函数的第三个参数传进去。
2.2.2内置验证器¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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) }); } get name() { return this.heroForm.get('name'); } get power() { return this.heroForm.get('power'); } |
name 控件设置了两个内置验证器:Validators.required 和 Validators.minLength(4);
由于这些验证器都是同步验证器,因此你要把它们作为第二个参数传进去;
可以通过把这些函数放进一个数组后传进去,可以支持多重验证器
2.2.3 自定义验证器¶
前面的例子中的 forbiddenNameValidator 函数:
1 2 3 4 5 6 | export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn { return (control: AbstractControl): {[key: string]: any} => { const forbidden = nameRe.test(control.value); return forbidden ? {'forbiddenName': {value: control.value}} : null; }; } |
添加响应式表单¶
添加自定义验证器相当简单。你所要做的一切就是直接把这个函数传给 FormControl
1 2 3 4 5 6 7 8 9 | 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) }); |
添加到模板驱动表单¶
Angular 在验证流程中的识别出指令的作用,是因为指令把自己注册到了 NG_VALIDATORS 提供商中,该提供商拥有一组可扩展的验证器
1 | providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}] |
然后该指令类实现了 Validator 接口,以便它能简单的与 Angular 表单集成在一起
1 2 3 4 5 6 7 8 9 10 11 12 | @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} { return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control) : null; } } |
一旦 ForbiddenValidatorDirective 写好了,你只要把 forbiddenName 选择器添加到输入框上就可以激活这个验证器了
1 2 3 | <input id="name" name="name" class="form-control" required minlength="4" appForbiddenName="bob" [(ngModel)]="hero.name" #name="ngModel" > |
自定义验证器指令是用 useExisting 而不是 useClass 来实例化的。注册的验证器必须是这个 ForbiddenValidatorDirective 实例本身,也就是表单中 forbiddenName 属性被绑定到了"bob"的那个。如果用 useClass 来代替 useExisting,就会注册一个新的类实例,而它是没有 forbiddenName 的。