コンテンツにスキップ

リモートダイアログ

remote-dialog は、サーバが所有するモーダルフローの正準パターン です: トリガーボタンが完全な <dialog> フラグメントをサーバから取得し、 installRemoteDialog ビヘイビアがそれを開き、中のフォームが正常に 送信されると installCloseDialog が再び閉じます。

別名: Ajax モーダル、サーバ描画モーダル。

Edit item… をクリックしてみてください — ボタンが完全な <dialog> フラグメントを api/recipes/remote-dialog/ 配下の実エンドポイントから 取得し、スワップ後に installRemoteDialog が開きます。Name を 空にして保存すると、サーバはエラー状態で再レンダリングしたダイアログを 422 で返し、ダイアログは開いたままになります。有効な保存は空の 200 を 返します — ダイアログが消え、保存内容を告げるトーストが上がります。

トリガーとホスト要素:

<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-rootinstallRemoteDialog ビヘイビアの オプトイン。

サーバは完全なダイアログを返します:

<!-- 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" 属性でフォームへ届きます。

  1. ユーザーが Edit をクリック。htmx が GET /items/123/edit を 送ります。
  2. htmx がレスポンスを #dialog-root へスワップ(innerHTML)。
  3. #dialog-roothtmx:afterSwap が発火。installRemoteDialog ビヘイビアがターゲット上の data-hc-remote-dialog-root を確認し、 最初の <dialog> 子孫を見つけて showModal() を呼びます。
  4. ユーザーが編集してフォームを送信。フォームには data-hx-post="/items/123"data-hc-close-dialog-on-success があります。
  5. サーバは、更新後の行 HTML(closest dialog へスワップされ、 ダイアログを置き換える)か、バリデーションエラーつきで再描画された フォーム(まだダイアログの中)の 4xx を返します。
  6. 2xx なら installCloseDialog が成功を確認して dialog.close() を 呼びます。ダイアログが消えます。

ダイアログへのスワップがダイアログ要素そのものをすでに置き換えている 場合(closest dialog ターゲット + outerHTML はそうなります)、 そのリクエストについて installCloseDialog は no-op です — ダイアログは すでに消えています。

タイミング返すもの
最初のトリガー(GET)完全な <dialog class="hc-dialog">…</dialog> フラグメント。
送信成功(POST)ダイアログの data-hx-target / data-hx-swap 経由で、ページ側ターゲット(例: 行)の更新後コンテンツ。
バリデーション失敗(422)エラー状態で再描画したダイアログ全体(問題のフィールドに data-invalid="true" / aria-invalid="true")に、HX-Retarget: #dialog-rootHX-Reswap: innerHTML を付けて返し、422 用の一度きりの htmx:beforeSwap 許可を使います。ルートへのリターゲットが要点です: 開いているダイアログを outerHTML で再スワップすると、新しい閉じた <dialog> が挿入され、afterSwap はその要素で発火します — installRemoteDialog が見張るルートではなく — ので、エラー状態は見えないまま着地します。ルートをスワップすればビヘイビアが再実行され、ダイアログはエラーを表示して開き直します。(installCloseDialog は 2xx でしか閉じないので、これを消すものはありません。)

サーバ駆動のトーストフィードバックには、成功レスポンスに HX-Trigger ヘッダーを含めます:

HX-Trigger: {"hc:toast":{"message":"Saved.","variant":"success"}}
  • ダイアログはネイティブの <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> — インライン描画にフォールバックしてください。