ミューテーションフォーム
mutating-form は、サーバ状態を変更するフォームのための公認の
合成です: htmx の POST、4xx でのインラインバリデーションエラー、成功時の
フルページリダイレクト、ビジースピナーつき二重送信ガード、非 JS
フォールバック。すでにそれぞれ契約を持つ 3 つの部品 —
field-errors
フラグメント、
request-action の
ビジー / 無効化パターン、
confirm-action の
ゲート — を、コードジェネレーターがそのまま出力できる 1 つのフォームに
配線します。マークアップのバージョニングポリシーの
下で安定しています。
必要なのは installFieldErrors()(自動初期化の
./behaviors
バンドルに同梱)と、破壊的変種には installConfirm() です。
メールを空のまま(または taken@example.com で)送信すると、422 の
field-errors フラグメントがインラインに描画されます。有効な送信は契約の
成功ブランチに従います: サーバが 204 + HX-Redirect で応答し、htmx が
ウィンドウ全体をデモのランディングページへ遷移させます(戻るリンク
付き)。エンドポイントはサーバ契約のデモ実装で、
api/recipes/mutating-form/ 配下に置かれています(実際のアプリでは
/members など自前の URL を使います)。
2 つ目の確認ゲートつき Delete フォームは
確認つき破壊的変種のデモです — 契約は同じで、
Member deleted ページに着地します。
<form method="post" action="/members" data-hx-post="/members" data-hx-target="#member-form-errors" data-hx-swap="innerHTML" data-hx-disabled-elt="find button[type=submit]" data-hx-indicator="find .hc-spinner">
<!-- The 4xx fragment swaps in here; empty on success. --> <div id="member-form-errors"></div>
<div class="hc-field"> <label class="hc-field__label" for="email">Email</label> <input class="hc-input" id="email" name="email" type="email" required autocomplete="email"> </div>
<span class="hc-action"> <button class="hc-button" data-variant="primary" type="submit">Create</button> <span class="hc-spinner htmx-indicator" aria-hidden="true"></span> </span></form>この 1 要素に 4 つのことが乗っています:
method/actionはdata-hx-postをミラー(同じ URL)。 ビヘイビアが読み込まれなくても、フォームはネイティブに送信されます — 非 JS デグラデーションを参照。data-hx-target+data-hx-swap="innerHTML"が 4xx フラグメントをフォーム内コンテナへ着地させ、フォーム自体は無傷に 保ちます。data-hx-disabled-eltがリクエストの間、送信ボタンを無効化 します — 二重送信ガードです。data-hx-indicatorがリクエスト中にスピナーを表示します。 どちらも htmx ネイティブで、カスタム JS はありません。
成功: スワップせずリダイレクト
Section titled “成功: スワップせずリダイレクト”罠: data-hx-post では、htmx は生の 303 Location を透過的に追跡し、
リダイレクト先のページをフォームのターゲットにスワップします。
これは post/redirect/get として誤りです。修正は HX-Request ヘッダーで
分岐し、各呼び出し元に正しい答えを返すことです:
POST /members (no HX-Request) → 303 See Other, Location: /members/42 HX-Request: true → 204 No Content, HX-Redirect: /members/42- htmx には
HX-Redirect: /members/42つきの空ボディ。 htmx はネイティブに完全なwindow.locationナビゲーションを実行 します — 行き先は同じ、DOM スワップなし、糊のビヘイビア不要。 - 非 htmx(非 JS パス)には素の
303 Location。ブラウザが 自分で追跡します。
失敗: インラインフィールドエラー(4xx)
Section titled “失敗: インラインフィールドエラー(4xx)”バリデーション失敗時、サーバは正準の
field-errors フラグメントを
422 で返し、#member-form-errors へスワップさせます:
<div class="hc-alert" data-variant="error" role="alert" data-hc-field-errors> <p class="hc-alert__title">Please fix the errors below.</p> <ul class="hc-alert__errors"> <li class="hc-alert__error" data-field="email" data-code="duplicate" data-message-key="members.email.duplicate">email: already registered</li> </ul></div>installFieldErrors() が各項目を名前の一致するフィールドへ振り分け
(aria-invalid、aria-describedby、.hc-field__error スロット、
最初のフィールドへのフォーカス)、どのコントロールにも一致しない項目は
フォームレベルのエラーとしてサマリーに残します。
htmx ≥ 2 はデフォルトで非 2xx をスワップしません。一度だけグローバルに 422 のスワップを許可します:
document.body.addEventListener('htmx:beforeSwap', (event) => { if (event.detail.xhr.status === 422) { event.detail.shouldSwap = true; event.detail.isError = false; }});(あるいはフラグメントを 200 で返します — クライアント側の設定が
一切要らない唯一の代替です。HX-Retarget + HX-Reswap はスワップの
行き先と方法を操縦するだけで、htmx ≥ 2 ではスワップを許可しません。
これらを付けた 422 にもこの許可 — もしくは
htmx.config.responseHandling のルール — が必要です。
field-errors レシピを参照。)
確認つき破壊的変種
Section titled “確認つき破壊的変種”削除(または任意の破壊的送信)には、
confirm-action
パターンでゲートします。変更点は 2 つだけ: 送信ボタンに
data-hc-confirm、フォームに data-hx-trigger="hc:confirmed" —
htmx がネイティブ送信ではなく確認イベントで発火するようにします。
<form method="post" action="/members/42/delete" data-hx-post="/members/42/delete" data-hx-trigger="hc:confirmed" data-hx-target="#member-form-errors" data-hx-swap="innerHTML" data-hx-disabled-elt="find button[type=submit]" data-hx-indicator="find .hc-spinner"> <span class="hc-action"> <button class="hc-button" data-variant="error" type="submit" data-hc-confirm="Delete this member? This cannot be undone." data-hc-confirm-title="Delete member"> Delete </button> <span class="hc-spinner htmx-indicator" aria-hidden="true"></span> </span></form>エラー、リダイレクト、ビジー / 無効化ガードは作成フォームと同一です。 非 JS パスは素の送信でそのまま POST します — 確認ステップなしで。 どのみちサーバサイドで再バリデートするサーバにとって、これは安全な デグラデーションです。
非 JS デグラデーション
Section titled “非 JS デグラデーション”フォームが method/action を保っているため、ビヘイビアが読み込まれ
なくても何も失われません:
- 送信はネイティブに POST します。サーバは field-errors
フラグメントをインラインに含むフルページを再描画するか(サマリーの
アラートがすべてのエラーを列挙します — 何も失われません。フィールド
への振り分けはエンハンスメントです)、成功なら
303リダイレクトを 送ります。 - 二重送信ガードとスピナーは htmx のエンハンスメントで、なくても送信は 壊れません。
- 確認つき変種は、ダイアログなしで送信されます。
これが、生成されるフォームが常に data-hx-post と並べて
method/action を持つべき理由です。ページが
CSRFにも
依存しているなら、非 JS フォームにはフレームワークの隠し CSRF
フィールドも必要です — installCsrfHeader() がカバーするのは htmx
パスだけです。
サーバレスポンス契約
Section titled “サーバレスポンス契約”| リクエスト | レスポンス |
|---|---|
POST /members(htmx 成功) | 204 + HX-Redirect: /members/42 |
POST /members(非 JS 成功) | 303 + Location: /members/42 |
POST /members(無効) | 422 + field-errors フラグメント(#member-form-errors へスワップ) |
完全なワイヤ仕様 — フラグメントの形、i18n キー、htmx:beforeSwap の
許可、アクセシビリティ — は
レシピソースに
あります。キーボード・フォーカス・リダイレクト・二重送信の主張は、
まさにこのマークアップに対するブラウザテスト
(test-browser/mutating-form.spec.mjs)で固定されています。
アクセシビリティ
Section titled “アクセシビリティ”- エラーサマリーは
role="alert"を持つため、422のスワップ自体が 読み上げられます。続いてinstallFieldErrors()が名指しされた各 コントロールにaria-invalidとaria-describedbyを設定し、最初の 無効なコントロールへフォーカスします — 送信後エラーの標準 パターンです。 - 二重送信ガードはネイティブの
disabled属性 (data-hx-disabled-elt)なので、支援技術がビジー状態を報告 します。スピナーはaria-hidden="true"の装飾です。 - コントロールごとに本物の
<label class="hc-field__label" for="…">を 保ってください — フラグメントのエラーはnameでコントロールに 取り付きますが、支援技術にフィールドの名前を伝えるのはラベル です。
- フィールドエラーレシピ — この上に組む 4xx フラグメント。
- リクエストアクションレシピ — ビジー / 無効化パターン。
- 確認アクションレシピ — 破壊的送信のゲート。
- チェックボックス → 真偽値フィールド — フォーム内の真偽値入力。