iT邦幫忙

2026 iThome 鐵人賽

DAY 20
2
Modern Web

Angular 22 Signal 進化論系列 第 20 篇

Day 20:Signal Forms 非同步驗證,validateHttp 與 validateAsync

  • 分享至 

  • xImage
  •  

前兩篇介紹了 Signal Forms 的基本驗證,以及需要參考其他欄位的 Cross-field Validation。

不過有些驗證沒辦法只靠前端資料判斷,例如:

  • Username 是否已經被使用
  • Email 是否已經註冊
  • 邀請碼是否有效
  • 某個代碼是否存在

這些情況需要向後端或其他資料來源確認,就會用到非同步驗證。

非同步驗證:前端無法獨自判斷

Signal Forms 主要提供兩種方式:

  • validateHttp():處理 HTTP 驗證
  • validateAsync():自行建立 Resource 處理其他非同步資料來源

先從比較常見的 validateHttp() 開始。

使用 validateHttp()

假設註冊時,需要確認 Username 是否已經被使用:

interface AccountModel {
  username: string;
}

readonly accountModel = signal<AccountModel>({
  username: '',
});

Schema 可以同時設定同步和非同步驗證:

const accountSchema = schema<AccountModel>(path => {
  required(path.username, {
    message: 'Username 為必填',
  });

  minLength(path.username, 3, {
    message: 'Username 至少需要 3 個字元',
  });

  validateHttp(path.username, {
    request: ({ value }) =>
      `/api/users/check-username?username=${value()}`,

    onSuccess: (
      result: { available: boolean }
    ) =>
      result.available
        ? null
        : {
            kind: 'usernameTaken',
            message: 'Username 已被使用',
          },

    onError: () => ({
      kind: 'serverError',
      message: '目前無法檢查 Username',
    }),
  });
});

readonly accountForm = form(
  this.accountModel,
  accountSchema
);

request 會根據目前的 Username 建立 Request,API 回傳後再由 onSuccess 決定驗證結果。

如果 Username 已經被使用,就回傳 Validation Error;可以使用則回傳 null。Request 本身失敗時,也可以透過 onError 回傳對應的錯誤。

validateHttp():從 Request 到驗證結果

除了直接回傳 URL,request 也可以回傳完整的 HttpResourceRequest。需要設定 Query Parameters、Timeout 等 HTTP 選項時,可以改成:

validateHttp(path.username, {
  request: ({ value }) => ({
    url: '/api/users/check-username',
    params: {
      username: value(),
    },
    timeout: 5000,
  }),

  onSuccess: (
    result: { available: boolean }
  ) =>
    result.available
      ? null
      : {
          kind: 'usernameTaken',
          message: 'Username 已被使用',
        },

  onError: () => ({
    kind: 'serverError',
    message: '目前無法檢查 Username',
  }),
});

一般情況直接回傳 URL 就足夠,需要額外設定 Request 時再使用完整物件。

非同步驗證的執行狀態

HTTP Request 需要等待回應,因此非同步驗證執行期間,Field State 會進入 Pending。

Template 可以透過 pending() 顯示目前狀態:

<input
  [formField]="accountForm.username"
/>

@if (accountForm.username().pending()) {
  <p>檢查中...</p>
}

等非同步驗證完成後,Field State 才會反映最後的驗證結果。

另外,Signal Forms 會先執行同步驗證,通過後才開始非同步驗證。

前面的 Username 已經設定 required() 和 minLength()。如果只輸入一個字元,minLength() 已經驗證失敗,就不需要再向後端確認 Username 是否被使用。

這樣可以先處理前端就能判斷的規則,避免送出沒有必要的 Request。

減少不必要的 Request

Username 會隨著使用者輸入持續改變,如果每打一個字元就立刻送出 Request,很容易產生大量沒有必要的請求。

validateHttp() 可以設定自己的 debounce:

validateHttp(path.username, {
  debounce: 500,

  request: ({ value }) =>
    `/api/users/check-username?username=${value()}`,

  onSuccess: (
    result: { available: boolean }
  ) =>
    result.available
      ? null
      : {
          kind: 'usernameTaken',
          message: 'Username 已被使用',
        },

  onError: () => ({
    kind: 'serverError',
    message: '目前無法檢查 Username',
  }),
});

設定 debounce: 500 後,非同步驗證會等待 500ms 再開始。如果這段時間 Username 又改變,就會重新等待。

這裡和上一篇介紹的 Schema debounce() 不一樣:

  • debounce(path.username, 500):延後欄位值同步到 Form Model,其他依賴這個值的邏輯也會一起延後
  • validateHttp(path.username, { debounce: 500 }):Form Model 正常更新,只延後這一條非同步驗證

如果只是希望減少 Username 檢查 API 的呼叫次數,使用 validateHttp() 自己的 debounce 就可以。

有些情況則是根本不需要執行這條驗證。

例如只有開啟 Username 檢查時才需要呼叫 API:

interface AccountModel {
  username: string;
  checkUsername: boolean;
}

可以透過 when 設定條件:

validateHttp(path.username, {
  when: ({ valueOf }) =>
    valueOf(path.checkUsername),

  request: ({ value }) =>
    `/api/users/check-username?username=${value()}`,

  onSuccess: (
    result: { available: boolean }
  ) =>
    result.available
      ? null
      : {
          kind: 'usernameTaken',
          message: 'Username 已被使用',
        },

  onError: () => ({
    kind: 'serverError',
    message: '目前無法檢查 Username',
  }),
});

當 checkUsername 是 false 時,這條非同步驗證就不會執行。

request 也可以回傳 undefined,略過這一次 Request。如果只是控制整條驗證規則要不要執行,使用 when 會比較清楚;如果是在建立 Request 時才知道這次是否需要送出,再回傳 undefined。

像必填、最小長度這類條件,前面已經可以透過 required()、minLength() 處理,就不需要在 request 裡重複判斷。

Request 發出後,使用者也可能已經繼續輸入:

ant
anto
antonio

當欄位值改變時,Signal Forms 會取消前一次還在進行的非同步驗證,避免舊的 Request 結果影響目前欄位。

搭配 debounce 後,可以進一步減少輸入過程中產生的不必要 Request。

減少不必要的 Request

使用 validateAsync() 自訂非同步驗證

如果驗證只是呼叫 HTTP API,使用 validateHttp() 通常就足夠了。

不過非同步資料不一定來自 HTTP,例如:

  • IndexedDB
  • Web Worker
  • WebSocket
  • 自訂 Resource
  • 需要額外快取或 Retry 的資料來源

這些情況可以使用 validateAsync():

validateAsync(path.username, {
  params: ({ value }) => value(),

  debounce: 500,

  factory: params => {
    // 根據 params 建立 Resource
  },

  onSuccess: result => {
    // 回傳 Validation Error 或 null
  },

  onError: () => ({
    kind: 'serverError',
    message: '驗證失敗',
  }),
});

這裡主要會用到:

  • params:準備 Resource 需要的參數
  • factory:根據參數建立 Resource
  • onSuccess:把取得的結果轉成 Validation Error 或 null
  • onError:處理 Resource 發生錯誤的情況

validateAsync() 同樣可以搭配 debounce、when,主要差別在於資料取得的方式由自己建立的 Resource 負責。

實際使用時可以依照資料來源選擇:

  • 一般 HTTP API:validateHttp()
  • 自訂 Resource 或其他非同步資料來源:validateAsync()

像 Username、Email 是否重複這類情境,直接使用 validateHttp() 就可以;需要自行控制 Resource 時,再使用 validateAsync()。

validateHttp() 與 validateAsync()

本日結語

非同步驗證適合處理無法只靠前端資料判斷的規則,例如 Username 是否重複、Email 是否已經註冊。

Signal Forms 會先執行同步驗證,通過後才開始非同步驗證。執行期間可以透過 pending() 取得狀態,也能搭配 debounce、when 控制非同步驗證的執行時機。

一般 HTTP API 可以直接使用 validateHttp();如果資料來源不是一般 HTTP Request,或需要自行控制 Resource,再使用 validateAsync()。

資料來源


上一篇
Day 19:Signal Forms Cross-field Validation 與欄位更新時機
下一篇
Day 21:Angular Template 還適合呼叫 Method 嗎?從 Pipe、RxJS 到 computed
系列文
Angular 22 Signal 進化論 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言