一般,若是有多个FormControl,咱们会但愿把它们注册进一个父FormGroup中:html
src/app/hero-detail.component.ts import { Component } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; export class HeroDetailComponent2 { heroForm = new FormGroup ({ name: new FormControl() }); }
如今咱们改完了这个类,该把它映射到模板中了,把hero-detail.component.html改为这样:react
src/app/hero-detail.component.html <h2>Hero Detail</h2> <h3><i>FormControl in a FormGroup</i></h3> <form [formGroup]="heroForm" novalidate> <div class="form-group"> <label class="center-block">Name: <input class="form-control" formControlName="name"> </label> </div> </form>
要想知道表单模型是什么样的,请在hero-detail.component.html的form标签紧后面添加以下代码:正则表达式
src/app/hero-detail.component.html content_copy <p>Form value: {{ heroForm.value | json }}</p> <p>Form status: {{ heroForm.status | json }}</p>
如今,咱们遵循下列步骤用FormBuilder来把HeroDetailComponent重构得更加容易读写:express
src/app/hero-detail.component.ts import { Component } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; export class HeroDetailComponent3 { heroForm: FormGroup; // <--- heroForm is of type FormGroup constructor(private fb: FormBuilder) { // <--- inject FormBuilder this.createForm(); } createForm() { this.heroForm = this.fb.group({ name: '', // <--- the FormControl called "name" }); } }
FormBuilder.group是一个用来建立FormGroup的工厂方法,它接受一个对象,对象的键和值分别是FormControl的名字和它的定义。 在这个例子中,name控件的初始值是空字符串。json
要想让name这个FormControl是必须的,请把FormGroup中的name属性改成一个数组。第一个条目是name的初始值,第二个是required验证器:Validators.required。数组
src/app/hero-detail.component.ts this.heroForm = this.fb.group({ name: ['', Validators.required ], });
用FormBuilder在这个名叫heroForm的组件中建立一个FormGroup,并把它用做父FormGroup。 再次使用FormBuilder建立一个子级FormGroup,其中包括这些住址控件。把结果赋值给父FormGroup中新的address属性:浏览器
src/app/hero-detail.component.ts (excerpt) 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: '' }); } }
在hero-detail.component.html中,把与住址有关的FormControl包裹进一个div中。 往这个div上添加一个formGroupName指令,而且把它绑定到"address"上。 这个address属性是一个FormGroup,它的父FormGroup就是heroForm:服务器
src/app/hero-detail.component.html (address) <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>
可使用.get()方法来提取表单中一个单独FormControl的状态。 咱们能够在组件类中这么作,或者经过往模板中添加下列代码来把它显示在页面中,就添加在{{form.value | json}}插值表达式的紧后面:app
src/app/hero-detail.component.html <p>Name value: {{ heroForm.get('name').value }}</p> <p>Street value: {{ heroForm.get('address.street').value}}</p>
来自服务器的hero就是数据模型,而FormControl的结构就是表单模型。异步
组件必须把数据模型中的英雄值复制到表单模型中,这里隐含着两个很是重要的点:
一般只会展示数据模型的一个子集,表单模型的形态越接近数据模型,事情就会越简单,在HeroDetailComponent中,这两个模型是很是接近的,data-model.ts中的Hero定义:
export class Hero { id = 0; name = ''; addresses: Address[]; } export class Address { street = ''; city = ''; state = ''; zip = ''; }
组件的FormGroup定义:
src/app/hero-detail.component.ts this.heroForm = this.fb.group({ name: ['', Validators.required ], address: this.fb.group({ street: '', city: '', state: '', zip: '' }), power: '', sidekick: '' });
在这些模型中有两点显著的差别:
src/app/hero-detail.component.ts this.heroForm = this.fb.group({ name: ['', Validators.required ], address: this.fb.group(new Address()), // <-- a FormGroup with a new address power: '', sidekick: '' });
借助setValue,咱们能够当即设置每一个表单控件的值,只要把与表单模型的属性精确匹配的数据模型传进去就能够了
src/app/hero-detail.component.ts this.heroForm.setValue({ name: this.hero.name, address: this.hero.addresses[0] || new Address() });
setValue方法会在赋值给任何表单控件以前先检查数据对象的值。
它不会接受一个与FormGroup结构不一样或缺乏表单组中任何一个控件的数据对象。 这种方式下,若是咱们有什么拼写错误或控件嵌套的不正确,它就能返回一些有用的错误信息。 patchValue会默默地失败。
而setValue会捕获错误,并清晰的报告它。
注意,你几乎能够把这个hero用做setValue的参数,由于它的形态与组件的FormGroup结构是很是像的。
咱们如今只能显示英雄的第一个住址,不过咱们还必须考虑hero彻底没有住址的可能性。 下面的例子解释了如何在数据对象参数中对address属性进行有条件的设置:
address: this.hero.addresses[0] || new Address()
借助patchValue,咱们能够经过提供一个只包含要更新的控件的键值对象来把值赋给FormGroup中的指定控件,这个例子只会设置表单的name控件:
this.heroForm.patchValue({ name: this.hero.name });
借助patchValue,咱们能够更灵活地解决数据模型和表单模型之间的差别。 可是和setValue不一样,patchValue不会检查缺失的控件值,而且不会抛出有用的错误信息。
何时设置表单的模型值取决于组件什么时候获得数据模型的值
HeroListComponent组件把英雄的名字显示给用户,当用户点击一个英雄时,列表组件把所选的英雄经过输入属性hero传给HeroDetailComponent:
hero-list.component.html (simplified) <nav> <a *ngFor="let hero of heroes | async" (click)="select(hero)">{{hero.name}}</a> </nav> <div *ngIf="selectedHero"> <app-hero-detail [hero]="selectedHero"></app-hero-detail> </div>
这种方式下,每当用户选择一个新英雄时,HeroDetailComponent中的hero值就会发生变化。 咱们能够在ngOnChanges钩子中调用setValue,就像例子中所演示的那样, 每当输入属性hero发生变化时,Angular就会调用它。
src/app/hero-detail.component.ts (ngOnchanges) ngOnChanges() this.heroForm.setValue({ name: this.hero.name, address: this.hero.addresses[0] || new Address() }); }
咱们应该在更换英雄的时候重置表单,以便来自前一个英雄的控件值被清除,而且其状态被恢复为pristine(原始)状态。 咱们能够在ngOnChanges的顶部调用reset,就像这样:
src/app/hero-detail-7.component.ts this.heroForm.reset();
reset方法有一个可选的state值,让咱们能在重置状态的同时顺便设置控件的值。 在内部实现上,reset会把该参数传给了setValue。 略微重构以后,ngOnChanges会变成这样:
src/app/hero-detail.component.ts (ngOnchanges - revised) ngOnChanges() { this.heroForm.reset({ name: this.hero.name, address: this.hero.addresses[0] || new Address() }); }
src/app/hero-detail.component.ts this.heroForm = this.fb.group({ name: ['', Validators.required ], secretLairs: this.fb.array([]), // <-- secretLairs as an empty FormArray power: '', sidekick: '' });
把表单的控件名从address改成secretLairs让咱们遇到了一个重要问题:表单模型与数据模型再也不匹配了。
### 初始化FormArray型的secretLairs
下面的setAddresses方法把secretLairs数组替换为一个新的FormArray,使用一组表示英雄地址的FormGroup来进行初始化:
src/app/hero-detail.component.ts 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。 咱们所要替换的是控件,而不是控件的值。
### 获取FormArray
使用FormGroup.get方法来获取到FormArray的引用:
get secretLairs(): FormArray { return this.heroForm.get('secretLairs') as FormArray; };
### 显示FormArray
诀窍在于要知道如何编写*ngFor。主要有三点:
src/app/hero-detail.component.html (excerpt) <div formArrayName="secretLairs" class="well well-lg"> <div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" > <!-- The repeated address template --> <h4>Address #{{i + 1}}</h4> <div style="margin-left: 1em;"> <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> <br> <!-- End of the repeated address template --> </div> </div>
### 把新的address添加到FormArray中
addLair() { this.heroForm.get('secretLairs').push(this.fb.group(new Address())); }
每当用户在父组件HeroListComponent中选取了一个英雄,Angular就会调用一次ngOnChanges。 选取英雄会修改输入属性HeroDetailComponent.hero。
当用户修改英雄的名字或秘密小屋时,Angular并不会调用ngOnChanges。 幸运的是,咱们能够经过订阅表单控件的属性之一来了解这些变化,此属性会发出变动通知。
有一些属性,好比valueChanges,能够返回一个RxJS的Observable对象。 要监听控件值的变化,咱们并不须要对RxJS的Observable了解更多。
添加下列方法,以监听姓名这个FormControl中值的变化:
src/app/hero-detail.component.ts (logNameChange) nameChangeLog: string[] = []; logNameChange() { const nameControl = this.heroForm.get('name'); nameControl.valueChanges.forEach( (value: string) => this.nameChangeLog.push(value) ); }
在构造函数中调用它,就在建立表单的代码以后:
src/app/hero-detail-8.component.ts constructor(private fb: FormBuilder) { this.createForm(); this.logNameChange(); }
### 保存
当用户提交表单时,HeroDetailComponent会把英雄实例的数据模型传给所注入进来的HeroService的一个方法来进行保存:
src/app/hero-detail.component.ts (onSubmit) onSubmit() { this.hero = this.prepareSaveHero(); this.heroService.updateHero(this.hero).subscribe(/* error handling */); this.ngOnChanges(); }
原始的hero中有一些保存以前的值,用户的修改仍然是在表单模型中。 因此咱们要根据原始英雄(根据hero.id找到它)的值组合出一个新的hero对象,并用prepareSaveHero助手来深层复制变化后的模型值。
src/app/hero-detail.component.ts (prepareSaveHero) prepareSaveHero(): Hero { const formModel = this.heroForm.value; // deep copy of form model lairs const secretLairsDeepCopy: Address[] = formModel.secretLairs.map( (address: Address) => Object.assign({}, address) ); // return new `Hero` object containing a combination of original hero value(s) // and deep copies of changed form model values const saveHero: Hero = { id: this.hero.id, name: formModel.name as string, // addresses: formModel.secretLairs // <-- bad! addresses: secretLairsDeepCopy }; return saveHero; }
地址的深层复制
咱们已经把formModel.secretLairs赋值给了saveHero.addresses(参见注释掉的部分), saveHero.addresses数组中的地址和formModel.secretLairs中的会是同一个对象。 用户随后对小屋所在街道的修改将会改变saveHero中的街道地址。
但prepareSaveHero方法会制做表单模型中的secretLairs对象的复本,所以实际上并无修改原有对象。
### 丢弃(撤销修改)
丢弃很容易。只要从新执行ngOnChanges方法就能够拆而,它会从新从原始的、未修改过的hero数据模型来构建出表单模型:
src/app/hero-detail.component.ts (revert) revert() { this.ngOnChanges(); }
在响应式表单中,真正的源码都在组件类中。咱们不该该经过模板上的属性来添加验证器,而应该在组件类中直接把验证器函数添加到表单控件模型上(FormControl)。而后,一旦控件发生了变化,Angular 就会调用这些函数。
有两种验证器函数:同步验证器和异步验证器。
注意:出于性能方面的考虑,只有在全部同步验证器都经过以后,Angular 才会运行异步验证器。当每个异步验证器都执行完以后,才会设置这些验证错误。
模板驱动表单中可用的那些属性型验证器(如required、minlength等)对应于Validators类中的同名函数。要想查看内置验证器的全列表,参见 API 参考手册中的Validators部分。
reactive/hero-form-reactive.component.ts (validator functions) 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'); }
注意
reactive/hero-form-reactive.component.html (name with error msg) <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>
shared/forbidden-name.directive.ts (forbiddenNameValidator) /** A hero's name can't match the given regular expression */ 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; }; }
这个函数其实是一个工厂,它接受一个用来检测指定名字是否已被禁用的正则表达式,并返回一个验证器函数。
在本例中,禁止的名字是“bob”; 验证器会拒绝任何带有“bob”的英雄名字。 在其余地方,只要配置的正则表达式能够匹配上,它可能拒绝“alice”或者任何其余名字。
forbiddenNameValidator工厂函数返回配置好的验证器函数。 该函数接受一个Angular控制器对象,并在控制器值有效时返回null,或无效时返回验证错误对象。 验证错误对象一般有一个名为验证秘钥(forbiddenName)的属性。其值为一个任意词典,咱们能够用来插入错误信息
在响应式表单组件中,添加自定义验证器至关简单。你所要作的一切就是直接把这个函数传给 FormControl 。
reactive/hero-form-reactive.component.ts (validator functions) 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) });