响应式表单是同步的。模板驱动表单是异步的。这个不一样点很重要javascript
使用响应式表单,咱们会在代码中建立整个表单控件树。 咱们能够当即更新一个值或者深刻到表单中的任意节点,由于全部的控件都始终是可用的。java
模板驱动表单会委托指令来建立它们的表单控件。 为了消除“检查完后又变化了”的错误,这些指令须要消耗一个以上的变动检测周期来构建整个控件树。 这意味着在从组件类中操纵任何控件以前,咱们都必须先等待一个节拍。react
好比,若是咱们用 @ViewChild(NgForm)
查询来注入表单控件,并在 生命周期钩子 ngAfterViewInit
中检查它,就会发现它没有子控件。 咱们必须使用 setTimeout
等待一个节拍才能从控件中提取值、测试有效性,或把它设置为新值。api
ReactiveFormsModule
AbstractControl
是三个具体表单类的抽象基类。 并为它们提供了一些共同的行为和属性,其中有些是可观察对象(Observable)。数组
FormControl 用于跟踪一个单独的表单控件的值和有效性状态。它对应于一个HTML表单控件,好比输入框和下拉框。浏览器
FormGroup用于 跟踪一组AbstractControl
的实例的值和有效性状态。 该组的属性中包含了它的子控件。 组件中的顶级表单就是一个FormGroup
。服务器
FormArray用于跟踪AbstractControl
实例组成的有序数组的值和有效性状态。异步
多个FormControl,咱们会但愿把它们注册进一个父FormGroup
中。这很容易。只要把它加入hero-detail.component.ts
的import
区就能够了ide
export class HeroDetailComponent2 { heroForm = new FormGroup ({ name: new FormControl() }); }
<form [formGroup]="heroForm" novalidate> <div class="form-group"> <label class="center-block">Name: <input class="form-control" formControlName="name"> </label> </div> </form>
<form>
元素上的novalidate
属性会阻止浏览器使用原生HTML中的表单验证器。函数
formGroup
是一个响应式表单的指令,它拿到一个现有FormGroup
实例,并把它关联到一个HTML元素上。 这种状况下,它关联到的是form
元素上的FormGroup
实例heroForm
heroForm.value ==> { name : xxxx, ....... }
FormBuilder:
FormBuilder
类能经过处理控件建立的细节问题来帮咱们减小重复劳动。
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
控件的初始值是空字符串。
要想让name
这个FormControl
是必须的,请把FormGroup
中的name
属性改成一个数组。第一个条目是name
的初始值,第二个是required
验证器:Validators.required
this.heroForm = this.fb.group({ name: ['', Validators.required ], });
FormGroup
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: ''
});
}
}
<div formGroupName="address" class="well well-lg"> <input class="form-control" formControlName="street"> <input class="form-control" formControlName="city"> <select class="form-control" formControlName="state"> <input class="form-control" formControlName="zip"> </div>
作完这些以后,浏览器中的JSON输出就变成了带有多级FormGroup
的住址。
FormControl
的属性
使用.get()
方法来提取表单中一个单独FormControl
的状态
<p>Name value: {{ heroForm.get('name').value }}</p>
要点取得FormGroup
中的FormControl
的状态,使用点语法来指定到控件的路径
<p>Street value: {{ heroForm.get('address.street').value}}</p>
使用此技术来显示FromControl
的任意属性,代码以下
属性 |
说明 |
---|---|
myControl.value |
|
myControl.status |
|
myControl.pristine |
若是用户还没有改变过这个控件的值,则为 |
myControl.untouched |
|
自服务器的 hero
就是数据模型,而FormControl
的结构就是表单模型
组件必须把数据模型中的英雄值复制到表单模型中。这里隐含着两个很是重要的点。
开发人员必须理解数据模型是如何映射到表单模型中的属性的。
用户修改时的数据流是从DOM元素流向表单模型的,而不是数据模型。表单控件永远不会修改数据模型。
// 数据模型
export class Hero {
id = 0;
name = '';
addresses: Address[];
}
export class Address {
street = '';
city = '';
state = '';
zip = '';
}
//表单模型
this.heroForm = this.fb.group({
name: ['', Validators.required ],
address: this.fb.group(new Address()),
power: '',
sidekick: ''
});
在这些模型中有两点显著的差别:
Hero
有一个id
。表单模型中则没有,由于咱们一般不会把主键展现给用户。
Hero
有一个住址数组。这个表单模型只表示了一个住址,稍后的修改则能够表示多个。
setValue
和patchValue
来操纵表单模型this.heroForm.setValue({ name: this.hero.name, address: this.hero.addresses[0] || new Address() //(只能显示英雄的第一个住址,不过咱们还必须考虑彻底没有住址的可能性) }); hero
setValue
方法会在赋值给任何表单控件以前先检查数据对象的值。
它不会接受一个与FormGroup结构不一样或缺乏表单组中任何一个控件的数据对象。
若是咱们有什么拼写错误或控件嵌套的不正确,它就能返回一些有用的错误信息。 patchValue
会默默地失败
借助patchValue
,咱们能够经过提供一个只包含要更新的控件的键值对象来把值赋给FormGroup
中的指定控件
this.heroForm.patchValue({ name: this.hero.name });
ngOnChanges
)咱们能够在ngOnChanges钩子中调用setValue
,就像例子中所演示的那样, 每当输入属性hero
发生变化时,Angular就会调用它。
咱们应该在更换英雄的时候重置表单,以便来自前一个英雄的控件值被清除,而且其状态被恢复为pristine
(原始)状态。 咱们能够在ngOnChanges
的顶部调用reset
,就像这样
this.heroForm.reset();
reset
方法有一个可选的state
值,让咱们能在重置状态的同时顺便设置控件的值。 在内部实现上,reset
会把该参数传给了setValue
。 略微重构以后,ngOnChanges
会变成这样
ngOnChanges() { this.heroForm.reset({ name: this.hero.name, address: this.hero.addresses[0] || new Address() }); }
FormArray
来表示FormGroup
数组Hero.addresses
属性就是一个Address
实例的数组。 一个住址的FormGroup
能够显示一个Address
对象。 而FormArray
能够显示一个住址FormGroup
的数组
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
要想使用FormArray
,咱们要这么作:
在数组中定义条目(FormControl
或FormGroup
)。
把这个数组初始化微一组从数据模型中的数据建立的条目。
根据用户的需求添加或移除这些条目。
从用户的视角来看,英雄们没有住址。 只有咱们凡人才有住址,英雄们拥有的是秘密小屋! 把FormGroup
型的住址替换为FormArray
型的secretLairs
定义:
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
来进行初始化。
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
的引用。 把这个表达式包装进一个名叫secretLairs
的便捷属性中来让它更清晰,并供复用
this.heroForm = this.fb.group({
name: '',
secretLairs: this.fb.array([]),
power: '',
sidekick: ''
});
get secretLairs(): FormArray { return this.heroForm.get('secretLairs') as FormArray; };
FormArray
当前HTML模板显示单个的地址FormGroup
。 咱们要把它修改为能显示0、1或更多个表示英雄地址的FormGroup
。
要改的部分主要是把之前表示地址的HTML模板包裹进一个<div>
中,而且使用*ngFor
来重复渲染这个<div>
。
诀窍在于要知道如何编写*ngFor
。主要有三点:
在*ngFor
的<div>
以外套上另外一个包装<div>
,而且把它的formArrayName
指令设为"secretLairs"
。 这一步为内部的表单控件创建了一个FormArray
型的secretLairs
做为上下文,以便重复渲染HTML模板。
这些重复条目的数据源是FormArray.controls
而不是FormArray
自己。 每一个控件都是一个FormGroup
型的地址对象,与之前的模板HTML所指望的格式彻底同样。
每一个被重复渲染的FormGroup
都须要一个独一无二的formGroupName
,它必须是FormGroup
在这个FormArray
中的索引。 咱们将复用这个索引,以便为每一个地址组合出一个独一无二的标签。
<div formArrayName="secretLairs" class="well well-lg"> <div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" > <input class="form-control" formControlName="street"> <input class="form-control" formControlName="city"> <select class="form-control" formControlName="state"> <input class="form-control" formControlName="zip"> </div> </div>
添加一个addLair
方法,它获取secretLairs
数组,并把新的表示地址的FormGroup
添加到其中。
addLair() { this.secretLairs.push(this.fb.group(new Address())); } <button (click)="addLair()" type="button">Add a Secret Lair</button> //务必确保添加了type="button"属性。 事实上,咱们应该老是指定按钮的type。 若是不明确指定类型,按钮的默认类型就是“submit”(提交)。
当咱们稍后添加了表单提交的动做时,每一个“submit”按钮都是触发一次提交操做,而它将可能会作一些处理,好比保存当前的修改。
咱们显然不会但愿每当用户点击“Add a Secret Lair”按钮时就保存一次。
用户在父组件HeroListComponent
中选取了一个英雄,Angular就会调用一次ngOnChanges
用户修改英雄的名字或秘密小屋时,Angular并不会调用ngOnChanges
经过订阅表单控件的属性之一来了解这些变化,此属性会发出变动通知, valueChanges
,能够返回一个RxJS的Observable
对象。
添加下列方法,以监听姓名这个FormControl
中值的变化
nameChangeLog: string[] = []; logNameChange() { const nameControl = this.heroForm.get('name'); nameControl.valueChanges.forEach( (value: string) => this.nameChangeLog.push(value) ); }
在构造函数中调用它,就在建立表单的代码以后
constructor(private fb: FormBuilder) { this.createForm(); this.logNameChange(); }