用表單處理用戶(hù)輸入是許多常見(jiàn)應(yīng)用的基礎(chǔ)功能。 應(yīng)用通過(guò)表單來(lái)讓用戶(hù)登錄、修改個(gè)人檔案、輸入敏感信息以及執(zhí)行各種數(shù)據(jù)輸入任務(wù)。
Angular 提供了兩種不同的方法來(lái)通過(guò)表單處理用戶(hù)輸入:響應(yīng)式表單和模板驅(qū)動(dòng)表單。 兩者都從視圖中捕獲用戶(hù)輸入事件、驗(yàn)證用戶(hù)輸入、創(chuàng)建表單模型、修改數(shù)據(jù)模型,并提供跟蹤這些更改的途徑。
本指南提供的信息可以幫你確定哪種方式最適合你的情況。它介紹了這兩種方法所用的公共構(gòu)造塊,還總結(jié)了兩種方式之間的關(guān)鍵區(qū)別,并在建立、數(shù)據(jù)流和測(cè)試等不同的情境下展示了這些差異。
響應(yīng)式表單和模板驅(qū)動(dòng)表單以不同的方式處理和管理表單數(shù)據(jù)。每種方法都有各自的優(yōu)點(diǎn)。
表單 |
詳情 |
---|---|
響應(yīng)式表單 |
提供對(duì)底層表單對(duì)象模型直接、顯式的訪(fǎng)問(wèn)。它們與模板驅(qū)動(dòng)表單相比,更加健壯:它們的可擴(kuò)展性、可復(fù)用性和可測(cè)試性都更高。如果表單是你的應(yīng)用程序的關(guān)鍵部分,或者你已經(jīng)在使用響應(yīng)式表單來(lái)構(gòu)建應(yīng)用,那就使用響應(yīng)式表單。 |
模板驅(qū)動(dòng)表單 |
依賴(lài)模板中的指令來(lái)創(chuàng)建和操作底層的對(duì)象模型。它們對(duì)于向應(yīng)用添加一個(gè)簡(jiǎn)單的表單非常有用,比如電子郵件列表注冊(cè)表單。它們很容易添加到應(yīng)用中,但在擴(kuò)展性方面不如響應(yīng)式表單。如果你有可以只在模板中管理的非常基本的表單需求和邏輯,那么模板驅(qū)動(dòng)表單就很合適。 |
下表總結(jié)了響應(yīng)式表單和模板驅(qū)動(dòng)表單之間的一些關(guān)鍵差異。
響應(yīng)式 |
模板驅(qū)動(dòng) |
|
---|---|---|
建立表單模型 |
顯式的,在組件類(lèi)中創(chuàng)建 |
隱式的,由指令創(chuàng)建 |
數(shù)據(jù)模型 |
結(jié)構(gòu)化和不可變的 |
非結(jié)構(gòu)化和可變的 |
數(shù)據(jù)流 |
同步 |
異步 |
表單驗(yàn)證 |
函數(shù) |
指令 |
如果表單是應(yīng)用程序的核心部分,那么可伸縮性就非常重要。能夠跨組件復(fù)用表單模型是至關(guān)重要的。
響應(yīng)式表單比模板驅(qū)動(dòng)表單更有可伸縮性。它們提供對(duì)底層表單 API 的直接訪(fǎng)問(wèn),并且在視圖和數(shù)據(jù)模型之間使用同步數(shù)據(jù)流,從而可以更輕松地創(chuàng)建大型表單。響應(yīng)式表單需要較少的測(cè)試設(shè)置,測(cè)試時(shí)不需要深入理解變更檢測(cè),就能正確測(cè)試表單更新和驗(yàn)證。
模板驅(qū)動(dòng)表單專(zhuān)注于簡(jiǎn)單的場(chǎng)景,可復(fù)用性沒(méi)那么高。它們抽象出了底層表單 API,并且在視圖和數(shù)據(jù)模型之間使用異步數(shù)據(jù)流。對(duì)模板驅(qū)動(dòng)表單的這種抽象也會(huì)影響測(cè)試。測(cè)試程序非常依賴(lài)于手動(dòng)觸發(fā)變更檢測(cè)才能正常運(yùn)行,并且需要進(jìn)行更多設(shè)置工作。
響應(yīng)式表單和模板驅(qū)動(dòng)型表單都會(huì)跟蹤用戶(hù)與之交互的表單輸入元素和組件模型中的表單數(shù)據(jù)之間的值變更。這兩種方法共享同一套底層構(gòu)建塊,只在如何創(chuàng)建和管理常用表單控件實(shí)例方面有所不同。
響應(yīng)式表單和模板驅(qū)動(dòng)表單都建立在下列基礎(chǔ)類(lèi)之上。
基類(lèi) |
詳情 |
---|---|
FormControl
|
追蹤單個(gè)表單控件的值和驗(yàn)證狀態(tài)。 |
FormGroup
|
追蹤一個(gè)表單控件組的值和狀態(tài)。 |
FormArray
|
追蹤表單控件數(shù)組的值和狀態(tài)。 |
ControlValueAccessor
|
在 Angular 的 |
對(duì)于響應(yīng)式表單,你可以直接在組件類(lèi)中定義表單模型。?[formControl]
? 指令會(huì)通過(guò)內(nèi)部值訪(fǎng)問(wèn)器來(lái)把顯式創(chuàng)建的 ?FormControl
?實(shí)例與視圖中的特定表單元素聯(lián)系起來(lái)。
下面的組件使用響應(yīng)式表單為單個(gè)控件實(shí)現(xiàn)了一個(gè)輸入字段。在這個(gè)例子中,表單模型是 ?FormControl
?實(shí)例。
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
@Component({
selector: 'app-reactive-favorite-color',
template: `
Favorite Color: <input type="text" [formControl]="favoriteColorControl">
`
})
export class FavoriteColorComponent {
favoriteColorControl = new FormControl('');
}
圖 1 展示了在響應(yīng)式表單中,表單模型是如何成為事實(shí)之源(source of truth)的。它通過(guò)輸入元素上的 ?[formControl]
? 指令,在任何給定的時(shí)間點(diǎn)提供表單元素的值和狀態(tài)。
圖 1. 在響應(yīng)式表單中直接訪(fǎng)問(wèn)表單模型
在模板驅(qū)動(dòng)表單中,表單模型是隱式的,而不是顯式的。指令 ?NgModel
?為指定的表單元素創(chuàng)建并管理一個(gè) ?FormControl
?實(shí)例。
下面的組件使用模板驅(qū)動(dòng)表單為單個(gè)控件實(shí)現(xiàn)了同樣的輸入字段。
import { Component } from '@angular/core';
@Component({
selector: 'app-template-favorite-color',
template: `
Favorite Color: <input type="text" [(ngModel)]="favoriteColor">
`
})
export class FavoriteColorComponent {
favoriteColor = '';
}
在模板驅(qū)動(dòng)表單中,其事實(shí)之源就是模板。你沒(méi)有對(duì) ?FormControl
?實(shí)例的直接編程訪(fǎng)問(wèn),如圖 2 所示。
圖 2. 模板驅(qū)動(dòng)表單中對(duì)表單模型的間接訪(fǎng)問(wèn)
當(dāng)應(yīng)用包含一個(gè)表單時(shí),Angular 必須讓該視圖與組件模型保持同步,并讓組件模型與視圖保持同步。當(dāng)用戶(hù)通過(guò)視圖更改值并進(jìn)行選擇時(shí),新值必須反映在數(shù)據(jù)模型中。同樣,當(dāng)程序邏輯改變數(shù)據(jù)模型中的值時(shí),這些值也必須反映到視圖中。
響應(yīng)式表單和模板驅(qū)動(dòng)表單在處理來(lái)自用戶(hù)或程序化變更時(shí)的數(shù)據(jù)處理方式上有所不同。下面的這些原理圖會(huì)以上面定義的 ?favorite-color
? 輸入字段為例,分別說(shuō)明兩種表單各自的數(shù)據(jù)流。
在響應(yīng)式表單中,視圖中的每個(gè)表單元素都直接鏈接到一個(gè)表單模型(?FormControl
?實(shí)例)。 從視圖到模型的修改以及從模型到視圖的修改都是同步的,而且不依賴(lài)于 UI 的渲染方式。
這個(gè)視圖到模型的圖表展示了當(dāng)輸入字段的值發(fā)生變化時(shí),數(shù)據(jù)流是如何從視圖開(kāi)始經(jīng)過(guò)下列步驟進(jìn)行流動(dòng)的。
ControlValueAccessor
?會(huì)監(jiān)聽(tīng)表單輸入框元素上的事件,并立即把新值傳給 ?FormControl
?實(shí)例。FormControl
?實(shí)例會(huì)通過(guò) ?valueChanges
?這個(gè)可觀察對(duì)象發(fā)出這個(gè)新值。valueChanges
?的任何一個(gè)訂閱者都會(huì)收到這個(gè)新值。
這個(gè)模型到視圖的示意圖體現(xiàn)了程序中對(duì)模型的修改是如何通過(guò)下列步驟傳播到視圖中的。
favoriteColorControl.setValue()
? 方法被調(diào)用,它會(huì)更新這個(gè) ?FormControl
?的值。FormControl
?實(shí)例會(huì)通過(guò) ?valueChanges
?這個(gè)可觀察對(duì)象發(fā)出新值。valueChanges
?的任何訂閱者都會(huì)收到這個(gè)新值。
在模板驅(qū)動(dòng)表單中,每一個(gè)表單元素都是和一個(gè)負(fù)責(zé)管理內(nèi)部表單模型的指令關(guān)聯(lián)起來(lái)的。
這個(gè)視圖到模型的圖表展示了當(dāng)輸入字段的值發(fā)生變化時(shí),數(shù)據(jù)流是如何從視圖開(kāi)始經(jīng)過(guò)下列步驟進(jìn)行流動(dòng)的。
FormControl
?實(shí)例上的 ?setValue()
? 方法。FormControl
?實(shí)例通過(guò) ?valueChanges
?這個(gè)可觀察對(duì)象發(fā)出新值。valueChanges
?的任何訂閱者都會(huì)收到新值。ControlValueAccessory
?還會(huì)調(diào)用 ?NgModel.viewToModelUpdate()
? 方法,它會(huì)發(fā)出一個(gè) ?ngModelChange
?事件。favoriteColor
?,組件中的 ?favoriteColor
?屬性就會(huì)修改為 ?ngModelChange
?事件所發(fā)出的值("Blue")。
這個(gè)模型到視圖的示意圖展示了當(dāng) ?favoriteColor
?從藍(lán)變到紅時(shí),數(shù)據(jù)是如何經(jīng)過(guò)如下步驟從模型流動(dòng)到視圖的。
favoriteColor
?的值。NgModel
?指令上的 ?ngOnChanges
?生命周期鉤子。ngOnChanges()
? 方法會(huì)把一個(gè)異步任務(wù)排入隊(duì)列,以設(shè)置內(nèi)部 ?FormControl
?實(shí)例的值。FormControl
?實(shí)例賦值的任務(wù)就會(huì)執(zhí)行。FormControl
?實(shí)例通過(guò)可觀察對(duì)象 ?valueChanges
?發(fā)出最新值。valueChanges
?的任何訂閱者都會(huì)收到這個(gè)新值。ControlValueAccessor
?會(huì)使用 ?favoriteColor
?的最新值來(lái)修改表單的輸入框元素。
變更追蹤的方法對(duì)應(yīng)用的效率有著重要影響。
表格 |
詳細(xì)信息 |
---|---|
響應(yīng)式表單 |
通過(guò)以不可變的數(shù)據(jù)結(jié)構(gòu)提供數(shù)據(jù)模型,來(lái)保持?jǐn)?shù)據(jù)模型的純粹性。每當(dāng)在數(shù)據(jù)模型上觸發(fā)更改時(shí), |
模板驅(qū)動(dòng)表單 |
依賴(lài)于可變性和雙向數(shù)據(jù)綁定,可以在模板中做出更改時(shí)更新組件中的數(shù)據(jù)模型。由于使用雙向數(shù)據(jù)綁定時(shí)沒(méi)有用來(lái)對(duì)數(shù)據(jù)模型進(jìn)行跟蹤的唯一性更改,因此變更檢測(cè)在需要確定何時(shí)更新時(shí)效率較低。 |
前面那些使用 ?favorite-color
? 輸入元素的例子就演示了這種差異。
FormControl
?的實(shí)例總會(huì)返回一個(gè)新值favorite-color
? 屬性總會(huì)被修改為新值驗(yàn)證是管理任何表單時(shí)必備的一部分。無(wú)論你是要檢查必填項(xiàng),還是查詢(xún)外部 API 來(lái)檢查用戶(hù)名是否已存在,Angular 都會(huì)提供一組內(nèi)置的驗(yàn)證器,以及創(chuàng)建自定義驗(yàn)證器所需的能力。
表格 |
詳細(xì)信息 |
---|---|
響應(yīng)式表單 |
把自定義驗(yàn)證器定義成函數(shù),它以要驗(yàn)證的控件作為參數(shù) |
模板驅(qū)動(dòng)表單 |
和模板指令緊密相關(guān),并且必須提供包裝了驗(yàn)證函數(shù)的自定義驗(yàn)證器指令 |
測(cè)試在復(fù)雜的應(yīng)用程序中也起著重要的作用。當(dāng)驗(yàn)證你的表單功能是否正確時(shí),更簡(jiǎn)單的測(cè)試策略往往也更有用。測(cè)試響應(yīng)式表單和模板驅(qū)動(dòng)表單的差別之一在于它們是否需要渲染 UI 才能基于表單控件和表單字段變化來(lái)執(zhí)行斷言。下面的例子演示了使用響應(yīng)式表單和模板驅(qū)動(dòng)表單時(shí)表單的測(cè)試過(guò)程。
響應(yīng)式表單提供了相對(duì)簡(jiǎn)單的測(cè)試策略,因?yàn)樗鼈兡芴峁?duì)表單和數(shù)據(jù)模型的同步訪(fǎng)問(wèn),而且不必渲染 UI 就能測(cè)試它們。在這些測(cè)試中,控件和數(shù)據(jù)是通過(guò)控件進(jìn)行查詢(xún)和操縱的,不需要和變更檢測(cè)周期打交道。
下面的測(cè)試?yán)们懊胬又械?nbsp;"喜歡的顏色" 組件來(lái)驗(yàn)證響應(yīng)式表單中的 "從視圖到模型" 和 "從模型到視圖" 數(shù)據(jù)流。
驗(yàn)證“從視圖到模型”的數(shù)據(jù)流
第一個(gè)例子執(zhí)行了下列步驟來(lái)驗(yàn)證“從視圖到模型”數(shù)據(jù)流。
favoriteColorControl
?的值與來(lái)自輸入框的值是匹配的。it('should update the value of the input field', () => {
const input = fixture.nativeElement.querySelector('input');
const event = createNewEvent('input');
input.value = 'Red';
input.dispatchEvent(event);
expect(fixture.componentInstance.favoriteColorControl.value).toEqual('Red');
});
下一個(gè)例子執(zhí)行了下列步驟來(lái)驗(yàn)證“從模型到視圖”數(shù)據(jù)流。
favoriteColorControl
?這個(gè) ?FormControl
?實(shí)例來(lái)設(shè)置新值。it('should update the value in the control', () => {
component.favoriteColorControl.setValue('Blue');
const input = fixture.nativeElement.querySelector('input');
expect(input.value).toBe('Blue');
});
使用模板驅(qū)動(dòng)表單編寫(xiě)測(cè)試就需要詳細(xì)了解變更檢測(cè)過(guò)程,以及指令在每個(gè)變更檢測(cè)周期中如何運(yùn)行,以確保在正確的時(shí)間查詢(xún)、測(cè)試或更改元素。
下面的測(cè)試使用了以前的 "喜歡的顏色" 組件,來(lái)驗(yàn)證模板驅(qū)動(dòng)表單的 "從視圖到模型" 和 "從模型到視圖" 數(shù)據(jù)流。
下面的測(cè)試驗(yàn)證了 "從視圖到模型" 數(shù)據(jù)流:
it('should update the favorite color in the component', fakeAsync(() => {
const input = fixture.nativeElement.querySelector('input');
const event = createNewEvent('input');
input.value = 'Red';
input.dispatchEvent(event);
fixture.detectChanges();
expect(component.favoriteColor).toEqual('Red');
}));
這個(gè) "視圖到模型" 測(cè)試的執(zhí)行步驟如下:
favoriteColor
?屬性的值與來(lái)自輸入框的值是匹配的。
下面的測(cè)試驗(yàn)證了 "從模型到視圖" 的數(shù)據(jù)流:
it('should update the favorite color on the input field', fakeAsync(() => {
component.favoriteColor = 'Blue';
fixture.detectChanges();
tick();
const input = fixture.nativeElement.querySelector('input');
expect(input.value).toBe('Blue');
}));
這個(gè) "模型到視圖" 測(cè)試的執(zhí)行步驟如下:
favoriteColor
?的值。
fakeAsync()
? 任務(wù)中使用 ?tick()
? 方法來(lái)模擬時(shí)間的流逝。
favoriteColor
?屬性值是匹配的。
更多建議: