入れ子と繰り返し行
スキーマの中にあるオブジェクトは、ドットでつないだパスで扱います。オブジェクトの配列は、行の集まりとして扱います。どちらの場合も、パスがそのままブラウザの送るnameになります。
このページの内容
入れ子のオブジェクトの入力欄を使う
field()には、ドットでつないだパスを渡します。送信されるnameも、state.errorsのキーも同じパスです。
schema.tsz.object({
user: z.object({
email: z.email(),
}),
});profile-form.tsxconst email = form.field('user.email');
<input {...email.input} />.optional()や.default()の付いたオブジェクトでも、中の入力欄はそのまま使えます。
落とし穴
オブジェクトを省ける場合でも、中の入力欄は必ず描いてください。名前がFormDataに無いと、parseFormはinputの展開し忘れとみなしてエラーを投げます。
行を足したり消したりする
オブジェクトの配列はarray()で扱います。行ごとの入力欄はrow.field()で取り出し、row.keyをkeyに渡します。
schema.tsz.object({
items: z
.array(
z.object({
name: z.string().min(1),
quantity: z.coerce.number().int().min(1),
}),
)
.min(1)
.max(3),
});order-form.tsxconst items = form.array('items');
{items.rows.map((row) => (
<fieldset key={row.key}>
<input {...row.field('name').input} />
<input {...row.field('quantity').input} />
{items.canRemove && (
<button onClick={row.remove} type="button">
Remove
</button>
)}
</fieldset>
))}
{items.canAdd && (
<button onClick={items.add} type="button">
Add a row
</button>
)}canAddとcanRemoveは、スキーマの.max()と.min()から決まります。サーバーが受け付けない行数になる手前で、ボタンが消えます。
Reactのstateが持つのは、行を見分けるためのkeyだけです。値はDOMにあるので、行を足しても消しても、入力された値をReactに写すことはありません。
注文の行を足す
1行から3行まで入力できるスキーマです。送信すると、送られるはずだった名前と値を表示します。
試してみる
- 「行を足す」を2回押すと、3行目でボタンが消えます。スキーマの
.max(3)が効いています。 - 2行目を空のまま「送信」を押すと、送信が止まり、2行目の入力欄にフォーカスが移ります。
- 1行目を消してから残りを埋めて送ると、添字が詰まり、
items[0].nameから並びます。
行数のエラーを表示する
行が.min()より少ない、または.max()より多いというエラーは、どの行の入力欄にも属しません。このエラーはitems.errorに入ります。
order-form.tsx<form {...form.props} action={formAction}>
{items.error !== undefined && (
<p {...items.errorProps}>{items.error}</p>
)}
{items.rows.map((row) => (
<fieldset key={row.key}>{/* fields */}</fieldset>
))}
</form>表示する要素にはitems.errorPropsを展開します。展開しないと、配列だけが失敗した送信ではフォーカスがどこにも移らず、スクリーンリーダーにも何も伝わりません。
表示する場所は、行より上にします。行も失敗しているときでもまずここにフォーカスが移り、そこからTabキーで行へ進めます。
送られる名前
行の中の入力欄のnameは、items[0].nameのように添字を含みます。エラーのキーも同じ形です。
row.field('name').input.name;
// 'items[0].name'
state.errors;
// { 'items[1].quantity': 'Order at least one' }行を消すと、後ろの行の添字が1つずつ詰まります。ブラウザで出したエラーも、行と一緒に移ります。
落とし穴
ただし、サーバーが返したエラーの添字は振り直されません。上の行を消すと、まだ直していないエラーは、その添字になった別の行に表示されます。
最初に描く行数
送信のあとならstate.rowsの行数、そうでなければスキーマの.min()の行数を描きます。.min()も無ければ0行です。
parseFormは届いた行数をstate.rowsに入れて返します。そのため、JavaScriptが無い状態で送り直しても、同じ行数で描き直せます。
行数は送られた添字から数えますが、.max()を超える数は信じません。大きな添字を偽って送られても、サーバーが大きな配列を作ることはありません。
文字列の配列
z.array(z.string())のような値の配列では、行の入力欄にキーがありません。row.field()を引数なしで呼ぶと、nameはtags[0]のようになります。
tags-form.tsxconst tags = form.array('tags');
{tags.rows.map((row) => (
<input key={row.key} {...row.field().input} />
))}メモ
選択肢の配列(z.array(z.enum([…])))は、行ではなく1つの入力欄として扱うチェックボックスの集まりです。詳しくは「入力欄の種類」を見てください。
扱えない形
- 行の中にさらに行を入れる形は、送信する名前が1つに決まらないため、
formFieldsとparseFormがエラーを投げます。型の上では止められないので注意してください。 sameAsなどのルールは、行の中の入力欄には宣言できません。行の入力欄の検証は、スキーマの中に書きます。