複数ステップのフォーム
入力欄が多いフォームは、いくつかのステップに分けると入力しやすくなります。@k8ordo/formでは値をDOMが持つので、ステップごとに値を受け渡す仕組みは要りません。すべてのステップを描いたまま、いま表示しないものを隠すだけで作れます。
このページの内容
すべてのステップを描いたまま隠す
ステップごとに<fieldset>で囲み、いまのステップ以外にhiddenを付けます。隠れたステップの入力欄もDOMに残るので、ステップを行き来しても値は消えず、送信も最後に1回で済みます。
entry-form.tsxconst subscribe = () => () => {};
const hydrated = useSyncExternalStore(subscribe, () => true, () => false);
<fieldset hidden={hydrated && step !== 0} ref={stepOne}>
{/* step 1 */}
</fieldset>
<fieldset hidden={hydrated && step !== 1}>
{/* step 2 */}
</fieldset>ステップを隠すのは、ハイドレーションが終わってからにします。サーバーが描いたHTMLの時点で隠してしまうと、JavaScriptが無い環境では後のステップにたどり着けません。
隠さずにおけば、JavaScriptが無いときは1枚の長いフォームになり、1回のリクエストで送信されます。これは壊れているのではなく、正しい動きです。
次へ進む前に、いまのステップを確かめる
次へ進む前に、いまのステップの中にある入力欄だけをcheckValidity()で確かめます。まだ入力していない後のステップまで含めて確かめると、必ず失敗するからです。
entry-form.tsxconst next = () => {
const controls = stepOne.current?.querySelectorAll('input') ?? [];
const invalid = [...controls].find((control) => !control.checkValidity());
if (invalid !== undefined) {
invalid.focus();
return;
}
setStep(1);
};useFormがエラーを表示するのは、入力欄から離れたときです。失敗した入力欄にフォーカスを移しておけば、その欄を離れたときにエラーが表示されます。
送信ボタンは最後のステップにだけ描く
ハイドレーションが終わったら、送信ボタンは最後のステップにだけ描きます。
entry-form.tsx{(!hydrated || step === 1) && <button type="submit">Submit</button>}落とし穴
テキストの入力欄でEnterキーを押すと、ブラウザはフォームの最初の送信ボタンが押されたものとして扱います。そのボタンが隠れたステップにあっても送信されてしまい、ステップごとの確認を飛ばしてフォーム全体が送られます。
送信に失敗したら、エラーのあるステップへ戻す
送信に失敗すると、useFormはページの中で最初に失敗した入力欄へフォーカスを移します。ただし、隠れたステップの入力欄はフォーカスを受け取れません。
そこで新しいstateが届いたら、state.errorsのキーを持つステップのうち最も前のものへ、描画の中で切り替えます。フォーカスが移る前に入力欄が表示されるので、エラーのある欄にフォーカスが届きます。
entry-form.tsxconst STEP_OF: Record<string, number> = {
name: 0,
email: 0,
title: 1,
minutes: 1,
};
const [shownState, setShownState] = useState(state);
if (state !== shownState) {
setShownState(state);
const failed = Object.keys(state.errors ?? {}).map(
(key) => STEP_OF[key] ?? 0,
);
if (failed.length > 0) setStep(Math.min(...failed));
}送信のときのブラウザでの検証は、隠れたステップの入力欄も対象にします。そこで失敗すると、エラーを見せられないまま送信が止まります。ステップごとに確かめてから進めば、前のステップが最後になって失敗することはありません。
2ステップの申し込みフォームを試す
1ステップ目で連絡先を、2ステップ目で登壇の内容を入力します。どちらのステップも同じ<form>の中にあります。
試してみる
- 名前を空のまま「次へ」を押すと、先へは進まず、名前の入力欄にフォーカスが移ります。
- 1ステップ目を埋めて進み、「戻る」を押すと、入力した値がそのまま残っています。
- 2ステップ目の長さに
90と入力して「申し込む」を押すと、送信が止まり、エラーが表示されます。