コンテンツにスキップ

マルチステップフォーム

multi-step-form は公認のウィザードです: 現在のステップと、入力 されたすべてのドラフトをサーバが所有します。クライアントは 1 つの 領域(ステップ全体の outerHTML スワップ)、ステップごとに 1 つの フォーム、そしてレスポンスのたびにサーバが再描画する hc-stepper です — クライアント側のステップ状態なし、ステップをまたいで蓄積する隠し フィールドなし、htmx 以外の JavaScript なし。 マークアップのバージョニングポリシーの 下で安定しています。

別名: ウィザード、ステップフォーム。

下のウィザードを操作してみてください — Next はステップを バリデートし(ネイティブの required に加え、サーバが権威として 422 + field-errors フラグメントで応答)、Back は空欄があっても 決してバリデートしません。最終ステップは確認ページへリダイレクト します(戻るリンク付き)。エンドポイントはサーバレスポンス契約を 実装した名前空間付きデモで、api/recipes/multi-step-form/ 配下に あります。 デモだけの自由が 1 つあります: ステートレスなドキュメント用 エンドポイントは、各ステップで hidden input を通じてドラフトを 引き回します — 実際のサーバはドラフトをサーバ側(セッション、 ドラフト行、署名付き Cookie)に保持します。冒頭の「hidden フィールドがステップをまたいで蓄積しない」ルールが指しているのは そちらです。

<section id="wizard">
<ol class="hc-stepper">
<li class="hc-stepper__step" data-state="complete">
<span class="hc-stepper__marker" aria-hidden="true"></span>
<span class="hc-stepper__label">Account
<span class="hc-sr-only">(completed)</span></span>
</li>
<li class="hc-stepper__step" aria-current="step">
<span class="hc-stepper__marker" aria-hidden="true">2</span>
<span class="hc-stepper__label">Profile</span>
</li>
<li class="hc-stepper__step">
<span class="hc-stepper__marker" aria-hidden="true">3</span>
<span class="hc-stepper__label">Review</span>
</li>
</ol>
<form method="post" action="/signup/2"
data-hx-post="/signup/2"
data-hx-target="#wizard" data-hx-swap="outerHTML"
data-hx-disabled-elt="find button[type=submit]">
<div id="wizard-errors"></div>
…step 2 fields, pre-filled from the draft…
<button class="hc-button" type="submit" name="nav" value="back"
formnovalidate>Back</button>
<button class="hc-button" data-variant="primary" type="submit"
name="nav" value="next">Next</button>
</form>
</section>

ナビの両方向は同じフォームの名前付き送信(name="nav")です。 そのため htmx とネイティブの非 JS パスは同一のボディを送り、現在の ステップの入力は常にナビゲーションと一緒に運ばれます。

ケースレスポンス
nav=next、有効200 — 次のステップの完全な #wizard フラグメント: ステッパー再描画(このステップは に)、フィールドはドラフトからプリフィル
nav=back200 — 前のステップのフラグメント。POST されたフィールドはバリデーションなしでドラフトへマージ済み
nav=next、無効422 + HX-Retarget: #wizard-errors + HX-Reswap: innerHTMLfield-errors フラグメント — ステップは再描画されず、入力途中の値はそのまま
最終の nav=nextmutating-form の完了: 204 + HX-Redirect(htmx)/ 303 Location(非 JS)

422 には、mutating form の標準の 1 行 beforeSwap 許可が必要です。

ステップの URL は本物です(/signup/1/signup/2、…): リロードや ディープリンクはサーバが考える現在のステップに着地し、JavaScript なしではウィザードは単に古典的なページ群になります。

ステッパーは状態を構造的に伝えます(<ol> + aria-current="step" + sr-only の (completed))。formnovalidate がキーボードユーザーを 閉じ込めから守ります。422 パスは field-errors のフォーカス管理を 保ちます。スワップ後に読み上げ順が自然に再開するよう、各フラグメントの 先頭にステップ見出しを置いてください。

プログレッシブエンハンスメント

Section titled “プログレッシブエンハンスメント”

本物のステップ URL(/signup/1/signup/2、…)+ method/action + formnovalidate により、ウィザードはクラシックなページ群へ デグレードします:

  • JavaScript なしでは、各名前付き送信(nav=next / nav=back)が 同じボディをネイティブに POST し、サーバは次の(エラー時は同じ) ステップのフルページで応答します — htmx レイヤーはその ページ読み込みを領域スワップに変えるだけです。
  • Back は依然としてバリデートしません: formnovalidate は htmx では なくネイティブの仕組みです。
  • リフレッシュもディープリンクも、どちらの経路でもサーバが考える 現在ステップに着地します。ステップは URL に、ドラフトはサーバに あるからです。
  • CSRF: htmx 側は meta 規約で、 ネイティブ側にはフレームワークの隠しフィールドが必要です — mutating-form と 同様です。
  • ドラフトの保存先(セッション、ドラフト行)はフレームワークの 関心事です — 契約が要求するのは、無損失な 戻る→次へ の往復と、 バリデートされないドラフトだけです。
  • file input はドラフトを往復しません — ファイルは別途 アップロードし(file-upload)、 保存済みアップロードへの参照をドラフトに持たせてください。
  • フローが再訪を許すなら、完了済みステップはそのステップ URL への リンクにできます。

ここでの主張 — ステッパーの再描画、無損失なドラフト往復、無効な ステップからの「戻る」脱出、値を保つ 422、完了 — は、実 htmx の ブラウザテスト(test-browser/multi-step-form.spec.mjs)で固定されて います。