Angular 提供三種建置表單的方法:Signal Forms、Reactive Forms 與 Template-driven Forms。三者在管理狀態、驗證與資料流的方式上各有不同的模式。本指南協助你了解彼此的差異,為你的專案選出合適的方法。
了解差異最好的方式,是看同一個表單以三種方法各實作一次。
import {Component, signal} from '@angular/core';
import {
email,
form,
FormField,
FormRoot,
minLength,
required,
submit,
} from '@angular/forms/signals';
@Component({
selector: 'app-login',
imports: [FormField, FormRoot],
template: `
<form [formRoot]="loginForm" (submit)="onSubmit()">
<div>
<label>
Email
<input type="email" [formField]="loginForm.email" />
</label>
@if (loginForm.email().touched() && loginForm.email().invalid()) {
<span class="error">
{{ loginForm.email().errors()[0].message }}
</span>
}
</div>
<div>
<label>
Password
<input type="password" [formField]="loginForm.password" />
</label>
@if (loginForm.password().touched() && loginForm.password().invalid()) {
<span class="error">
{{ loginForm.password().errors()[0].message }}
</span>
}
</div>
<button type="submit" [disabled]="loginForm().invalid()">Sign In</button>
</form>
`,
})
export class LoginComponent {
loginModel = signal({
email: '',
password: '',
});
loginForm = form(this.loginModel, (fieldPath) => {
required(fieldPath.email, {message: 'Email is required'});
email(fieldPath.email, {message: 'Enter a valid email address'});
required(fieldPath.password, {message: 'Password is required'});
minLength(fieldPath.password, 8, {message: 'Password must be at least 8 characters'});
});
onSubmit() {
// Only runs the action when the form is valid.
submit(this.loginForm, async (field) => {
console.log('Submitting:', field().value());
});
}
}
import {Component} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
@Component({
selector: 'app-login',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="loginForm" (submit)="onSubmit()">
<div>
<label>
Email
<input type="email" formControlName="email" />
</label>
@if (loginForm.controls.email.touched && loginForm.controls.email.invalid) {
<span class="error">
@if (loginForm.controls.email.errors?.['required']) {
Email is required
}
@if (loginForm.controls.email.errors?.['email']) {
Enter a valid email address
}
</span>
}
</div>
<div>
<label>
Password
<input type="password" formControlName="password" />
</label>
@if (loginForm.controls.password.touched && loginForm.controls.password.invalid) {
<span class="error">
@if (loginForm.controls.password.errors?.['required']) {
Password is required
}
@if (loginForm.controls.password.errors?.['minlength']) {
Password must be at least 8 characters
}
</span>
}
</div>
<button type="submit" [disabled]="loginForm.invalid">Sign In</button>
</form>
`,
})
export class LoginComponent {
loginForm = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email]),
password: new FormControl('', [Validators.required, Validators.minLength(8)]),
});
onSubmit() {
if (this.loginForm.valid) {
const credentials = this.loginForm.value;
console.log('Submitting:', credentials);
}
}
}
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
@Component({
selector: 'app-login',
imports: [FormsModule],
template: `
<form #loginForm="ngForm" (submit)="onSubmit()">
<div>
<label>
Email
<input
type="email"
name="email"
[(ngModel)]="email"
#emailInput="ngModel"
required
email
/>
</label>
@if (emailInput.touched && emailInput.invalid) {
<span class="error">
@if (emailInput.errors?.['required']) {
Email is required
}
@if (emailInput.errors?.['email']) {
Enter a valid email address
}
</span>
}
</div>
<div>
<label>
Password
<input
type="password"
name="password"
[(ngModel)]="password"
#passwordInput="ngModel"
required
minlength="8"
/>
</label>
@if (passwordInput.touched && passwordInput.invalid) {
<span class="error">
@if (passwordInput.errors?.['required']) {
Password is required
}
@if (passwordInput.errors?.['minlength']) {
Password must be at least 8 characters
}
</span>
}
</div>
<button type="submit" [disabled]="loginForm.invalid">Sign In</button>
</form>
`,
})
export class LoginComponent {
email = '';
password = '';
onSubmit() {
const credentials = {
email: this.email,
password: this.password,
};
console.log('Submitting:', credentials);
}
}
三種方法在設計上做了不同的取捨,影響你撰寫與維護表單的方式。這些差異源自於每種方法儲存表單狀態的位置,以及管理驗證的方式。
最根本的差異,在於每種方法把表單值的「真實來源」放在哪裡。
Signal Forms 把資料存在可寫入的訊號中。需要目前的表單值時,呼叫該訊號即可:
const credentials = this.loginModel(); // { email: '...', password: '...' }
這樣表單資料就放在單一的反應式容器中,值改變時會自動通知 Angular。表單結構與你的資料模型完全對應。
Reactive Forms 把資料存在 FormControl 與 FormGroup 實例中。你透過表單階層存取值:
const credentials = this.loginForm.value; // { email: '...', password: '...' }
這讓表單狀態管理與元件的資料模型分離。表單結構是明確的,但需要較多的建置程式碼。
Template-driven Forms 把資料存在元件屬性中。你直接存取值:
const credentials = {email: this.email, password: this.password};
這是最直接的方法,但需要值的時候得手動組裝。Angular 透過範本中的指令管理表單狀態。
每種方法定義驗證規則的方式不同,影響驗證邏輯存放的位置與維護方式。
Signal Forms 使用結構定義函式,把驗證器綁定到欄位路徑:
loginForm = form(this.loginModel, (fieldPath) => {
required(fieldPath.email, {message: 'Email is required'});
email(fieldPath.email, {message: 'Enter a valid email address'});
});
所有驗證規則都集中在同一處。結構定義函式在建立表單時執行一次,欄位值改變時驗證器會自動執行。錯誤訊息屬於驗證定義的一部分。
Reactive Forms 在建立控制項時附加驗證器:
驗證器與表單結構中的個別控制項綁在一起,驗證因此分散在整個表單定義中。錯誤訊息通常放在範本裡。
Template-driven Forms 在範本中使用指令屬性:
<input [(ngModel)]="email" required email />
驗證規則與 HTML 並存在範本中。這讓驗證貼近 UI,但邏輯會分散在範本與元件之間。
三種方法與 TypeScript 的整合差異明顯,影響編譯器能幫你避免多少錯誤。
Signal Forms 從你的模型結構推斷型別:
const loginModel = signal({email: '', password: ''});
const loginForm = form(loginModel);
// TypeScript knows: loginForm.email exists and returns FieldState<string>
你只需在訊號中定義一次資料形狀,TypeScript 就會自動知道有哪些欄位以及它們的型別。存取不存在的 loginForm.username 會產生型別錯誤。
Reactive Forms 需要以型別化表單明確標註型別:
const loginForm = new FormGroup({
email: new FormControl<string>(''),
password: new FormControl<string>(''),
});
// TypeScript knows: loginForm.controls.email is FormControl<string>
你為每個控制項個別指定型別。TypeScript 會驗證表單結構,但型別資訊需要你另外維護,與資料模型分開。
Template-driven Forms 提供的型別安全極少:
email = '';
password = '';
// TypeScript only knows these are strings, no form-level typing
TypeScript 只了解元件屬性,對表單結構或驗證一無所知,表單操作也失去了編譯時期檢查。
- 你在建置以訊號為基礎的新應用程式(Angular v22+)
- 你希望型別安全由模型結構自動推斷
- 以結構定義為基礎的驗證方式合你的胃口
- 你的團隊已熟悉訊號
- 你需要可上線的穩定性
- 你在建置複雜、動態的表單
- 你偏好以 Observable 為基礎的模式
- 你需要對表單狀態做細緻控制
- 你在既有的 Reactive Forms 程式碼庫上工作
- 你在建置簡單表單(登入、聯絡、搜尋)
- 你在快速製作原型
- 你的表單邏輯很單純
- 你偏好把表單邏輯放在範本中
- 你在既有的 Template-driven Forms 程式碼庫上工作
想進一步了解每種方法: