響應式表單是同步的。模板驅動表單是異步的。這個不一樣點很重要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(); }