螢幕上彩色語法高亮的程式碼特寫

← INSIGHTS & PERSPECTIVES | 前端開發

Angular Reactive Forms 教學:Model-Driven Forms 的驗證、FormBuilder 與 FormArray

我整理 Angular Reactive Forms(Model-Driven Forms)的完整筆記:與 Template-Driven Forms 的同步/非同步差異、FormControl、FormGroup、FormArray 四大成員、FormBuilder 用法、Validators 驗證器、setValue/patchValue 填值與巢狀表單的實作範例。

Angular 的表單有兩條路線:Template-Driven Forms 與 Reactive Forms(又稱 Model-Driven Forms)。我在寫需要複雜驗證的表單時,更偏好把驗證邏輯集中在 controller 裡的 Reactive Forms——測試好寫、資料同步即時,維護起來也清楚。這篇整理兩者的比較、四大核心成員,以及 FormBuilder、Validators、setValue/patchValue 和 FormArray 的實際用法。

Template-Driven Forms 與 Model-Driven Forms 有什麼不同?

Reactive forms

Reactive forms 的驗證大多是直接寫在 controller 裡的,會是一個明確的、非 UI 的 data flowing。Reactive forms 的 reactive patterns 可以讓測試與驗證更加簡單。

使用 Reactive forms 可以用一個樹狀的控制物件來 binding 到表單 template 的元件上,這讓所有驗證的程式碼都集中在一起,方便維護與管理,在撰寫單元測試時也會較為容易。

使用 Model-Driven Forms 也較符合 reactive programming 的概念(延伸閱讀:Functional Reactive Programming 的入門心得)。

Template-driven forms

Template-driven forms 是將組件驗證控制的功能寫在像是 `<input>` 或 `<select>` 的標籤內,並利用 `ngModel` 來確認是否輸入了合法的內容。

使用表單驅動驗證不需要自己創建 control objects,因為 Angular 已經為我們建好了。

`ngModel` 會處理使用者改變與輸入表單的事件,並更新 `ngModel` 裡面的可變數據,讓我們可以去處理後續的事。也因此 `ngModel` 並不是 `ReactiveFormsModule` 的一部分。

這代表著使用表單驅動驗證,我們需要撰寫的程式碼更少。

但是如果我們的表單需要很複雜的驗證步驟並且要顯示很多不同的錯誤訊息時,使用表單驅動驗證會使事情變得更複雜並難以維護。

最大的差異,同步與非同步

Reactive forms 是同步的,而 Template-driven forms 為非同步處理,是這兩者間最大的差異。

比較項目Reactive formsTemplate-driven forms
驗證邏輯位置controller(集中管理)template 標籤內的 directive
資料處理同步、即時更新非同步,需等待一段時間才有檢查結果
control objects需自己創建Angular 透過 directive 自動建立
適合場景複雜驗證、單元測試簡單表單、少寫程式碼

對 Reactive forms 來說,所有表單的資料是在 code 裡以 tree 的方式來呈現,所以在任一個節點可以取得其他表單的資料,並且這些資料是即時同步被更新的。我們也可以在使用者修改了某個 input 的值時,去為使用者自動 update 另一個 input 內的預設值,這是因為所有資料都是隨時可取得的。

Template-driven forms 在每一個表單元件各自透過 directive 委派檢查的功能,為了避免檢查後修改而造成檢查失效的問題,directive 會在更多的時候去檢查輸入的值的正確性,因此並沒有辦法立即的得到回應,而需要一小段的時間才有辦法得到使用者輸入的值是否合法的回應。這會讓我們在撰寫單元測試時更加複雜,我們會需要利用 `setTimeout` 去讓取得的檢查結果是正確的。

如何開始使用 Reactive Forms?

Reactive Forms 的功能封裝在 `ReactiveFormsModule` 中,和 `FormsModule` 同樣在 `@angular/forms` 之下。如果要使用 Reactive Forms 需要使用下面的程式碼:

```ts

import { ReactiveFormsModule } from '@angular/forms';

```

Reactive Forms 的四個重要成員是哪些?

  • AbstractControl:AbstractControl 是 FormControl、FormGroup、FormArray 這三個實例表單類的抽象基類。它提供了它們的通用行為以及屬性,例如 observable。
  • FormControl:在單個表單元件中檢查值並驗證狀態(比如 input、select 等等)。
  • FormGroup:一組值與驗證狀態(FormControl),其屬性包含了它們的子控件。例如一個 form 表單就是一個 FormGroup。
  • FormArray:用索引的方式去追蹤檢查表單的驗證狀態。

如何新增一個 FormGroup?

首先要新增 FormGroup 所需使用的類別:

```ts

import { Component } from '@angular/core';

import { FormControl, FormGroup } from '@angular/forms';

```

然後創建這個 Group,並定義裡面的驗證元素:

```ts

export class HeroDetailComponent2 {

heroForm = new FormGroup ({

name: new FormControl()

});

}

```

接著,在 template 裡面的 form 裡指定這個 form 要使用 `heroForm` 來做表單驗證,並且在 input 裡面指定它的 `formControlName`:

```html

Hero Detail

FormControl in a FormGroup

<label class="center-block">Name:

```

form 標籤下的 `novalidate` 屬性,是為了要防止瀏覽器自己執行 native 的驗證:

```html

```

而 `[formGroup]="heroForm"` 則是將 template 內的 form 元件與 controller 裡所創的 formGroup 做關連:

```html

```

這個則是將 input 與 formGroup 下名為 name 的 formControl 做關連。

註:Bootstrap 的 `form-group` 以及 `form-control` 與 Angular 完全無關。下面是 Bootstrap 為我們設計的 form 表單樣式範例,但是這只是 CSS,沒辦法讓組件與控制器結合:

```html

```

Bootstrap 表單樣式範例的呈現結果

FormBuilder 怎麼減少重複的程式碼?

FormBuilder 可以減少我們在創建 formGroup 時有太多重複的定義,要使用要先 import 必要的檔案:

```ts

import { Component } from '@angular/core';

import { FormBuilder, FormGroup } from '@angular/forms';

```

使用 FormBuilder 大致要做的事如下:

  • 宣告 `heroForm` 為 FormGroup
  • 在初始化元件時 inject FormBuilder
  • 創建 form 控件時需要另外去呼叫函數 `createForm`,使用注入的 FormBuilder 來創建 formControl
  • 在創建 formGroup 時,用 `this.fb.group` 來宣告這個 formGroup 裡所有的 formControl

```ts

export class HeroDetailComponent3 {

heroForm: FormGroup; // <--- 宣告heroForm為FormGroup

constructor(private fb: FormBuilder) { // <--- 注入FormBuilder

this.createForm();

}

createForm() {

this.heroForm = this.fb.group({

name: '', // <--- 建立一個名為name,預設值為''的formControl

});

}

}

```

FormBuilder 的宣告方式如上,name 控件由其初始數據值(一個空字串)定義。

如何使用驗證器(Validators)?

首先要先 import 該驗證器:

```ts

import { Component } from '@angular/core';

import { FormBuilder, FormGroup, Validators } from '@angular/forms';

```

然後在建立 formControl 時指定使用該驗證器:

```ts

this.heroForm = this.fb.group({

name: ['', Validators.required ],

});

```

什麼時候需要 Nested FormGroups?

有時我們在做地址輸入框時,會有如國家、區、鄉、市、街、郵遞區號等不同的輸入欄位,但它們應該是一個 group,這時候就可以用 nested formGroup。透過這樣的結構層次,可以讓我們在追蹤表格狀態更為容易清楚:

```ts

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"` 來與 heroForm 裡的 address 做連結:

```html

Secret Lair

<label class="center-block">Street:

<label class="center-block">City:

<label class="center-block">State:

<label class="center-block">Zip Code:

```

FormControl 有哪些常用屬性可以用?

我們可以用下面的方式來將 formControl 裡可使用的屬性都印出來:

```html

Form value: {{ heroForm.value | json }}

```

基本上會有下面四個屬性可以讓我們使用:

屬性描述
`myControl.value`FormControl 使用者輸入的值
`myControl.status`這個 FormControl 的驗證結果。可能的值有:`VALID`、`INVALID`、`PENDING` 或 `DISABLED`。
`myControl.pristine`使用者是否有在 UI 上更動過這個元素,假如沒有的話會是 true。相反的屬性為 `myControl.dirty`。
`myControl.untouched`使用者尚未輸入並且從未觸發過 blur event 時為 true。相反的屬性為 `myControl.touched`。

Data model 和 form model 有什麼差別?

以往我們在創建資料類型時是像這樣子的:

```ts

export class Hero {

id = 0;

name = '';

addresses: Address[];

}

export class Address {

street = '';

city = '';

state = '';

zip = '';

}

```

但是我們在創建 formGroup 是這樣子的:

```ts

this.heroForm = this.fb.group({

name: ['', Validators.required ],

address: this.fb.group({

street: '',

city: '',

state: '',

zip: ''

}),

power: '',

sidekick: ''

});

```

我們可以直接利用 class 來創建 formControl:

```ts

this.heroForm = this.fb.group({

name: ['', Validators.required ],

address: this.fb.group(new Address()), // <-- a FormGroup with a new address

power: '',

sidekick: ''

});

```

如何用 setValue 和 patchValue 為表單填入初始值?

在上面 data model 和 form model 的介紹範例中,可以看到 Hero 與 formGroup 建立 heroForm 模型有兩個顯著的區別:

  • Hero class 有 `id`,formGroup 沒有。
  • Hero class 的地址是一個陣列。

但是我們可以利用 `setValue` 來更簡單的將一個 class 的資料填進表單中:

```ts

this.heroForm.setValue({

name: this.hero.name,

address: this.hero.addresses[0] || new Address() // 這是因為form元件只能顯示一個地址,如果class內容沒有值時,要預設新建立一個Address物件

});

```

也可以使用 `patchValue` 來將單一的值填入表單裡:

```ts

this.heroForm.patchValue({

name: this.hero.name

});

```

如果我們要做一個修改 hero 資料的列表,當點下某個 hero 時就可以修改該 hero 的資料:

```html

```

然後在 controller 裡面去監聽 `ngOnChanges` 事件並且用 setValue 來設定要修改的值:

```ts

ngOnChanges() {

this.heroForm.reset({

name: this.hero.name,

address: this.hero.addresses[0] || new Address()

});

}

```

會需要使用 `reset` 是為了要清除前一個 hero 的資料。

如何使用 FormArray 呈現多組 FormGroup?

如果一個 hero 可能需要有多組的地址時,就會需要使用 FormArray。

原本我們是這樣定義 Address 的:

```ts

this.heroForm = this.fb.group({

name: ['', Validators.required ],

address: this.fb.group(new Address()), // <-- a FormGroup with a new address

power: '',

sidekick: ''

});

```

使用 FormArray 則變成這樣:

```ts

this.heroForm = this.fb.group({

name: ['', Validators.required ],

secretLairs: this.fb.array([]), // <-- secretLairs as an empty FormArray

power: '',

sidekick: ''

});

```

可以用下面的 function 將很多組的 address 設定進去 formArray 成為預設值:

```ts

setAddresses(addresses: Address[]) {

const addressFGs = addresses.map(address => this.fb.group(address));

const addressFormArray = this.fb.array(addressFGs);

this.heroForm.setControl('secretLairs', addressFormArray);

}

```

要取得 formArray 可以撰寫下面的方法:

```ts

get secretLairs(): FormArray {

return this.heroForm.get('secretLairs') as FormArray;

};

```

而顯示方式如下,完整內容如下:

```html

Address #{{i + 1}}

<label class="center-block">Street:

<label class="center-block">City:

<label class="center-block">State:

<label class="center-block">Zip Code:


```

要為這個 hero 新增一個地址可以用下面這個方法:

```ts

addLair() {

this.secretLairs.push(this.fb.group(new Address()));

}

```

按下增加地址按鈕時呼叫這個方法:

```html

```

將 formControl 的資料用深層複製存回 class 裡的方法:

```ts

prepareSaveHero(): Hero {

const formModel = this.heroForm.value;

// deep copy of form model lairs

const secretLairsDeepCopy: Address[] = formModel.secretLairs.map(

(address: Address) => Object.assign({}, address)

);

// return new `Hero` object containing a combination of original hero value(s)

// and deep copies of changed form model values

const saveHero: Hero = {

id: this.hero.id,

name: formModel.name as string,

// addresses: formModel.secretLairs // <-- bad!

addresses: secretLairsDeepCopy

};

return saveHero;

}

```

延伸閱讀

常見問題

QReactive Forms 和 Template-Driven Forms 最大的差異是什麼?

最大的差異在於同步與非同步:Reactive forms 的表單資料以樹狀結構存在 code 裡,任一節點都能即時同步取得其他表單資料;Template-driven forms 則透過 directive 非同步檢查,需要一小段時間才能取得驗證結果,單元測試也因此更複雜。

Q什麼時候該選 Reactive Forms?

當表單需要複雜的驗證步驟、要顯示多種錯誤訊息,或需要撰寫單元測試時,建議使用 Reactive Forms。所有驗證程式碼集中在 controller,方便維護管理,測試也更簡單。簡單的小表單用 Template-driven forms 可以少寫很多程式碼。

Q`novalidate` 屬性的作用是什麼?

在 `<form>` 標籤加上 `novalidate` 是為了防止瀏覽器自己執行原生的表單驗證,把驗證權交給 Angular 的 Reactive Forms 機制處理。

QsetValue 和 patchValue 有什麼不同?

`setValue` 會填入整個表單的值,適合一次把 class 的完整資料填進表單;`patchValue` 則可以只填入單一部分的值,不需要提供所有欄位。

Q什麼情況需要用 FormArray?

當同一組欄位需要重複出現多份時(例如一個 hero 有多組地址),就用 FormArray 以索引方式追蹤每組表單的驗證狀態,並可透過 `push()` 動態新增一組地址。

參考資料

最後更新

2026-08-28(原文發布於 2018-01-05,本文保留原始筆記內容並補上 GEO 結構。)

關於作者 {#author}

Claire Chang | 企業 AI 導入與流程轉型顧問。專注於 AI Agent 架構設計、ERP 系統整合與企業 AI 治理。

首次發布:2018-01-05