本篇文章帶大家了解一下angular中的表單,聊聊響應式表單與模板驅(qū)動表單,介紹一下響應式表單怎么驗證表單輸入,希望對大家有所幫助!
一、angular表單簡介
Angular 提供了兩種不同的方法來通過表單處理用戶輸入:響應式表單
和模板驅(qū)動表單
。 兩者都從視圖中捕獲用戶輸入事件、驗證用戶輸入、創(chuàng)建表單模型、修改數(shù)據(jù)模型,并提供跟蹤這些更改的途徑。【相關教程推薦:《angular教程》】
1.1 響應式表單與模板驅(qū)動表單的差異
響應式表單
提供對底層表單對象模型直接
、顯式
的訪問。它們與模板驅(qū)動表單相比,更加健壯:它們的可擴展性、可復用性和可測試性
都更高。如果表單是你的應用程序的關鍵部分,或者你已經(jīng)在使用響應式表單來構(gòu)建應用,那就使用響應式表單。模板驅(qū)動表單
依賴模板中的指令
來創(chuàng)建和操作底層的對象模型。它們對于向應用添加一個簡單的表單非常有用,比如電子郵件列表注冊表單。它們很容易添加到應用中,但在擴展性方面不如響應式表單。如果你有可以只在模板中管理的非?;镜谋韱涡枨蠛瓦壿嫞敲茨0弪?qū)動表單就很合適。
響應式 | 模板驅(qū)動 | |
---|---|---|
建立表單模型 | 顯式的,在組件類中創(chuàng)建 | 隱式的,由指令創(chuàng)建 |
數(shù)據(jù)模型 | 結(jié)構(gòu)化和不可變的 | 非結(jié)構(gòu)化和可變的 |
可預測性 | 同步 | 異步 |
表單驗證 | 函數(shù) | 指令 |
1.2 建立表單模型
響應式表單和模板驅(qū)動型表單都會跟蹤用戶與之交互的表單輸入元素和組件模型中的表單數(shù)據(jù)之間的值變更。這兩種方法共享同一套底層構(gòu)建塊
,只在如何創(chuàng)建
和管理
常用表單控件實例
方面有所不同。
1.3 常用表單基礎類
響應式表單和模板驅(qū)動表單都建立在下列基礎類之上。
- FormControl 實例用于追蹤單個表單控件的值和驗證狀態(tài)。
- FormGroup 用于追蹤一個表單控件組的值和狀態(tài)。
- FormArray 用于追蹤表單控件數(shù)組的值和狀態(tài)。
- ControlValueAccessor 用于在 Angular 的 FormControl 實例和原生 DOM 元素之間創(chuàng)建一個橋梁。
二、 響應式表單
響應式表單使用顯式的、不可變的方式,管理表單在特定的時間點上的狀態(tài)。對表單狀態(tài)的每一次變更都會返回一個新的狀態(tài),這樣可以在變化時維護模型的整體性。響應式表單是圍繞 Observable
流構(gòu)建的,表單的輸入和值都是通過這些輸入值組成的流來提供的,它可以同步
訪問。
2.1 添加基礎表單控件
使用表單控件有三個步驟。
-
在你的應用中注冊響應式表單模塊。該模塊聲明了一些你要用在響應式表單中的指令。
-
生成一個新的 FormControl 實例,并把它保存在組件中。
-
在模板中注冊這個 FormControl。
要使用響應式表單控件,就要從 @angular/forms 包中導入 ReactiveFormsModule,并把它添加到你的 NgModule 的 imports 數(shù)組中。
import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ imports: [ // other imports ... ReactiveFormsModule ], }) export class AppModule { }
要注冊一個表單控件,就要導入 FormControl 類并創(chuàng)建一個 FormControl 的新實例,將其保存為類的屬性。
import { Component } from '@angular/core'; import { FormControl } from '@angular/forms'; @Component({ selector: 'app-name-editor', templateUrl: './name-editor.component.html', styleUrls: ['./name-editor.component.css'] }) export class NameEditorComponent { name = new FormControl(''); }
可以用 FormControl 的構(gòu)造函數(shù)設置初始值
,這個例子中它是空字符串
。通過在你的組件類中創(chuàng)建這些控件,你可以直接對表單控件的狀態(tài)進行監(jiān)聽
、修改
和校驗
。
在組件類中創(chuàng)建了控件之后,你還要把它和模板中的一個表單控件關聯(lián)起來。修改模板,為表單控件添加 formControl 綁定,formControl 是由 ReactiveFormsModule 中的 FormControlDirective 提供的。
<label> Name: <input type="text" [formControl]="name"> </label>
2.2 顯示表單控件的值
你可以用下列方式顯示它的值:
- 通過可觀察對象
valueChanges
,你可以在模板中使用AsyncPipe
或在組件類中使用subscribe()
方法來監(jiān)聽表單值的變化。 - 使用 value 屬性。它能讓你獲得當前值的一份快照。
<label> Name: <input type="text" [formControl]="name"> </label> <p>Value: {{ name.value }}</p>
public name = new FormControl('test'); public testValueChange() { this.name.valueChanges.subscribe({ next: value => { console.log("name value is: " + value); } }) }
2.3 替換表單控件的值
響應式表單還有一些方法可以用編程的方式``修改
控件的值,它讓你可以靈活的修改控件的值而不需要借助用戶交互。FormControl 提供了一個 setValue()
方法,它會修改這個表單控件的值,并且驗證與控件結(jié)構(gòu)相對應的值的結(jié)構(gòu)。比如,當從后端 API 或服務接收到了表單數(shù)據(jù)時,可以通過 setValue() 方法來把原來的值替換為新的值。
updateName() { this.name.setValue('Nancy' + new Date().getTime()); }
<p> <button (click)="updateName()">Update Name</button> </p>
2.4 把表單控件分組
表單中通常會包含幾個相互關聯(lián)的控件
。響應式表單提供了兩種把多個相關控件分組到同一個輸入表單中的方法。
表單組
定義了一個帶有一組控件的表單,你可以把它們放在一起管理。表單組的基礎知識將在本節(jié)中討論。你也可以通過嵌套表單組
來創(chuàng)建更復雜的表單。表單數(shù)組
定義了一個動態(tài)表單,你可以在運行時添加和刪除控件。你也可以通過嵌套表單數(shù)組
來創(chuàng)建更復雜的表單
要將表單組添加到此組件中,請執(zhí)行以下步驟。
-
創(chuàng)建一個
FormGroup
實例。 -
把這個 FormGroup 模型關聯(lián)到視圖。
-
保存表單數(shù)據(jù)。
在組件類中創(chuàng)建一個名叫 profileForm 的屬性,并設置為 FormGroup 的一個新實例。要初始化這個 FormGroup,請為構(gòu)造函數(shù)提供一個由控件組成的對象,對象中的每個名字都要和表單控件的名字一一對應。
import { FormControl, FormGroup } from '@angular/forms'; profileForm = new FormGroup({ firstName: new FormControl(''), lastName: new FormControl(''), }); // 可以整個獲取值 public onSubmit() { // TODO: Use EventEmitter with form value console.warn(this.profileForm.value);// {firstName: "", lastName: ""} } // 可以借助 valueChanges 整個可觀察對象整個獲取值 this.profileForm.valueChanges.subscribe( { next: value => { console.log("name value is: " + JSON.stringify(value)); // dashboard.component.ts:53 name value is: {"firstName":"dddd","lastName":"bb"} } }) // 可以通過后期單個控件單獨獲取值 this.profileForm.get('firstName').valueChanges.subscribe({ next: value => { console.log("First Name is: " + value); // First Name is: aa }
ps: 這個 FormGroup 用對象的形式提供了它的模型值,這個值來自組中每個控件的值
。 FormGroup 實例擁有和 FormControl 實例相同的屬性
(比如 value、untouched)和方法(比如 setValue())。
這個表單組還能跟蹤其中每個控件的狀態(tài)及其變化,所以如果其中的某個控件的狀態(tài)或值變化了,父控件也會發(fā)出一次新的狀態(tài)變更或值變更事件。該控件組的模型來自它的所有成員。在定義了這個模型之后,你必須更新模板,來把該模型反映到視圖中。
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()"> <label> First Name: <input type="text" formControlName="firstName"> </label> <label> Last Name: <input type="text" formControlName="lastName"> </label> <button type="submit" [disabled]="!profileForm.valid">Submit</button> </form>
2.5 創(chuàng)建嵌套的表單組
表單組可以同時接受單個表單控件實例和其它表單組實例作為其子控件。這可以讓復雜的表單模型更容易維護,并在邏輯上把它們分組到一起。
要制作更復雜的表單,請遵循如下步驟。
-
創(chuàng)建一個嵌套的表單組
-
板中對這個嵌套表單分組。
要在 profileForm 中創(chuàng)建一個嵌套組,就要把一個嵌套的 address 元素添加到此表單組的實例中。
public profileForm = new FormGroup({ firstName: new FormControl(''), lastName: new FormControl(''), address: new FormGroup({ street: new FormControl(''), city: new FormControl(''), state: new FormControl(''), zip: new FormControl('') }) }); // 可以借助 valueChanges 整個可觀察對象整個獲取值 this.profileForm.valueChanges.subscribe( { next: value => { console.log("name value is: " + JSON.stringify(value));// name value is: {"firstName":"","lastName":"","address":{"street":"b","city":"","state":"","zip":""}} } }); // 可以通過后期單個控件單獨獲取值 this.profileForm.get('firstName').valueChanges.subscribe({ next: value => { console.log("First Name is: " + value); } }); // 可以獲取form組件某個form組的整個值 this.profileForm.get('address').valueChanges.subscribe(({ next: value => { console.log('address value is: ' + JSON.stringify(value));// address value is: {"street":"b","city":"","state":"","zip":""} } })); // 可以獲取form組件某個form組的某個formcontrol實例的值 this.profileForm.get('address').get('street').valueChanges.subscribe(({ next: value => { console.log('street value is: ' + value);// street value is: b } }));
在修改了組件類中的模型之后,還要修改模板,來把這個 FormGroup 實例對接到它的輸入元素。
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()"> <label> First Name: <input type="text" formControlName="firstName"> </label> <label> Last Name: <input type="text" formControlName="lastName"> </label> <div formGroupName="address"> <h3>Address</h3> <label> Street: <input type="text" formControlName="street"> </label> <label> City: <input type="text" formControlName="city"> </label> <label> State: <input type="text" formControlName="state"> </label> <label> Zip Code: <input type="text" formControlName="zip"> </label> </div> <button type="submit" [disabled]="!profileForm.valid">Submit</button> </form>
2.6 更新部分數(shù)據(jù)模型
當修改包含多個 FormGroup 實例的值時,你可能只希望更新模型中的一部分,而不是完全替換掉。
有兩種更新模型值的方式:
- 使用
setValue()
方法來為單個控件
設置新值。 setValue() 方法會嚴格遵循表單組的結(jié)構(gòu)
,并整體性替換控件的值
。 - 使用
patchValue()
方法可以用對象中所定義的任何屬性
為表單模型進行替換。
setValue() 方法的嚴格檢查可以幫助你捕獲復雜表單嵌套中的錯誤,而 patchValue() 在遇到那些錯誤時可能會默默的失敗。
public updateProfile() { // profileForm 模型中只有 firstName 和 street 被修改了。注意,street 是在 address 屬性的對象中被修改的。這種結(jié)構(gòu)是必須的,因為 patchValue() 方法要針對模型的結(jié)構(gòu)進行更新。patchValue() 只會更新表單模型中所定義的那些屬性。 this.profileForm.patchValue({ firstName: 'Nancy' + new Date().getTime(), address: { street: '123 Drew Street' + new Date().getTime() } }); // ERROR Error: Must supply a value for form control with name: 'lastName'. // setValue() 方法會嚴格遵循表單組的結(jié)構(gòu) this.profileForm.setValue({ firstName: 'Nancy' + new Date().getTime(), address: { street: '123 Drew Street' + new Date().getTime() } }); }
2.7 創(chuàng)建動態(tài)表單
FormArray 是 FormGroup 之外的另一個選擇,用于管理任意數(shù)量的匿名控件。像 FormGroup 實例一樣,你也可以往 FormArray 中動態(tài)插入和移除控件,并且 FormArray 實例的值和驗證狀態(tài)也是根據(jù)它的子控件計算得來的。 不過,你不需要為每個控件定義一個名字作為 key,因此,如果你事先不知道子控件的數(shù)量,這就是一個很好的選擇。
要定義一個動態(tài)表單,請執(zhí)行以下步驟。
-
導入 FormArray 類。
-
定義一個 FormArray 控件。
-
使用 getter 方法訪問 FormArray 控件。
-
在模板中顯示這個表單數(shù)組
通過把一組(從零項到多項)控件定義在一個數(shù)組中來初始化一個 FormArray。為 profileForm 添加一個 aliases 屬性,把它定義為 FormArray 類型。
import { FormControl, FormGroup, FormArray } from '@angular/forms'; public profileForm = new FormGroup({ firstName: new FormControl(''), lastName: new FormControl(''), address: new FormGroup({ street: new FormControl(''), city: new FormControl(''), state: new FormControl(''), zip: new FormControl('') }), aliases: new FormArray([ new FormControl('1') ]) }); public aliases = (<FormArray>this.profileForm.get('aliases')); public addAlias() { (<FormArray>this.profileForm.get('aliases')).push(new FormControl('1')); } // 獲取整個 formArray 的數(shù)據(jù) this.profileForm.get('aliases').valueChanges.subscribe({ next: value => { console.log('aliases values is: ' + JSON.stringify(value)); // aliases values is: ["1","3"] } }); // 獲取 formArray 中單個 formControl 的數(shù)據(jù) (<FormArray>this.profileForm.get('aliases')).controls[0].valueChanges.subscribe({ next: value => { console.log('aliases[0] values is: ' + value); // aliases[0] values is: 0 } })
要想為表單模型添加 aliases,你必須把它加入到模板中供用戶輸入。和 FormGroupNameDirective 提供的 formGroupName 一樣,F(xiàn)ormArrayNameDirective 也使用 formArrayName 在這個 FormArray 實例和模板之間建立綁定
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()"> <label> First Name: <input type="text" formControlName="firstName"> </label> <label> Last Name: <input type="text" formControlName="lastName"> </label> <div formGroupName="address"> <h3>Address</h3> <label> Street: <input type="text" formControlName="street"> </label> <label> City: <input type="text" formControlName="city"> </label> <label> State: <input type="text" formControlName="state"> </label> <label> Zip Code: <input type="text" formControlName="zip"> </label> </div> <div formArrayName="aliases"> <h3>Aliases</h3> <button (click)="addAlias()">Add Alias</button> <div *ngFor="let alias of aliases.controls; let i=index"> <!-- The repeated alias template --> <label> Alias: <input type="text" [formControlName]="i"> </label> </div> </div> </form>
2.8 響應式表單 API 匯總
類 | 說明 |
---|---|
AbstractControl | 所有三種表單控件類(FormControl、FormGroup 和 FormArray)的抽象基類。它提供了一些公共的行為和屬性。 |
FormControl | 管理單體表單控件的值和有效性狀態(tài)。它對應于 HTML 的表單控件,比如 或 。 |
FormGroup | 管理一組 AbstractControl 實例的值和有效性狀態(tài)。該組的屬性中包括了它的子控件。組件中的頂層表單就是 FormGroup。 |
FormArray | 管理一些 AbstractControl 實例數(shù)組的值和有效性狀態(tài)。 |
FormBuilder | 一個可注入的服務,提供一些用于提供創(chuàng)建控件實例的工廠方法。 |
三、模板驅(qū)動表單
在模板驅(qū)動表單中,表單模型是隱式的,而不是顯式的。指令 NgModel 為指定的表單元素創(chuàng)建并管理一個 FormControl 實例。
下面的組件使用模板驅(qū)動表單為單個控件實現(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 = ''; }
四、響應式表單驗證表單輸入
在組件類中直接
把驗證器函數(shù)添加到表單控件模型
上(FormControl)。然后,一旦控件發(fā)生了變化,Angular 就會調(diào)用這些函數(shù)。
4.1 驗證器(Validator)函數(shù)
驗證器函數(shù)可以是同步函數(shù),也可以是異步函數(shù)。
- 同步驗證器:這些同步函數(shù)接受一個控件實例,然后返回
一組驗證錯誤或 null
。你可以在實例化一個 FormControl 時把它作為構(gòu)造函數(shù)的第二個參數(shù)
傳進去。 - 異步驗證器 :這些異步函數(shù)接受一個控件實例并返回
一個 Promise 或 Observable
,它稍后
會發(fā)出一組驗證錯誤或 null
。在實例化 FormControl 時,可以把它們作為第三個參數(shù)
傳入。
出于性能方面的考慮,只有在所有同步驗證器都通過之后,Angular 才會運行異步驗證器。當每一個異步驗證器都執(zhí)行完之后,才會設置這些驗證錯誤。
4.2 內(nèi)置驗證器函數(shù)
在模板驅(qū)動表單中用作屬性的那些內(nèi)置驗證器,比如 required 和 minlength,也都可以作為 Validators 類中的函數(shù)使用
public profileForm = new FormGroup({ firstName: new FormControl('', [ Validators.required ]), }); this.profileForm.get('firstName').valueChanges.subscribe({ next: value => { console.log("First Name is: " + value); console.log(this.profileForm.get('firstName').errors);// { required: true } | null } });
<form [formGroup]="profileForm"> <label> First Name: <input type="text" formControlName="firstName"> <div *ngIf="firstName.errors?.required"> Name is required. </div> </label> </form>
4.3 定義自定義驗證器
內(nèi)置的驗證器并不是總能精確匹配應用中的用例,因此有時你需要創(chuàng)建一個自定義驗證器。
public profileForm = new FormGroup({ firstName: new FormControl('', [ Validators.required, this.forbiddenNameValidator(/bob/i) ]) }); public forbiddenNameValidator(nameRe: RegExp): ValidatorFn { return (control: AbstractControl): {[key: string]: any} | null => { const forbidden = nameRe.test(control.value); return forbidden ? {forbiddenName: {value: control.value}} : null; }; } get firstName() { return this.profileForm.get('firstName'); } this.profileForm.get('firstName').valueChanges.subscribe({ next: value => { console.log("First Name is: " + value); // First Name is: bob console.log(JSON.stringify(this.profileForm.get('firstName').errors));// {"forbiddenName":{"value":"bob"}} | null } });
4.4 跨字段交叉驗證
跨字段交叉驗證器是一種自定義驗證器
,可以對表單中不同字段的值進行比較,并針對它們的組合進行接受或拒絕。
下列交叉驗證的例子說明了如何進行如下操作:
- 根據(jù)兩個兄弟控件的值驗證響應式表單或模板驅(qū)動表單的輸入,
- 當用戶與表單交互過,且驗證失敗后,就會顯示描述性的錯誤信息
要想在單個自定義驗證器中計算這兩個控件,你就必須在它們共同的祖先控件中執(zhí)行驗證: FormGroup。你可以在 FormGroup 中查詢它的子控件,從而讓你能比較它們的值。要想給 FormGroup 添加驗證器,就要在創(chuàng)建時把一個新的驗證器傳給它的第二個參數(shù)。
this.profileForm.valueChanges.subscribe( { next: value => { console.log(JSON.stringify(this.profileForm.errors));// {"identityRevealed":true} | null } }); public profileForm = new FormGroup({ firstName: new FormControl('', [ Validators.required, ]), lastName: new FormControl(''), }, { validators: this.identityRevealedValidator}); public identityRevealedValidator(control: FormGroup): ValidationErrors | null{ const firstName = control.get('firstName'); const lastName = control.get('lastName'); return firstName && lastName && firstName.value === lastName.value ? { identityRevealed: true } : null; };
4.5 創(chuàng)建異步驗證器
異步驗證器實現(xiàn)了 AsyncValidatorFn
和 AsyncValidator
接口。它們與其同步版本非常相似,但有以下不同之處。
- validate() 函數(shù)必須返回一個
Promise 或可觀察對象
, - 返回的可觀察對象必須是
有盡
的,這意味著它必須在某個時刻完成(complete)
。要把無盡的可觀察對象轉(zhuǎn)換成有盡的,可以在管道中加入過濾操作符,比如 first、last、take 或 takeUntil。
異步驗證在同步驗證完成后才會發(fā)生
,并且只有在同步驗證成功時才會執(zhí)行。如果更基本的驗證方法已經(jīng)發(fā)現(xiàn)了無效輸入,那么這種檢查順序就可以讓表單避免使用昂貴的異步驗證流程(例如 HTTP 請求)。
4.6 觸發(fā)某個formControlName
let formControl = this.profileForm.get('firstName'); formControl.updateValueAndValidity();