リモートダイアログ
remote-dialog は、サーバが所有するモーダルフローの正準パターン
です: トリガーボタンが完全な <dialog> フラグメントをサーバから取得し、
installRemoteDialog ビヘイビアがそれを開き、中のフォームが正常に
送信されると installCloseDialog が再び閉じます。
別名: Ajax モーダル、サーバ描画モーダル。
Edit item… をクリックしてみてください — ボタンが完全な <dialog>
フラグメントを api/recipes/remote-dialog/ 配下の実エンドポイントから
取得し、スワップ後に installRemoteDialog が開きます。Name を
空にして保存すると、サーバはエラー状態で再レンダリングしたダイアログを
422 で返し、ダイアログは開いたままになります。有効な保存は空の 200 を
返します — ダイアログが消え、保存内容を告げるトーストが上がります。
基本の使い方
Section titled “基本の使い方”トリガーとホスト要素:
<button class="hc-button" type="button" data-hx-get="/items/123/edit" data-hx-target="#dialog-root" data-hx-swap="innerHTML"> Edit</button>
<div id="dialog-root" data-hc-remote-dialog-root></div>ホスト要素には 2 つのマーカーがあります:
id="dialog-root"— htmx のスワップターゲット。data-hc-remote-dialog-root—installRemoteDialogビヘイビアの オプトイン。
サーバは完全なダイアログを返します:
<!-- GET /items/123/edit --><dialog class="hc-dialog" aria-labelledby="dialog-title"> <header class="hc-dialog__header"> <h2 class="hc-dialog__title" id="dialog-title">Edit item</h2> </header>
<form id="edit-item" data-hx-post="/items/123" data-hx-target="closest dialog" data-hx-swap="outerHTML" data-hc-close-dialog-on-success> <div class="hc-dialog__body"> <div class="hc-field"> <label class="hc-field__label" for="name">Name</label> <input id="name" class="hc-input" name="name" value="Acme"> </div> </div> </form>
<footer class="hc-dialog__footer"> <!-- method="dialog" closes the dialog natively — no JS. --> <form method="dialog"><button class="hc-button">Cancel</button></form> <button class="hc-button" data-variant="primary" type="submit" form="edit-item">Save</button> </footer></dialog>Cancel は自分専用の <form method="dialog"> に置きます —
dialog メソッドのフォームの送信はネイティブにダイアログを閉じます。
JavaScript は不要で、CSP が咎めるものもありません。フォームは入れ子に
できないため、フッターは編集フォームの外に置き、Save ボタンは
form="edit-item" 属性でフォームへ届きます。
何が起きるか
Section titled “何が起きるか”- ユーザーが Edit をクリック。htmx が
GET /items/123/editを 送ります。 - htmx がレスポンスを
#dialog-rootへスワップ(innerHTML)。 #dialog-rootでhtmx:afterSwapが発火。installRemoteDialogビヘイビアがターゲット上のdata-hc-remote-dialog-rootを確認し、 最初の<dialog>子孫を見つけてshowModal()を呼びます。- ユーザーが編集してフォームを送信。フォームには
data-hx-post="/items/123"とdata-hc-close-dialog-on-successがあります。 - サーバは、更新後の行 HTML(
closest dialogへスワップされ、 ダイアログを置き換える)か、バリデーションエラーつきで再描画された フォーム(まだダイアログの中)の 4xx を返します。 - 2xx なら
installCloseDialogが成功を確認してdialog.close()を 呼びます。ダイアログが消えます。
ダイアログへのスワップがダイアログ要素そのものをすでに置き換えている
場合(closest dialog ターゲット + outerHTML はそうなります)、
そのリクエストについて installCloseDialog は no-op です — ダイアログは
すでに消えています。
サーバレスポンス契約
Section titled “サーバレスポンス契約”| タイミング | 返すもの |
|---|---|
| 最初のトリガー(GET) | 完全な <dialog class="hc-dialog">…</dialog> フラグメント。 |
| 送信成功(POST) | ダイアログの data-hx-target / data-hx-swap 経由で、ページ側ターゲット(例: 行)の更新後コンテンツ。 |
| バリデーション失敗(422) | エラー状態で再描画したダイアログ全体(問題のフィールドに data-invalid="true" / aria-invalid="true")に、HX-Retarget: #dialog-root と HX-Reswap: innerHTML を付けて返し、422 用の一度きりの htmx:beforeSwap 許可を使います。ルートへのリターゲットが要点です: 開いているダイアログを outerHTML で再スワップすると、新しい閉じた <dialog> が挿入され、afterSwap はその要素で発火します — installRemoteDialog が見張るルートではなく — ので、エラー状態は見えないまま着地します。ルートをスワップすればビヘイビアが再実行され、ダイアログはエラーを表示して開き直します。(installCloseDialog は 2xx でしか閉じないので、これを消すものはありません。) |
サーバ駆動のトーストフィードバックには、成功レスポンスに
HX-Trigger ヘッダーを含めます:
HX-Trigger: {"hc:toast":{"message":"Saved.","variant":"success"}}アクセシビリティ
Section titled “アクセシビリティ”- ダイアログはネイティブの
<dialog>要素をshowModal()で使い ます。フォーカストラップ、Escape で閉じる、背景の inert はブラウザが 処理します。 - ラベルつきタイトル(
#dialog-title)を宣言し、次のいずれかを:<dialog>にaria-labelledby="dialog-title"を設定する、または- タイトルを最初のフォーカス可能要素にして AT に拾わせる。
- Cancel ボタンもダイアログを閉じます — 上のネイティブな
<form method="dialog">パターンで、 ダイアログコンポーネントが 推奨するのと同じものです。クリックハンドラーなし、ビヘイビアなし、 CSP セーフです。
プログレッシブエンハンスメント
Section titled “プログレッシブエンハンスメント”-
htmx なしでは、
<button data-hx-get="…">は何もしません。JavaScript なしでも Edit アクションを動かし続けるには、トリガーをリンクに します(<a>の中に<button>は置けません — 不正な HTML です):<a class="hc-button" href="/items/123/edit"data-hx-get="/items/123/edit"data-hx-target="#dialog-root"data-hx-swap="innerHTML">Edit</a>htmx が読み込まれていればクリックをインターセプトし、そうでなければ
hrefがフルページの編集画面へ遷移します。 -
hc.behaviors.jsなしでは、ダイアログのフラグメントはページに着地 しますが自動では開きません。サーバレスポンスの<dialog>の直後に 開くための<script>を足すか — 例:<script>document.currentScript.previousElementSibling.showModal()</script>— インライン描画にフォールバックしてください。
- ダイアログコンポーネント
- 確認アクションレシピ — ラウンドトリップなしの事前確認。
- フィルタポップオーバーレシピ — 非モーダルの兄弟。