深入指南
表單

跨欄位邏輯

當某個欄位的規則、驗證或行為取決於另一個欄位的值或狀態時,就需要跨欄位邏輯。

訊號表單會為每個規則函式提供一個欄位上下文。欄位上下文讓你存取目前欄位的值與狀態,並透過 valueOf()、stateOf() 與 fieldTreeOf() 讀取表單中的其他欄位。

本指南深入說明欄位上下文 API,並展示常見的跨欄位模式。單一欄位的驗證請見驗證指南。

了解欄位上下文

訊號表單中的每個規則函式都會收到一個欄位上下文參數,它是一個物件,描述目前的欄位,並提供存取表單其他部分的方式。

目前欄位可以存取三個屬性:

屬性 型別 說明
value Signal<TValue> 目前欄位的值,以訊號表示
state FieldState<TValue> 目前欄位的狀態(如有效性、錯誤、touched、dirty)
fieldTree FieldTree<TValue> 目前欄位的樹,用於以程式方式存取子欄位

跨欄位邏輯則用以下三個屬性存取表單的其他部分:

屬性 型別 說明
valueOf() (path) => PValue 最常用。當你需要另一個欄位的原始值來比較或計算時使用。
stateOf() (path) => FieldState<PValue> 當邏輯取決於另一個欄位的狀態時使用,例如它是否有效、是否 touched 或 dirty。
fieldTreeOf() (path) => FieldTree<PModel> 當你需要以程式方式存取另一個欄位的樹時使用,例如用 validateTree 把錯誤推到特定的子欄位。

以下範例使用 value 與 valueOf(),驗證目前欄位(結束日期)必須晚於表單中的開始日期:

import {Component, signal} from '@angular/core';
import {form, validate} from '@angular/forms/signals';

@Component({/* ... */})
export class EventForm {
  eventModel = signal({
    startDate: new Date('2026-06-01'),
    endDate: new Date('2026-06-05'),
  });

  eventForm = form(this.eventModel, (schemaPath) => {
    validate(schemaPath.endDate, ({value, valueOf}) => {
      if (value() <= valueOf(schemaPath.startDate)) {
        return {
          kind: 'invalidDateRange',
          message: 'End date must be after start date',
        };
      }

      return null;
    });
  });
}

NOTE: fieldContext 參數通常會被解構,只取出規則需要的部分。本指南後續的範例都採用這個模式。

跨欄位驗證模式

上一節的日期範圍範例,以開始日期驗證結束日期。因為規則讀取了 valueOf(schemaPath.startDate),任一日期改變時它都會自動重新評估。換句話說,只要一個驗證器就能讓錯誤狀態保持正確。

不過,那個驗證器只會把錯誤放在結束日期欄位上。若你希望範圍無效時兩個欄位都顯示錯誤,就為每個欄位各加一條相符的驗證規則:

import {Component, signal} from '@angular/core';
import {form, validate} from '@angular/forms/signals';

@Component({/* ... */})
export class EventForm {
  eventModel = signal({
    startDate: new Date('2026-06-01'),
    endDate: new Date('2026-06-05'),
  });

  eventForm = form(this.eventModel, (schemaPath) => {
    validate(schemaPath.startDate, ({value, valueOf}) => {
      if (value() >= valueOf(schemaPath.endDate)) {
        return {
          kind: 'invalidDateRange',
          message: 'Start date must be before end date',
        };
      }
      return null;
    });

    validate(schemaPath.endDate, ({value, valueOf}) => {
      if (value() <= valueOf(schemaPath.startDate)) {
        return {
          kind: 'invalidDateRange',
          message: 'End date must be after start date',
        };
      }
      return null;
    });
  });
}

兩條規則都用 valueOf() 讀取另一個欄位。因為每條規則都是反應式的,任一日期改變時,兩邊的驗證都會自動重新評估。

NOTE: 規則涉及多個欄位時,你需要決定錯誤該歸屬何處:放在特定欄位、放在多個欄位、還是放在父層。一般原則是把錯誤放在使用者最可能去修正問題的位置。

條件式必填

有些表單中,特定欄位只在特定條件下才必填。例如註冊表單可能只在使用者選擇企業帳號類型時,才要求填寫公司名稱:

import {Component, signal} from '@angular/core';
import {form, required} from '@angular/forms/signals';

@Component({/* ... */})
export class RegistrationForm {
  registrationModel = signal({
    accountType: 'personal' as 'personal' | 'business',
    companyName: '',
  });

  registrationForm = form(this.registrationModel, (schemaPath) => {
    required(schemaPath.companyName, {
      when: ({valueOf}) => valueOf(schemaPath.accountType) === 'business',
      message: 'Company name is required for business accounts',
    });
  });
}

when 選項收到的欄位上下文與其他規則函式相同,所以 valueOf 的用法也一樣。當使用者切換回 'personal' 時,條件會重新評估,必填要求連同它的錯誤都會自動清除。

用帶 when 的 required() 取代手動的 validate() 檢查,還會為欄位加上正確的必填中繼資料,啟用無障礙功能,例如為螢幕閱讀器把欄位標示為必填。

依據另一個欄位的狀態驗證

目前為止的範例都用 valueOf() 讀取另一個欄位的值。有時你的邏輯取決於另一個欄位的「狀態」:它是否有效、是否 touched 或 dirty。這時就改用 stateOf()。

例如,確認密碼欄位應該在使用者與密碼欄位互動過後,才檢查兩者是否相符。使用者還沒碰過密碼欄位,就在確認欄位上標示不符,為時過早:

import {Component, signal} from '@angular/core';
import {form, validate} from '@angular/forms/signals';

@Component({/* ... */})
export class PasswordForm {
  passwordModel = signal({
    password: '',
    confirmPassword: '',
  });

  passwordForm = form(this.passwordModel, (schemaPath) => {
    validate(schemaPath.confirmPassword, ({value, valueOf, stateOf}) => {
      if (!stateOf(schemaPath.password).touched()) {
        return null;
      }
      if (value() !== valueOf(schemaPath.password)) {
        return {
          kind: 'passwordMismatch',
          message: 'Passwords do not match',
        };
      }
      return null;
    });
  });
}

stateOf() 呼叫會回傳另一個欄位的欄位狀態,讓你存取 invalid()、touched() 與 dirty() 等訊號。因為這些都是訊號,密碼欄位的有效性一改變,規則就會重新評估。

CRITICAL: 小心不要讀取依賴你自己欄位驗證的狀態,那會形成循環。例如,檢查父欄位是否有效的驗證器會造成無限迴圈,因為父欄位的有效性取決於其子欄位的有效性(其中包含你的驗證器)。

使用 validateTree

目前為止的範例都用 validate() 檢查個別欄位。有時你需要驗證一組欄位,其邏輯本質上就涉及群組中的多個欄位,並且要把錯誤直接指向其中的特定子欄位。validateTree 正適合這類情境。

例如在數獨謎題中,每一列的數字都必須唯一。這是一條群組層級的規則:你檢查整列,再標示出違反規則的特定格子。這種驗證用個別欄位上的 validate 很難寫得乾淨,因為每一格都得知道所有其他格的狀況。

import {Component, signal} from '@angular/core';
import {form, validateTree} from '@angular/forms/signals';

@Component({/* ... */})
export class SudokuRow {
  rowModel = signal({
    cell1: 1,
    cell2: 3,
    cell3: 1,
    cell4: 4,
  });

  rowForm = form(this.rowModel, (schemaPath) => {
    validateTree(schemaPath, ({value, fieldTreeOf}) => {
      const row = value();
      const entries = [
        {val: row.cell1, fieldTree: fieldTreeOf(schemaPath.cell1)},
        {val: row.cell2, fieldTree: fieldTreeOf(schemaPath.cell2)},
        {val: row.cell3, fieldTree: fieldTreeOf(schemaPath.cell3)},
        {val: row.cell4, fieldTree: fieldTreeOf(schemaPath.cell4)},
      ];

      const counts = new Map<number, number>();
      for (const {val} of entries) {
        if (val !== 0) {
          counts.set(val, (counts.get(val) ?? 0) + 1);
        }
      }

      const errors = entries
        .filter(({val}) => val !== 0 && (counts.get(val) ?? 0) > 1)
        .map(({val, fieldTree}) => ({
          kind: 'duplicateInRow',
          message: `${val} already appears in this row`,
          fieldTree,
        }));

      return errors.length > 0 ? errors : null;
    });
  });
}

驗證器在父欄位(該列)上執行,讀取所有格子的值、計算重複次數,並為每個含重複數字的格子回傳一個錯誤。每個錯誤上的 fieldTree 屬性會確切告訴 Angular 哪一格該顯示錯誤。沒有 fieldTree,錯誤就會套用到該列本身,而不是使用者需要看到的位置。

因為 validateTree 可以回傳錯誤陣列,單一驗證器就能一次標示多個格子。每個錯誤都包含指向其目標的 fieldTree,Angular 因此能把錯誤送到正確的欄位。

何時用 validateTree、何時用 validate

當錯誤歸屬於被驗證的欄位時,優先用帶 valueOf() 的 validate(),即使規則會讀取其他欄位。以下情況才改用 validateTree:

  • 驗證邏輯本質上是關於一組欄位,而不是任何單一欄位
  • 驗證器需要回傳指向不同子欄位的錯誤

TIP: 想了解 validateTree 及其回傳型別的入門說明,請見驗證指南。

下一步

本指南說明了欄位上下文 API 與常見的跨欄位模式。想進一步了解相關的 Signal Forms 指南,請看: