☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
angular 中級

📝 Angular 表單:Template-driven vs Reactive Forms

📌 兩種表單策略

Angular 提供兩種建立表單的方式:

特性 Template-driven Reactive Forms
定義位置 HTML 模板中 TypeScript 類別中
資料模型 隱式(由模板建立) 顯式(FormGroup / FormControl)
驗證 用模板指令 用 TypeScript 函式
適合場景 簡單表單 複雜表單(推薦)
可測試性 較差 優秀

📌 模板驅動表單(Template-driven)

// 需要匯入 FormsModule
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

interface UserForm {
  name: string;
  email: string;
  age: number;
}

@Component({
  selector: 'app-simple-form',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #myForm="ngForm" (ngSubmit)="onSubmit(myForm)">
      <div>
        <label>姓名:</label>
        <input name="name" [(ngModel)]="user.name" required minlength="2" #nameInput="ngModel">
        <div *ngIf="nameInput.invalid && nameInput.touched" class="error">
          <span *ngIf="nameInput.errors?.['required']">姓名必填</span>
          <span *ngIf="nameInput.errors?.['minlength']">至少 2 個字</span>
        </div>
      </div>

      <div>
        <label>Email:</label>
        <input name="email" [(ngModel)]="user.email" required email #emailInput="ngModel">
        <div *ngIf="emailInput.invalid && emailInput.touched" class="error">
          Email 格式不正確
        </div>
      </div>

      <button type="submit" [disabled]="myForm.invalid">送出</button>
    </form>
  `
})
export class SimpleFormComponent {
  user: UserForm = { name: '', email: '', age: 0 };

  onSubmit(form: any) {
    if (form.valid) {
      console.log('表單資料:', this.user);
    }
  }
}

📌 響應式表單(Reactive Forms)—— 推薦

// register.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, Validators, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-register',
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule],
  template: `
    <form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
      <div>
        <label>使用者名稱:</label>
        <input formControlName="username">
        <div *ngIf="f['username'].invalid && f['username'].touched" class="error">
          <span *ngIf="f['username'].errors?.['required']">必填</span>
          <span *ngIf="f['username'].errors?.['minlength']">至少 3 個字</span>
        </div>
      </div>

      <div>
        <label>Email:</label>
        <input formControlName="email">
      </div>

      <div formGroupName="password">
        <div>
          <label>密碼:</label>
          <input type="password" formControlName="pwd">
        </div>
        <div>
          <label>確認密碼:</label>
          <input type="password" formControlName="confirmPwd">
        </div>
        <div *ngIf="registerForm.get('password')?.errors?.['passwordMismatch']" class="error">
          兩次密碼不一致
        </div>
      </div>

      <div>
        <label>興趣:</label>
        <div formArrayName="hobbies">
          <div *ngFor="let hobby of hobbies.controls; let i = index">
            <input [formControlName]="i">
            <button type="button" (click)="removeHobby(i)">✕</button>
          </div>
        </div>
        <button type="button" (click)="addHobby()">+ 新增興趣</button>
      </div>

      <button type="submit" [disabled]="registerForm.invalid">註冊</button>
    </form>

    <pre>表單值:{{ registerForm.value | json }}</pre>
    <pre>表單狀態:{{ registerForm.status }}</pre>
  `
})
export class RegisterComponent implements OnInit {
  registerForm!: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.registerForm = this.fb.group({
      username: ['', [Validators.required, Validators.minLength(3)]],
      email: ['', [Validators.required, Validators.email]],
      password: this.fb.group({
        pwd: ['', [Validators.required, Validators.minLength(8)]],
        confirmPwd: ['', Validators.required]
      }, { validators: this.passwordMatchValidator }),
      hobbies: this.fb.array([])  // 動態陣列
    });
  }

  // 便利 getter
  get f() { return this.registerForm.controls; }
  get hobbies() { return this.registerForm.get('hobbies') as FormArray; }

  // 自訂驗證器:密碼比對
  passwordMatchValidator(group: FormGroup): { [key: string]: boolean } | null {
    const pwd = group.get('pwd')?.value;
    const confirmPwd = group.get('confirmPwd')?.value;
    return pwd === confirmPwd ? null : { passwordMismatch: true };
  }

  addHobby() {
    this.hobbies.push(this.fb.control(''));
  }

  removeHobby(index: number) {
    this.hobbies.removeAt(index);
  }

  onSubmit() {
    if (this.registerForm.valid) {
      console.log('送出表單:', this.registerForm.value);
    } else {
      // 標記所有欄位為 touched,觸發錯誤顯示
      this.registerForm.markAllAsTouched();
    }
  }
}

📌 FormControl、FormGroup、FormArray

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

// FormControl — 單一表單欄位
const name = new FormControl('預設值');
console.log(name.value);     // '預設值'
console.log(name.valid);     // true
console.log(name.touched);   // false

// FormGroup — 一組表單欄位
const form = new FormGroup({
  firstName: new FormControl(''),
  lastName: new FormControl('')
});
console.log(form.value);  // { firstName: '', lastName: '' }

// FormArray — 動態數量的表單欄位(如多個電話號碼)
const phones = new FormArray([
  new FormControl('0912-345-678'),
  new FormControl('02-1234-5678')
]);
phones.push(new FormControl('新號碼'));  // 動態新增
phones.removeAt(0);                      // 動態移除

📌 自訂驗證器

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

// 同步驗證器 — 檢查是否包含禁止字詞
export function forbiddenNameValidator(forbidden: RegExp): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const isForbidden = forbidden.test(control.value);
    return isForbidden ? { forbiddenName: { value: control.value } } : null;
  };
}

// 非同步驗證器 — 檢查使用者名稱是否已被使用
export function uniqueUsernameValidator(
  userService: UserService
): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    return userService.checkUsername(control.value).pipe(
      map(isTaken => isTaken ? { usernameTaken: true } : null)
    );
  };
}

// 使用自訂驗證器
this.fb.group({
  username: ['', [Validators.required, forbiddenNameValidator(/admin/i)]],
});

📌 小結

  • Template-driven 適合簡單表單,Reactive Forms 適合複雜表單
  • FormControl = 單一欄位、FormGroup = 欄位群組、FormArray = 動態欄位
  • Reactive Forms 在 TypeScript 中定義,可測試性高
  • 驗證器分為同步非同步,可自訂驗證邏輯
  • markAllAsTouched() 可以在送出時觸發所有驗證訊息顯示

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊