エラーを表示する
ブラウザが入力を確かめて見つけた誤りも、サーバーが検証して返した誤りも、同じerrorに届きます。このページでは、エラーをどこに表示するかと、送信に失敗したときにフォーカスがどう動くかを説明します。
このページの内容
入力欄にエラーを表示する
field()が返す値のうち、errorにはその欄のエラーの文言が、invalidには失敗しているかどうかが入ります。
const title = form.field('title');
<label htmlFor="title">Title</label>
<input {...title.input} aria-describedby="title-error" id="title" />
{title.error !== undefined && <p id="title-error">{title.error}</p>}エラーが出るのは、入力欄から離れたときと送信したときです。入力中にいきなり出ることはなく、正しい値に直すと消えます。
文言を変える
表示される文言は、zodが出すエラーメッセージそのものです。文言を変えたいときは、スキーマの側に書きます。
z.object({
title: z.string().min(1, 'Enter a title').max(120, 'Keep it to 120 characters'),
});ブラウザでの検証もサーバーでの検証も同じスキーマから文言を取り出すので、どちらで見つかっても同じ言葉で伝わります。
リクエストの言語に合わせて文言を切り替えるなら、文言を関数で渡し、formFieldsを描画の中で呼んでください。モジュールのトップレベルで呼ぶと、最初に呼ばれたときの言語で固定されてしまいます。
page.tsxconst talkSchema = z.object({
title: z.string().min(1, { error: () => m.talk.titleMissing() }),
});
export default function NewTalkPage() {
const talkFields = formFields(talkSchema);
return <TalkForm action={createTalk} fields={talkFields} />;
}失敗した入力欄にフォーカスを移す
送信に失敗すると、ページの中で最初に失敗した入力欄へフォーカスが移ります。このために書くコードはありません。
ここでいう「最初」は、ページに並んでいる順番です。スキーマに書いたキーの順番ではありません。
落とし穴
自分で組み立てたstateを返すときは、新しいtokenを入れてください。同じ内容の失敗が2回続いたとき、tokenが同じだと同じ返事とみなされ、フォーカスが移りません。
フォーム全体のエラーを表示する
どの入力欄にも属さないエラーは、form.formErrorに入ります。たとえば、pathを指定せずにスキーマ全体へ付けた.refine()のエラーがそうです。
<form {...form.props} action={formAction}>
{form.formError.message !== undefined && (
<p {...form.formError.props}>{form.formError.message}</p>
)}
{/* fields */}
</form>表示する要素にはformError.propsを展開します。要素がフォーカスを受け取れるようになるので、スクリーンリーダーにもエラーが伝わります。
表示する場所は、入力欄より上にします。入力欄の失敗と同時に起きても先にこちらへフォーカスが移り、そこからTabキーで入力欄へ進めます。
サーバーでしか分からない失敗を返す
タイトルがすでに使われている、といった失敗は、データベースを見るまで分かりません。parseFormで検証を通ったあとにこうした失敗が見つかったら、parsed.stateにエラーを足して返します。
actions.tsexport async function createTalk(
_prev: FormState,
formData: FormData,
) {
const parsed = parseForm(talkSchema, formData);
if (!parsed.success) return parsed.state;
if (await titleExists(parsed.data.title)) {
return {
...parsed.state,
errors: { title: 'A talk with this title already exists' },
};
}
await saveTalk(parsed.data);
redirect(href('/talks'));
}parsed.stateには、入力されていた値と新しいtokenがすでに入っています。そのため値は入力欄に戻り、フォーカスもエラーの出た欄に移ります。
エラーの出方を試す
ハンドルとパスワードを登録するフォームです。表示される文言は、どれもページのスキーマから作っています。
試してみる
- ハンドルに
abと入力して欄から離れると、3文字以上にするよう求めるエラーが出ます。 - 確認用のパスワードを違う値にすると、一致しないというエラーが出ます。
- 空の欄を残したまま「登録」を押すと、最初に失敗した欄へフォーカスが移ります。