@k8ordo/form

複数の入力欄にまたがる検証

パスワードの確認や「2つ以上選ぶ」のように、1つの入力欄だけでは決まらない検証があります。こうした検証はHTMLの属性で表せず、関数である.refine()はブラウザへ送れません。そこで@k8ordo/formでは、これをルールとしてスキーマの横に宣言します。

このページの内容

ルールを宣言する

defineFormに、スキーマとルールの配列を渡します。

schema.ts
export const signup = defineForm(
  z.object({
    password: z.string().min(8),
    confirm: z.string(),
  }),
  [sameAs('confirm', 'password', 'The passwords do not match')],
);

できあがった定義は、スキーマの代わりにformFieldsとparseFormへ渡します。ルールはただのデータなので、入力欄の情報と一緒にブラウザへ届きます。

ts
const signupFields = formFields(signup);

const parsed = parseForm(signup, formData);

ルールに書く入力欄のパスは、スキーマから型で確かめられます。存在しない欄の名前を書くと、型エラーになります。

用意されているルール

いま使えるルールは3つです。

  • sameAs(field, other, message):fieldの値がotherの値と同じであることを求めます。パスワードの確認に使います。
  • minChecked(field, min, message):同じ名前のチェックボックスのうち、min個以上にチェックが入っていることを求めます。
  • requiredWhen(field, when, equals, message):whenの値がequalsのときだけ、fieldを必須にします。
schema.ts
defineForm(schema, [
  sameAs('confirm', 'password', 'The passwords do not match'),
  minChecked('topics', 2, 'Pick at least two'),
  requiredWhen('reason', 'status', 'rejected', 'Give a reason'),
]);
Playground

ルールの動きを試す

登壇の審査フォームです。「却下」のときだけ理由が必須になり、観点は2つ以上選ぶ必要があります。

審査した観点

試してみる

  1. 観点を2つ選んでから、判定で「却下」を選び、理由を空のまま「送信」を押してください。理由の入力欄にエラーが出ます。
  2. 判定を「承認」に戻すと、理由のエラーは消えます。
  3. 観点を1つだけにして送信すると、2つ以上選ぶよう求めるエラーが出ます。

ブラウザとサーバーで同じ判定をする

ルールは、ブラウザでもサーバーでも同じ処理で判定します。ブラウザは入力中のフォームを、サーバーは送信された値を確かめます。判定の処理が1つしかないので、両者の結果が食い違うことはありません。

ブラウザでは、ルールを満たさない入力欄にsetCustomValidityでエラーを付けます。そのため組み込みの検証と見分けがつかず、:user-invalidの擬似クラスも当たり、文言もほかのエラーと同じerrorに届きます。

1つの入力欄が複数のルールを満たさないときは、先に宣言したルールの文言を表示します。

リクエストの言語で文言を出す

ルールの文言には、文字列の代わりに関数も渡せます。関数はルールを宣言したときではなく、エラーを報告するときに呼ばれます。

schema.ts
export const signup = defineForm(schema, [
  sameAs('confirm', 'password', m.signup.mismatch),
  minChecked('topics', 2, () => m.signup.pickAtLeast(2)),
]);

ブラウザはサーバーから送られた関数を呼べないので、formFieldsは入力欄の情報を作る時点で文言を確定させます。言語に合わせるなら、formFieldsを描画の中で呼んでください。

ルールで書けない検証

3つのルールで書けない検証は、これまでどおり.refine()で書きます。ただし、.refine()はサーバーでしか動きません。

schema.ts
z.object({
  start: z.iso.date(),
  end: z.iso.date(),
}).refine((value) => value.start <= value.end, {
  message: 'The end comes before the start',
});

スキーマ全体に付けた.refine()は、ブラウザでは確かめない検証としてdroppedに載ります。pathを指定しなければ、エラーはform.formErrorに届きます。

警告

落とし穴

1つの入力欄や入れ子のオブジェクト、配列の行に付けた.refine()は、まだdroppedに載りません。ブラウザでは何も知らせずに素通りし、送信してから初めてサーバーでエラーになります。

k8ordo

Baselineに入った機能を、制限なく使うReactのライブラリ群。

© 2026 k8o — MIT License

組版:Noto Sans JP / M PLUS 2