コンテンツにスキップ

確認アクション

confirm-action は、破壊的なアクション — htmx リクエスト、または 素のフォーム送信 — の前段にモーダルの確認ステップを追加します。 ボタンは普通の <button> のまま、ネットワークリクエストの所有者は htmx のまま — 小さなビヘイビアがダイアログを開き、ユーザーが承認した ときに hc:confirmed イベントを発火し直すだけです。htmx を使わない 素のフォームの submit ボタンでは、遮断した送信を form.requestSubmit() で完了させます。

別名: 確認ダイアログ。

Delete をクリックして承認してみてください — outerHTML スワップで 行が消え、本物の HX-Trigger ヘッダーが削除されたアイテム名を告げる トーストを発火します。下の「基本の使い方」のサーバなしプレビュー (結果を表示するだけ)と違い、こちらは実際のエンドポイントを往復します — サーバ応答契約をそのまま実装したデモで、api/recipes/confirm-action/ 配下に置かれています。サーバはステートレスなので、Reset rows は 行を再取得するだけです。

ItemActions

htmx リクエストを発行する要素に data-hc-confirm を追加し、htmx の トリガーを hc:confirmed でゲートします:

<button
class="hc-button"
data-variant="error"
type="button"
data-hc-confirm="Delete this item?"
data-hx-delete="/items/123"
data-hx-trigger="hc:confirmed"
data-hx-target="closest tr"
data-hx-swap="outerHTML">
Delete
</button>

試してみてください — Delete をクリックすると共有の確認ダイアログが 開きます。(このデモにはサーバがないため、承認すると htmx リクエストの 代わりに結果を表示します。)

流れは次のとおり:

  1. ユーザーがボタンをクリックします。
  2. ビヘイビアがクリックをインターセプトし、共有の確認ダイアログを 表示します。元のクリックは抑止され、htmx はまだ発火しません。
  3. ユーザーが承認すると、ビヘイビアは元のボタン上でバブリングする hc:confirmed イベントを発火します。
  4. data-hx-trigger="hc:confirmed" で待ち受けている htmx がリクエストを 送信します。
  5. サーバはターゲット領域の HTML(および任意で HX-Trigger イベント)を 返します。

ユーザーがキャンセルした場合、イベントは発火されず、何も起きません。

data-hc-confirm は、普通の <form> 内の submit ボタンでもそのまま 使えます — htmx 属性は不要です:

<form method="post" action="/items/123/delete">
<button class="hc-button" data-variant="error"
data-hc-confirm="この項目を削除しますか?">
削除
</button>
</form>

承認すると、ビヘイビアは form.requestSubmit(button) を呼び、遮断した 送信を本来のクリックと同じ形で完了させます: ボタンが submitter になる (formaction / formmethod が尊重される)ほか、制約バリデーションが 先に実行されます。キャンセルの場合は何も送信されません。

このパスが適用されるのは、ボタンにもフォームにも htmx の verb 属性 (hx-get/post/put/patch/delete および data-hx-* 変種)がない場合 のみです。どちらかにある時点で hc:confirmed の発火だけになり、 リクエストの所有者は htmx のままです — フォームが二重送信されることは ありません。

JavaScript がない環境でも同じマークアップが優雅に劣化します: フォームは ネイティブに送信され、確認ステップだけがなくなります。

メッセージは data-hc-confirm から取られます。残りの属性は任意です:

属性デフォルト
data-hc-confirm(必須) ダイアログの本文
data-hc-confirm-titleConfirm
data-hc-confirm-labelConfirm
data-hc-cancel-labelCancel
data-hc-confirm-variant発火元の data-variant、なければ primary
<button
class="hc-button"
data-variant="error"
type="button"
data-hc-confirm="Permanently delete invoice #123? This cannot be undone."
data-hc-confirm-title="Delete invoice"
data-hc-confirm-label="Delete"
data-hc-cancel-label="Keep"
data-hx-delete="/invoices/123"
data-hx-trigger="hc:confirmed"
data-hx-target="closest tr">
Delete
</button>

ボタンを .hc-action で包み htmx インジケーターを付けると、計画 §24 の 破壊的アクション体験一式になります:

<span class="hc-action">
<button
class="hc-button"
data-variant="error"
type="button"
data-hc-confirm="Delete this item?"
data-hx-delete="/items/123"
data-hx-trigger="hc:confirmed"
data-hx-target="closest tr"
data-hx-swap="outerHTML"
data-hx-disabled-elt="this"
data-hx-indicator="closest .hc-action">
Delete
</button>
<span class="hc-spinner htmx-indicator" aria-hidden="true"></span>
</span>

@hypermedia-components/core/macros をインポートすれば、同じパターンを 単一のカスタム要素として書けます:

<hc-confirm-action
method="delete"
action="/items/123"
target="closest tr"
swap="outerHTML"
variant="error"
message="Delete this item?"
confirm-label="Delete"
cancel-label="Keep">
Delete
</hc-confirm-action>

この要素は connectedCallback で一度だけ実行され、自身の子を上に示した 展開後の HTML に置き換えてから htmx.process(this) を呼び、htmx が新しい 属性を認識できるようにします。アップグレードは冪等です — 要素を再アタッチ しても再展開はされません。

属性:

属性デフォルト対応先
action(必須)htmx リクエストの URL
methodpostdata-hx-{method}
target(省略)data-hx-target
swapouterHTMLdata-hx-swap
variant(省略)ボタンの data-variant
messageContinue?data-hc-confirm
title(省略)data-hc-confirm-title
confirm-label(省略)data-hc-confirm-label
cancel-label(省略)data-hc-cancel-label
disabled-eltthisdata-hx-disabled-elt
indicatorclosest .hc-actiondata-hx-indicator
no-spinner(真偽値)設定時は .hc-spinner を省略

マクロは任意です — ドキュメント化された契約は展開後の HTML のほうです。 マクロの属性面から外れる必要がある場合(独自のボタン内容、追加のラッパー クラスなど)は、展開後の HTML をコピーして直接編集してください。

確認ゲートと htmx の間の契約を明文化します(これらはブラウザテストで 保証される事項であり、実装詳細ではありません):

  • クリックはキャプチャフェーズpreventDefault + stopPropagation によりインターセプトされます。したがって htmx (および要素上の他のクリックハンドラ)が元のアクティベーションを 観測することはありません。
  • そのため data-hx-trigger="hc:confirmed"必須です: リクエストを 開始できるのは、ユーザーが承認したときにトリガー要素上で発火される hc:confirmed イベントだけです。data-hc-confirm があるのにトリガーが デフォルトのクリックのままの要素は、htmx リクエストを一切発火しません。
  • キャンセルは何も発火しません。 リクエストもイベントも送信もなしです。
  • 素のフォームの submit ボタン(ボタンにもフォームにも htmx の verb 属性がない場合)は、hc:confirmed の後に form.requestSubmit(button) で送信されます — 素のフォームを参照。どちらかに htmx verb が あればこの分岐は抑止されるため、htmx 配線済みのフォームが二重発火する ことはありません。
  • htmx 自身の hx-confirm組み合わせないでください — htmx は アクティベーションを観測しないため hx-confirm は決して実行されず、 死んだ属性を書くことになります。1 要素につき 1 つの機構に絞ってください。
  • インターセプトは document レベルで委譲されているため、htmx によって スワップインされたトリガーも再初期化なしでゲートされます。

htmx インテグレーション → 確認ゲートの仕様も参照。

オーケストレーションをマークアップの隣に置きたい場合は、 installConfirm() に頼らず同じフローを _hyperscript で表現できます。data-hc-confirm 属性を外し、確認ステップをインラインで書き、htmx がすでに待ち受けている のと同じ hc:confirmed イベントを送ります:

<button
class="hc-button"
data-variant="error"
type="button"
data-hx-delete="/items/123"
data-hx-trigger="hc:confirmed"
data-hx-target="closest tr"
data-hx-swap="outerHTML"
_="on click
if confirm('Delete this item?')
send hc:confirmed to me
end">
Delete
</button>

ここでの confirm(...) はブラウザのブロッキングプリミティブで、 スタイルが当たらず、破壊的アクションには必ずしも望ましくありません。 ページレベルの単一要素から共有の <dialog class="hc-confirm-dialog"> マークアップを再利用するには、 hyperscript インテグレーションページを 参照してください。

この形式が向くケース:

  • 確認ステップが本当に一度きり(ページクローム内の削除ボタン 1 つ)で、 installConfirm をインポートしたくない。
  • ほかの理由ですでに _hyperscript を読み込んでいる。

vanilla 側に留まるべきケース:

  • 複数の要素が同じ確認フローを必要とする — installConfirm() 1 回の 呼び出しで全部カバーできます。
  • マークアップを自分で書かずに、共有のスタイル済みダイアログを使いたい。

サーバのレスポンスは htmx 属性が指定するとおりです — 確認ビヘイビアは リクエストの形を変えません。

行単位の削除の場合:

  • 成功時: ターゲット領域の HTML を返すか、htmx が何もスワップしない ステータスで空ボディを返します。上のレシピは最寄り <tr>outerHTML をスワップするので、サーバは新しい行の HTML(行が消えた なら何も)を返します。
  • トースト通知を出すには: レスポンスヘッダーに HX-Trigger: {"hc:toast":{"message":"Deleted","variant":"success"}} を含めます。
  • バリデーション失敗時: 失敗を説明する HTML を 4xx で返すか、 HX-Reswap / HX-Retarget でスワップ先を誘導します。
  • 共有ダイアログは標準の <dialog> 要素を showModal() で開きます。 フォーカストラップ、Escape で閉じる、背後コンテンツの inert は ブラウザが処理します。
  • ダイアログは aria-labelledby(タイトル)と aria-describedby (メッセージ)を宣言しているので、スクリーンリーダーは開いたときに 両方を読み上げます。
  • フォーカスはデフォルトで Cancel ボタンに移ります — 破壊的 アクションは、迷い込んだ Enter キーではなく明示的な承認を要求すべき です。
  • 閉じるとき — 承認、キャンセル、Escape のいずれでも — ネイティブの dialog.close()フォーカスを開いたトリガーへ返します。 ビヘイビア自身はフォーカスコードを追加しません。承認されたリクエストが そのトリガーを DOM から取り除く場合(テーブル行の削除が典型例)、次の フォーカス先をスワップの中に意図的に置いてください: 周辺領域を ターゲットにし、キーボードユーザーが着地すべき要素に autofocus (または tabindex="-1" + サーバ駆動のフォーカスヒント)を付けます。 そうしないと、ユーザーは静かに <body> に落とされてしまいます。
  • 元のボタンはネイティブ <button> のセマンティクスを保ちます。 アイコンだけでなく、見える説明的なラベル(DeleteDiscard changes) を使ってください。

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

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

ビヘイビアが読み込まれていなくても、ボタンは壊れません:

  • htmx なしでは、ボタンを <form method="post" action="/items/123/delete"> の中に置いて ください — 素のフォームのパスが確認の上で 送信します。htmx 属性つきのボタンは htmx が読み込まれていなければ 何のアクションも実行しません — マークアップの記述どおりです。
  • hc.behaviors.js なしでは、クリックは普通にバブリングします。htmx は hc:confirmed イベントを観測せず、何もしません。<noscript> の告知を 足すか、完全にサーバサイドの確認ページを描画するのが推奨の グレースフルデグラデーションです。
  • ボタン — アクションの 視覚面。
  • request-action レシピ — 確認なし・スピナーつきのバージョン。
  • live-search レシピ — 同じ htmx トリガーモデルを使います。