本文將半翻譯半總結的講講ng2官網的另一個未翻譯高級教程頁面。
文章目的是使用ng2提供的響應式表單技術快速搭出功能完善豐富的界面表單組件。
響應式表單是一項響應式風格的ng2技術,本文將解釋響應式表單並用來創建一個英雄詳情編輯器。
包含內容:
- 響應式表單介紹
- 開始搭建
- 創建數據模型
- 創建響應式的表單組件
- 創建組建的模板文件
- 引入ReactiveFormsModule
- 顯示HeroDetailComponent
- 添加一個FormGroup
- 看看表單模型
- 介紹FormBuilder
- 驗證的需求
- 放置FormGroup
- 檢查FormControl屬性
- 使用setValue以及patchValue設置表單模型數據
- 使用FotmArray提供FormGroup的數組
- 觀察控件的更改
響應式表單介紹
angular提供了兩種表單搭建技術: 響應式表單和模板驅動式表單。都依賴於@angular/forms庫,並共享了一些通用的表單控件集。
但是他們在原理、代碼風格以及技術上存在區別。他們甚至有自己的模塊:ReactiveFormsModule以及FormsModule。
響應式表單(ReactiveFormsModule):
anguar的響應式表單簡化了管理數據時響應式風格的編碼實現,使用了在無視圖數據模型(從服務器獲取)以及以視圖為導向的模型用於保持屏幕上HTML控件顯示的值與狀態。響應式表單提供了響應式模式測試以及驗證上的便利。
使用響應式表單,你將在組件類中創建一個anular的表單控件樹對象,並在組件模板中使用提供的技術綁定到原生表單控件標簽中。
你直接在組件類中創建並操作控件對象。因為組件類能直接訪問到數據模型以及表單控件結構,你可以將數據模型值推送到表單控件以及將用戶的更改響應到后邊來。組件可以觀察表單控件狀態的更改並響應這些更改。
直接使用表單控件對象工作的一個好處是值以及驗證的更新總能夠同步完成並受你控制。你不會遇到有時候因為模板驅動表單造成的時間問題,並且響應式表單更易測試。
為了保持響應的一致性,組件會保存不一致的數據模型,將其視為純粹的原始值。不會直接更新數據模型,組件會提取用戶的更改並轉發到外面的組件或服務中,(可能是用來保存他們的)並返回一個新數據模型到組件,用於響應模型狀態的更新。
使用響應式表單指令不需要你依賴於全部響應式原理,但是這確實能促進響應式編程方法如果你選擇了要使用這個方法的話。
模板驅動式表單(FormsModule):
模板驅動式的表單使用了完全不同的方式。
你在組件模板中放置HTML表單控件(input這些)並使用比如ngModel這些指令綁定到數據模型屬性。
你不需要創建angular表單控件對象,因為angular會根據你的數據綁定信息自動幫你創建出來。你不是推送或者拉取數據值。angular在ngModel中幫你處理了。angular會更新那些被改變的數據值。
出於這個原因,ngModel不再是ReactiveFormsModule的一部分了。
這意味着可以在組件類中寫更少的代碼,不過模板驅動表單是異步工作的,這可能會在某些情況下復雜化開發。
同步vs異步
響應式表單是同步的。模板驅動表單是異步的,這是其區別的根源。
在響應式表單中,你在代碼中創建一個完整的表單控件樹。你可以從子表單或父表單中立即更新或取回一個值,因為所有的控件都可訪問到。
模板驅動表單將他們的表單控件的創建委托給了指令。為了避免“檢查后又更改”的錯誤,這些指令使用了不止一個循環來建立整個控件樹。這意味着你必須在操作任何組件類中的空間表單時等那么一小會兒。
比如說,如果你使用@ViewChild(NgForm)查詢注入到表單控件中並在ngAfterViewInit這個生命周期鈎子中檢查它,你將發現它沒有子元素。你必須等一會,使用setTimeout來等待,然后你才能從這個空間中去除值並驗證它或者將它設置為新的值。
模板驅動表單的異步性同時復雜化了單元測試。你必須使用async()或者fakeAsync()來包裝你的測試塊來避免找不到表單的值。而如果使用的是響應式表單,一切都如你所願的存在着。
哪一個方式更好?
沒有哪種是更好的。他們是兩種不同的搭建方式,各自擁有長處和短處。使用最適合你的方式才是對的。在一個應用中你可能兩種方式都要使用到。
本文僅僅會描述響應式的范例與精華所在。對於模板驅動式表單,可以前往表單介紹頁。
接下來你將寫出你自己的項目來演示響應式表單。然后你將學會關於angular表單類以及如何在響應式表單中使用它。
對上文的總結就是,相比ng1中數據的雙向綁定,ng2保留了這個雙向綁定能力(底層其實優化了很多),原先的ng-model指令升級成了ngModel,使用的功能保持不變。
同時盡管ng2版本的數據雙向綁定得到了很大的優化,仍改變不了其數據異步綁定的方式,因為ng2不能確定數據何時綁定,我們也不能確定很多網絡請求得到的數據到來的時間。
在ng1中其實這個機制會有一些尷尬的場景,至少筆者在一些情況下不得不在一些業務場景下使用setTimeout來保證數據已經成功綁定進入scope的watch循環,但這個異步綁定數據又是不可避免的,除非我們自己來適應實際項目改寫angular代碼了。
所以ng2就提供了讓我們配合具體項目場景改寫ngModel的能力,也就是原文介紹的響應式表單。
其跟ngModel的關系就是,ngModel是響應式表單的官方實現,其在我們綁定數據時自動為我們實現響應式表單中用到的幾個機制,如果我們需要數據嚴格的實時同步綁定,就不必使用ngModel,可以親自來編寫響應式的表單,步驟覆蓋了組件模板到數據模型類整條龍,而這么多事情在合適的場景下使用ngModel已經可以實現了,這兩種表單綁定的方式各有其優勢。
1. 使用響應式表單
響應式表單的能力封裝在ReactiveFormsModule中,並且跟FormsModule同時包含在@angular/forms這個包中。
表單類的要點:
- AbstractControl是FormControl、FormGroup、FormArray這三個實例表單類的抽象基類。它提供了他們的通用行為以及屬性,其中就有observable。
- FormControl在單個表單控件中檢查值並驗證狀態。它負責將其通知給HTML表單控件(比如input這些)。
- FormGroup負責AbstractControl實例的一個組的值與驗證狀態。組的屬性包含了它們的子控件。你的組件的頂級表單就是一個FormGroup。
- FormArray負責AbstractControl實例的數值索引數組的值與狀態驗證。
2. FormControl
最核心的指令就是FormControl,算是底層的ngModel,在模板標簽中跟定義好的數據模型字段綁定起來,就像這樣:
<h2>Hero Detail</h2>
<h3><i>Just a FormControl</i></h3>
<label class="center-block">Name:
<input class="form-control" [formControl]="name">
</label>
同時在組件代碼中需要將上例中這個name字段聲明為FormControl類:
export class HeroDetailComponent1 { name = new FormControl(); }
3. FormGroup
將多個FormControl對象分組到FormGroup中,用來方便管理。定義方法如下:
import { Component } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; export class HeroDetailComponent2 { heroForm = new FormGroup ({ name: new FormControl() }); }
此時模板標簽中也要分組來寫:
<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>
現在的效果就是可以從heroForm中實時讀取到其值和一些附加的狀態的變化。
可以在模板中添加兩個標簽來展示數據的更改:
<p>Form value: {{ heroForm.value | json }}</p> <p>Form status: {{ heroForm.status | json }}</p>
4. FormBuilder
還有個新概念就是FormBuilder,是用來幫助創建表單類的:
- 1. 顯示聲明heroForm屬性的類型為FormGroup,后面你將會初始化它
- 2. 注入FormBuilder到構造器中
- 3. 使用FormsBuilder添加新方法定義heroForm,叫做createForm。
- 4. 在構造器中執行createForm方法。
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" }); } }
上例中執行createForm方法即可動態快速的創建出表單類,其在一些表單類需要更改的場景下可以使用。
5. setValue和patchValue
這兩個方法是真正給表單模型賦值用的。因為表單顯示的數據與真實的底層數據肯定不能使同一個,否則表單輸入數據一旦更改,源數據就被污染了,而這兩個方法就是用來將源數據賦值到表單模型數據上的。
每當需要賦值時就可以調用,其中setValue必須准確賦值,並且會在數據不匹配時報告錯誤;而patchValue沒有這么嚴格,但可以傳一個對象,且不匹配時不會報告錯誤。
而我們要做的就是在ng2組件的ngOnChanges回調中手動執行setValue設置數據值。比如這樣:
ngOnChanges() this.heroForm.setValue({ name: this.hero.name, address: this.hero.addresses[0] || new Address() }); }
同時ng2還提供了一個reset方法來重新調用setValue方法(setValue本身好像只是用來一次性賦值的)。
6. FormArray
FormArray是用來對付FormGroups列表的,比如一個英雄有可能有多個address字段,address字段本身就是個FormGroup,此時就要用到FormArray了:
this.heroForm = this.fb.group({ name: ['', Validators.required ], secretLairs: this.fb.array([]), // <-- secretLairs as an empty FormArray power: '', sidekick: '' });
獲取FormArray要用到FormGroup提供的一個get方法:
get secretLairs(): FormArray { return this.heroForm.get('secretLairs') as FormArray; };
其顯示的模板如下:
<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以后就可以使用ngFor把這一個表單列表遍歷出來了(直接使用ngModel加ngFor能省不少事情...)。
總結:
筆者目前使用ng2還沒涉及到比較復雜的表單,所以對原文提到的內容理解並不是很深,加上老外寫的文章通常都過於完整,都喜歡用長篇大論來說明一個簡單的知識點,所以本文后半段其實沒多少原文的影子,純屬筆者自己拙劣的概括,並且沒有做過太多實踐。
回看ng2的響應式表單能力,提供的指令以及服務也就這么幾個(FormControl、FormGroup、FormArray、FormBuilder以及幾個功能性方法),巧妙在使用這些能力就能完成一個強大的表單界面,其編碼體驗絕對是遠超傳統jQuery強行從DOM讀取節點值的方式的,並且提供了除了簡單的ngModel能力之外的更具體更強大的數據綁定方案,還有本文未提及的表單驗證這個大內容,在ng2提供的這個響應式表單方案下實現起來也是很得心應手的。