細(xì)說 Angular 2+ 的表單(一):模板驅(qū)動(dòng)型表單
響應(yīng)式表單
響應(yīng)式表單乍一看還是很像模板驅(qū)動(dòng)型表單的涮毫,但響應(yīng)式表單需要引入一個(gè)不同的模塊: ReactiveFormsModule
而不是 FormsModule
import {ReactiveFormsModule} from "@angular/forms";
@NgModule({
// 省略其他
imports: [..., ReactiveFormsModule],
// 省略其他
})
// 省略其他
與模板驅(qū)動(dòng)型表單的區(qū)別
接下來我們還是利用前面的例子,用響應(yīng)式表單的要求改寫一下:
<form [formGroup]="user" (ngSubmit)="onSubmit(user)">
<label>
<span>電子郵件地址</span>
<input type="text" formControlName="email" placeholder="請(qǐng)輸入您的 email 地址">
</label>
<div *ngIf="user.get('email').hasError('required') && user.get('email').touched" class="error">
email 是必填項(xiàng)
</div>
<div *ngIf="user.get('email').hasError('pattern') && user.get('email').touched" class="error">
email 格式不正確
</div>
<div>
<label>
<span>密碼</span>
<input type="password" formControlName="password" placeholder="請(qǐng)輸入您的密碼">
</label>
<div *ngIf="user.get('password').hasError('required') && user.get('password').touched" class="error">
密碼是必填項(xiàng)
</div>
<label>
<span>確認(rèn)密碼</span>
<input type="password" formControlName="repeat" placeholder="請(qǐng)?jiān)俅屋斎朊艽a">
</label>
<div *ngIf="user.get('repeat').hasError('required') && user.get('repeat').touched" class="error">
確認(rèn)密碼是必填項(xiàng)
</div>
<div *ngIf="user.hasError('validateEqual') && user.get('repeat').touched" class="error">
確認(rèn)密碼和密碼不一致
</div>
</div>
<div formGroupName="address">
<label>
<span>省份</span>
<select formControlName="province">
<option value="">請(qǐng)選擇省份</option>
<option [value]="province" *ngFor="let province of provinces">{{province}}</option>
</select>
</label>
<label>
<span>城市</span>
<select formControlName="city">
<option value="">請(qǐng)選擇城市</option>
<option [value]="city" *ngFor="let city of (cities$ | async)">{{city}}</option>
</select>
</label>
<label>
<span>區(qū)縣</span>
<select formControlName="area">
<option value="">請(qǐng)選擇區(qū)縣</option>
<option [value]="area" *ngFor="let area of (areas$ | async)">{{area}}</option>
</select>
</label>
<label>
<span>地址</span>
<input type="text" formControlName="addr">
</label>
</div>
<button type="submit" [disabled]="user.invalid">注冊(cè)</button>
</form>
這段代碼和模板驅(qū)動(dòng)型表單的那段看起來差不多宝剖,但是有幾個(gè)區(qū)別:
- 表單多了一個(gè)指令
[formGroup]="user"
- 去掉了對(duì)表單的引用
#f="ngForm"
- 每個(gè)控件多了一個(gè)
formControlName
- 但同時(shí)每個(gè)控件也去掉了驗(yàn)證條件拯钻,比如
required
吉嚣、minlength
等 - 在地址分組中用
formGroupName="address"
替代了ngModelGroup="address"
模板上的區(qū)別大概就這樣了观话,接下來我們來看看組件的區(qū)別:
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from "@angular/forms";
@Component({
selector: 'app-model-driven',
templateUrl: './model-driven.component.html',
styleUrls: ['./model-driven.component.css']
})
export class ModelDrivenComponent implements OnInit {
user: FormGroup;
ngOnInit() {
// 初始化表單
this.user = new FormGroup({
email: new FormControl('', [Validators.required, Validators.pattern(/([a-zA-Z0-9]+[_|_|.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|_|.]?)*[a-zA-Z0-9]+.[a-zA-Z]{2,4}/)]),
password: new FormControl('', [Validators.required]),
repeat: new FormControl('', [Validators.required]),
address: new FormGroup({
province: new FormControl(''),
city: new FormControl(''),
area: new FormControl(''),
addr: new FormControl('')
})
});
}
onSubmit({value, valid}){
if(!valid) return;
console.log(JSON.stringify(value));
}
}
從上面的代碼中我們可以看到,這里的表單( FormGroup
)是由一系列的表單控件( FormControl
)構(gòu)成的逊彭。其實(shí) FormGroup
的構(gòu)造函數(shù)接受的是三個(gè)參數(shù): controls
(表單控件『數(shù)組』咸灿,其實(shí)不是數(shù)組,是一個(gè)類似字典的對(duì)象) 侮叮、 validator
(驗(yàn)證器) 和 asyncValidator
(異步驗(yàn)證器) 避矢,其中只有 controls
數(shù)組是必須的參數(shù),后兩個(gè)都是可選參數(shù)。
// FormGroup 的構(gòu)造函數(shù)
constructor(
controls: {
[key: string]: AbstractControl;
},
validator?: ValidatorFn,
asyncValidator?: AsyncValidatorFn
)
我們上面的代碼中就沒有使用驗(yàn)證器和異步驗(yàn)證器的可選參數(shù)审胸,而且注意到我們提供 controls
的方式是亥宿,一個(gè) key
對(duì)應(yīng)一個(gè) FormControl
。比如下面的 key
是 password
砂沛,對(duì)應(yīng)的值是 new FormControl('', [Validators.required])
烫扼。這個(gè) key
對(duì)應(yīng)的就是模板中的 formControlName
的值,我們模板代碼中設(shè)置了 formControlName="password"
碍庵,而表單控件會(huì)根據(jù)這個(gè) password
的控件名來跟蹤實(shí)際的渲染出的表單頁(yè)面上的控件(比如 <input formcontrolname="password">
)的值和驗(yàn)證狀態(tài)映企。
password: new FormControl('', [Validators.required])
那么可以看出,這個(gè)表單控件的構(gòu)造函數(shù)同樣也接受三個(gè)可選參數(shù)静浴,分別是:控件初始值( formState
)堰氓、控件驗(yàn)證器或驗(yàn)證器數(shù)組( validator
)和控件異步驗(yàn)證器或異步驗(yàn)證器數(shù)組( asyncValidator
)。上面的那行代碼中苹享,初始值為空字符串双絮,驗(yàn)證器是『必選』,而異步驗(yàn)證器我們沒有提供得问。
// FormControl 的構(gòu)造函數(shù)
constructor(
formState?: any, // 控件初始值
validator?: ValidatorFn | ValidatorFn[], // 控件驗(yàn)證器或驗(yàn)證器數(shù)組
asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] // 控件異步驗(yàn)證器或異步驗(yàn)證器數(shù)組
)
由此可以看出囤攀,響應(yīng)式表單區(qū)別于模板驅(qū)動(dòng)型表單的的主要特點(diǎn)在于:是由組件類去創(chuàng)建、維護(hù)和跟蹤表單的變化宫纬,而不是依賴模板焚挠。
那么我們是否在響應(yīng)式表單中還可以使用 ngModel
呢?當(dāng)然可以哪怔,但這樣的話表單的值會(huì)在兩個(gè)不同的位置存儲(chǔ)了: ngModel
綁定的對(duì)象和 FormGroup
宣蔚,這個(gè)在設(shè)計(jì)上我們一般是要避免的,也就是說盡管可以這么做认境,但我們不建議這么做。
FormBuilder 快速構(gòu)建表單
上面的表單構(gòu)造起來雖然也不算太麻煩挟鸠,但是在表單項(xiàng)目逐漸多起來之后還是一個(gè)挺麻煩的工作叉信,所以 Angular 提供了一種快捷構(gòu)造表單的方式 -- 使用 FormBuilder。
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
@Component({
selector: 'app-model-driven',
templateUrl: './model-driven.component.html',
styleUrls: ['./model-driven.component.css']
})
export class ModelDrivenComponent implements OnInit {
user: FormGroup;
constructor(private fb: FormBuilder) {
}
ngOnInit() {
// 初始化表單
this.user = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required],
repeat: ['', Validators.required],
address: this.fb.group({
province: [],
city: [],
area: [],
addr: []
})
});
}
// 省略其他部分
}
使用 FormBuilder 我們可以無需顯式聲明 FormControl 或 FormGroup 艘希。 FormBuilder 提供三種類型的快速構(gòu)造: control
, group
和 array
硼身,分別對(duì)應(yīng) FormControl, FormGroup 和 FormArray。 我們?cè)诒韱沃凶畛R姷囊环N是通過 group
來初始化整個(gè)表單覆享。上面的例子中佳遂,我們可以看到 group
接受一個(gè)字典對(duì)象作為參數(shù),這個(gè)字典中的 key 就是這個(gè) FormGroup 中 FormControl 的名字撒顿,值是一個(gè)數(shù)組丑罪,數(shù)組中的第一個(gè)值是控件的初始值,第二個(gè)是同步驗(yàn)證器的數(shù)組,第三個(gè)是異步驗(yàn)證器數(shù)組(第三個(gè)并未出現(xiàn)在我們的例子中)吩屹。這其實(shí)已經(jīng)在隱性的使用 FormBuilder.control
了跪另,可以參看下面的 FormBuilder 中的 control
函數(shù)定義,其實(shí) FormBuilder 利用我們給出的值構(gòu)造了相對(duì)應(yīng)的 control
:
control(
formState: Object,
validator?: ValidatorFn | ValidatorFn[],
asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]
): FormControl;
此外還值得注意的一點(diǎn)是 address 的處理煤搜,我們可以清晰的看到 FormBuilder 支持嵌套免绿,遇到 FormGroup 時(shí)僅僅需要再次使用 this.fb.group({...})
即可。這樣我們的表單在擁有大量的表單項(xiàng)時(shí)擦盾,構(gòu)造起來就方便多了嘲驾。
自定義驗(yàn)證
對(duì)于響應(yīng)式表單來說,構(gòu)造一個(gè)自定義驗(yàn)證器是非常簡(jiǎn)單的迹卢,比如我們上面提到過的的驗(yàn)證 密碼
和 重復(fù)輸入密碼
是否相同的需求距淫,我們?cè)陧憫?yīng)式表單中來試一下。
validateEqual(passwordKey: string, confirmPasswordKey: string): ValidatorFn {
return (group: FormGroup): {[key: string]: any} => {
const password = group.controls[passwordKey];
const confirmPassword = group.controls[confirmPasswordKey];
if (password.value !== confirmPassword.value) {
return { validateEqual: true };
}
return null;
}
}
這個(gè)函數(shù)的邏輯比較簡(jiǎn)單:我們接受兩個(gè)字符串(是 FormControl 的名字),然后返回一個(gè) ValidatorFn
婶希。但是這個(gè)函數(shù)里面就奇奇怪怪的榕暇,
比如 (group: FormGroup): {[key: string]: any} => {...}
是什么意思啊喻杈?還有彤枢,這個(gè) ValidatorFn
是什么鬼?我們來看一下定義:
export interface ValidatorFn {
(c: AbstractControl): ValidationErrors | null;
}
這樣就清楚了筒饰, ValidatorFn
是一個(gè)對(duì)象定義缴啡,這個(gè)對(duì)象中有一個(gè)方法,此方法接受一個(gè) AbstractControl
類型的參數(shù)(其實(shí)也就是我們的 FormControl瓷们,而 AbstractControl 為其父類)业栅,而這個(gè)方法還要返回 ValidationErrors
,這個(gè) ValidationErrors
的定義如下:
export declare type ValidationErrors = {
[key: string]: any;
};
回過頭來再看我們的這句 (group: FormGroup): {[key: string]: any} => {...}
谬晕,大家就應(yīng)該明白為什么這么寫了碘裕,我們其實(shí)就是在返回一個(gè) ValidatorFn
類型的對(duì)象。只不過我們利用 javascript/typescript
對(duì)象展開的特性把 ValidationErrors
寫成了 {[key: string]: any}
攒钳。
弄清楚這個(gè)函數(shù)的邏輯后帮孔,我們?cè)趺词褂媚兀糠浅:?jiǎn)單不撑,先看代碼:
this.user = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required],
repeat: ['', Validators.required],
address: this.fb.group({
province: [],
city: [],
area: [],
addr: []
})
}, {validator: this.validateEqual('password', 'repeat')});
和最初的代碼相比文兢,多了一個(gè)參數(shù),那就是 {validator: this.validateEqual('password', 'repeat')}
焕檬。FormBuilder 的 group
函數(shù)接受兩個(gè)參數(shù)姆坚,第一個(gè)就是那串長(zhǎng)長(zhǎng)的,我們叫它 controlsConfig
实愚,用于表單控件的構(gòu)造兼呵,以及每個(gè)表單控件的驗(yàn)證器兔辅。但是如果一個(gè)驗(yàn)證器是要計(jì)算多個(gè) field
的話,我們可以把它作為整個(gè) group
的驗(yàn)證器萍程。所以 FormBuilder 的 group
函數(shù)還接收第二個(gè)參數(shù)幢妄,這個(gè)參數(shù)中可以提供同步驗(yàn)證器或異步驗(yàn)證器。同樣還是一個(gè)字典對(duì)象茫负,是同步驗(yàn)證器的話蕉鸳,key
寫成 validator,異步的話寫成 asyncValidator
忍法。
現(xiàn)在我們可以保存代碼潮尝,啟動(dòng) ng serve
到瀏覽器中看一下結(jié)果了:
FormArray 有什么用?
我們?cè)谫?gòu)物網(wǎng)站經(jīng)常遇到需要維護(hù)多個(gè)地址饿序,因?yàn)槲覀冇行┥唐废M偷焦久闶В行┬枰偷郊依铮€有些給父母采購(gòu)的需要送到父母那里原探。這就是一個(gè)典型的 FormArray 可以派上用場(chǎng)的場(chǎng)景乱凿。所有的這些地址的結(jié)構(gòu)都是一樣的,有省咽弦、市徒蟆、區(qū)縣和街道地址,那么對(duì)于處理這樣的場(chǎng)景型型,我們來看看在響應(yīng)式表單中怎么做段审。
首先,我們需要把 HTML 模板改造一下闹蒜,現(xiàn)在的地址是多項(xiàng)了寺枉,所以我們需要在原來的地址部分外面再套一層,并且聲明成 formArrayName="addrs"
绷落。 FormArray 顧名思義是一個(gè)數(shù)組姥闪,所以我們要對(duì)這個(gè)控件數(shù)組做一個(gè)循環(huán),然后讓每個(gè)數(shù)組元素是 FormGroup嘱函,只不過這次我們的 [formGroupName]="i"
是讓 formGroupName
等于該數(shù)組元素的索引甘畅。
<div formArrayName="addrs">
<button (click)="addAddr()">Add</button>
<div *ngFor="let item of user.controls['addrs'].controls; let i = index;">
<div [formGroupName]="i">
<label>
<span>省份</span>
<select formControlName="province">
<option value="">請(qǐng)選擇省份</option>
<option [value]="province" *ngFor="let province of provinces">{{province}}</option>
</select>
</label>
<label>
<span>城市</span>
<select formControlName="city">
<option value="">請(qǐng)選擇城市</option>
<option [value]="city" *ngFor="let city of (cities$ | async)">{{city}}</option>
</select>
</label>
<label>
<span>區(qū)縣</span>
<select formControlName="area">
<option value="">請(qǐng)選擇區(qū)縣</option>
<option [value]="area" *ngFor="let area of (areas$ | async)">{{area}}</option>
</select>
</label>
<label>
<span>地址</span>
<input type="text" formControlName="street">
</label>
</div>
</div>
</div>
改造好模板后,我們需要在類文件中也做對(duì)應(yīng)處理往弓,去掉原來的 address: this.fb.group({...})
,換成 addrs: this.fb.array([])
蓄氧。而
this.user = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required],
repeat: ['', Validators.required],
addrs: this.fb.array([])
}, {validator: this.validateEqual('password', 'repeat')});
但這樣我們是看不到也增加不了新的地址的函似,因?yàn)槲覀冞€沒有處理添加的邏輯呢,下面我們就添加一下:其實(shí)就是建立一個(gè)新的 FormGroup喉童,然后加入 FormArray 數(shù)組中撇寞。
addAddr(): void {
(<FormArray>this.user.controls['addrs']).push(this.createAddrItem());
}
private createAddrItem(): FormGroup {
return this.fb.group({
province: [],
city: [],
area: [],
street: []
})
}
到這里我們的結(jié)構(gòu)就建好了,保存后,到瀏覽器中去試試添加多個(gè)地址吧蔑担!
響應(yīng)式表單的優(yōu)勢(shì)
首先是可測(cè)試能力牌废。模板驅(qū)動(dòng)型表單進(jìn)行單元測(cè)試是比較困難的,因?yàn)轵?yàn)證邏輯是寫在模板中的啤握。但驗(yàn)證器的邏輯單元測(cè)試對(duì)于響應(yīng)式表單來說就非常簡(jiǎn)單了鸟缕,因?yàn)槟愕尿?yàn)證器無非就是一個(gè)函數(shù)而已。
當(dāng)然除了這個(gè)優(yōu)點(diǎn)排抬,我們對(duì)表單可以有完全的掌控:從初始化表單控件的值懂从、更新和獲取表單值的變化到表單的驗(yàn)證和提交,這一系列的流程都在程序邏輯控制之下蹲蒲。
而且更重要的是番甩,我們可以使用函數(shù)響應(yīng)式編程的風(fēng)格來處理各種表單操作,因?yàn)轫憫?yīng)式表單提供了一系列支持 Observable
的接口 API 届搁。那么這又能說明什么呢缘薛?有什么用呢?
首先是無論表單本身還是控件都可以看成是一系列的基于時(shí)間維度的數(shù)據(jù)流了卡睦,這個(gè)數(shù)據(jù)流可以被多個(gè)觀察者訂閱和處理宴胧,由于 valueChanges
本身是個(gè) Observable
,所以我們就可以利用 RxJS 提供的豐富的操作符么翰,將一個(gè)對(duì)數(shù)據(jù)驗(yàn)證牺汤、處理等的完整邏輯清晰的表達(dá)出來。當(dāng)然現(xiàn)在我們不會(huì)對(duì) RxJS 做深入的討論浩嫌,后面有專門針對(duì) RxJS 進(jìn)行講解的章節(jié)檐迟。
this.form.valueChanges
.filter((value) => this.user.valid)
.subscribe((value) => {
console.log("現(xiàn)在時(shí)刻表單的值為 ",JSON.stringify(value));
});
上面的例子中,我們?nèi)〉帽韱沃档淖兓肽停缓筮^濾掉表單存在非法值的情況追迟,然后輸出表單的值。這只是非常簡(jiǎn)單的一個(gè) Rx 應(yīng)用骚腥,隨著邏輯復(fù)雜度的增加敦间,我們后面會(huì)見證 Rx 卓越的處理能力。
慕課網(wǎng) Angular 視頻課上線: http://coding.imooc.com/class/123.html?mc_marking=1fdb7649e8a8143e8b81e221f9621c4a&mc_channel=banner