前兩篇介紹了 Signal Forms 的基本驗證,以及需要參考其他欄位的 Cross-field Validation。
不過有些驗證沒辦法只靠前端資料判斷,例如:
這些情況需要向後端或其他資料來源確認,就會用到非同步驗證。

Signal Forms 主要提供兩種方式:
validateHttp():處理 HTTP 驗證validateAsync():自行建立 Resource 處理其他非同步資料來源先從比較常見的 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 回傳對應的錯誤。

除了直接回傳 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。
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。

如果驗證只是呼叫 HTTP API,使用 validateHttp() 通常就足夠了。
不過非同步資料不一定來自 HTTP,例如:
這些情況可以使用 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:根據參數建立 ResourceonSuccess:把取得的結果轉成 Validation Error 或 null
onError:處理 Resource 發生錯誤的情況validateAsync() 同樣可以搭配 debounce、when,主要差別在於資料取得的方式由自己建立的 Resource 負責。
實際使用時可以依照資料來源選擇:
validateHttp()
validateAsync()
像 Username、Email 是否重複這類情境,直接使用 validateHttp() 就可以;需要自行控制 Resource 時,再使用 validateAsync()。

非同步驗證適合處理無法只靠前端資料判斷的規則,例如 Username 是否重複、Email 是否已經註冊。
Signal Forms 會先執行同步驗證,通過後才開始非同步驗證。執行期間可以透過 pending() 取得狀態,也能搭配 debounce、when 控制非同步驗證的執行時機。
一般 HTTP API 可以直接使用 validateHttp();如果資料來源不是一般 HTTP Request,或需要自行控制 Resource,再使用 validateAsync()。