Reactive Forms 巢狀表單結構

在實際應用中,我們常常需要將表單控制項分組,以便更好地組織和處理相關聯的資料。Angular 的 Reactive Forms 提供了巢狀 FormGroup 結構來實現這一點。

TypeScript 中的巢狀結構

在 TypeScript 中,可以在一個 FormGroup 內嵌套另一個 FormGroup,把想要放在同一群組的 FormControl 放在一起:

registerForm = new FormGroup({
  email: new FormControl('', {
    validators: [Validators.required, Validators.email],
  }),
  passwords: new FormGroup({
    password: new FormControl('', {
      validators: [Validators.required, Validators.minLength(6)],
    }),
    confirmPassword: new FormControl('', {
      validators: [Validators.required, Validators.minLength(6)],
    }),
  }),
});

在這個例子中,passwords 是一個嵌套的 FormGroup,包含了 passwordconfirmPassword 兩個 FormControl。

範本中的巢狀結構

在 HTML 範本中,也需要反映這種嵌套結構。有兩種方式可以實現:

方式一:使用 [formGroup]

<div class="control-row" [formGroup]="registerForm.controls.passwords">
  <div class="control">
    <label for="password">Password</label>
    <input
      id="password"
      type="password"
      name="password"
      formControlName="password"
    />
  </div>
  <div class="control">
    <label for="confirm-password">Confirm Password</label>
    <input
      id="confirm-password"
      type="password"
      name="confirm-password"
      formControlName="confirmPassword"
    />
  </div>
</div>

方式二:使用 formGroupName

<div class="control-row" formGroupName="passwords">
  <div class="control">
    <label for="password">Password</label>
    <input
      id="password"
      type="password"
      name="password"
      formControlName="password"
    />
  </div>
  <div class="control">
    <label for="confirm-password">Confirm Password</label>
    <input
      id="confirm-password"
      type="password"
      name="confirm-password"
      formControlName="confirmPassword"
    />
  </div>
</div>

兩種方式的差異

  1. [formGroup]:

    • 需要直接引用 TypeScript 中的 FormGroup 實例

    • 使用屬性綁定語法

    • 更適合動態產生的表單結構

  2. formGroupName:

    • 使用字串指定巢狀 FormGroup 的名稱

    • 語法更簡潔

    • 必須在父 FormGroup 的上下文中使用

在多數情況下,formGroupName 寫法更為常用和簡潔。

存取巢狀表單的值

巢狀表單會影響如何存取和設置表單值:

// 存取整個表單的值
console.log(this.registerForm.value);
// 輸出: { email: '...', passwords: { password: '...', confirmPassword: '...' } }

// 存取巢狀群組的值
console.log(this.registerForm.get('passwords').value);
// 輸出: { password: '...', confirmPassword: '...' }

// 存取巢狀控制項的值
console.log(this.registerForm.get('passwords.password').value);
// 或
console.log(this.registerForm.controls.passwords.get('password').value);

巢狀表單的驗證

除了對個別控制項進行驗證,我們也可以對整個巢狀 FormGroup 進行驗證:

registerForm = new FormGroup({
  email: new FormControl('', [...]),
  passwords: new FormGroup({
    password: new FormControl('', [...]),
    confirmPassword: new FormControl('', [...])
  }, { validators: this.passwordsMatch })
});

// 驗證兩個密碼是否匹配
passwordsMatch(group: FormGroup): ValidationErrors | null {
  const password = group.get('password').value;
  const confirmPassword = group.get('confirmPassword').value;
  
  return password === confirmPassword ? null : { passwordsDoNotMatch: true };
}

在範本中檢查 FormGroup 級別的錯誤:

<div formGroupName="passwords">
  <!-- 輸入欄位 -->
  
  @if(registerForm.controls.passwords.errors?.['passwordsDoNotMatch']){
    <p class="error">Passwords do not match!</p>
  }
</div>

修改巢狀表單的值

使用 patchValuesetValue 時也需要考慮巢狀結構:

// 部分更新
this.registerForm.patchValue({
  email: 'test@example.com',
  passwords: {
    password: '123456'
    // 不需要包含 confirmPassword
  }
});

// 完整更新
this.registerForm.setValue({
  email: 'test@example.com',
  passwords: {
    password: '123456',
    confirmPassword: '123456'  // 必須包含所有控制項
  }
});

巢狀表單的好處

  1. 邏輯分組:將相關的控制項組織在一起,提高代碼可讀性

  2. 結構化數據:表單值對象結構更清晰,方便處理

  3. 群組驗證:可以對相關控制項進行整體驗證

  4. 更好的維護性:大型表單更容易管理和維護

巢狀表單結構是處理複雜表單的有力工具,能夠讓表單結構更加清晰和有組織。

Last updated