htmx 逆引きレシピ
行を削除するには?
一覧の「削除」は、誤操作が起きやすい一方で、業務UIでは避けて通れない操作です。
htmxを使えば、確認ダイアログ→DELETE送信→結果の反映までを、必要な部分だけ差し替えてシンプルに実装できます。
このページでは「一覧の1行削除(一覧を再描画)」「削除後に行を消す」「権限がないときは弾く」の3例で、hx-delete / hx-confirm / hx-target / hx-swap を使った“安全な削除UI”をデモ付きで解説します。
実務で困りがちな「削除後の表示更新」と「権限チェック」まで、ひと通り揃えます。
使用するhtmx属性
hx-delete:DELETEリクエストを送って、削除処理を実行する(行削除向き)hx-confirm:送信前に確認ダイアログを出し、誤削除を防ぐhx-target:返ってきたHTMLを差し替える先の要素を指定(例:一覧全体/closest tr)hx-swap:差し替え方を指定(例:outerHTMLで行を消す/一覧コンポーネントを丸ごと更新)
利用シーン
- 「一覧の1行削除」:削除後に件数や並びを含めて一覧を正しい状態に揃えたい
- 「削除後に行を消す」:一覧全体は再描画せず、対象行だけを軽く消したい
- 「権限がないときは弾く」:削除は必ずサーバー側で権限チェックし、権限なしなら削除させない
① 一覧の1行削除(一覧を再描画)
削除ボタンで1行削除し、一覧コンポーネントをサーバーから取り直して再描画する基本形です。
削除後の「件数・並び・空表示」まで含めて、一覧を常に正しい状態に揃えられます。
HTML
<div class="DEMO">
<h4>① 一覧の1行削除(一覧を再描画)</h4>
<div id="DEMO_DELETE_1_LIST" class="TABLE_WRAPPER">
<table class="TABLE">
<thead>
<tr><th>ID</th><th>タイトル</th><th>操作</th></tr>
</thead>
<tbody>
<tr>
<td>101</td><td>申請A</td>
<td>
<button
type="button"
class="is-danger"
hx-delete="/htmx/demo/_delete_row_1.php?id=101&ids=101,102,103,104,105"
hx-confirm="この行を削除します。よろしいですか?"
hx-target="#DEMO_DELETE_1_LIST"
hx-swap="outerHTML"
>削除</button>
</td>
</tr>
<tr>
<td>102</td><td>申請B</td>
<td>
<button
type="button"
class="is-danger"
hx-delete="/htmx/demo/_delete_row_1.php?id=102&ids=101,102,103,104,105"
hx-confirm="この行を削除します。よろしいですか?"
hx-target="#DEMO_DELETE_1_LIST"
hx-swap="outerHTML"
>削除</button>
</td>
</tr>
<tr>
<td>103</td><td>申請C</td>
<td>
<button
type="button"
class="is-danger"
hx-delete="/htmx/demo/_delete_row_1.php?id=103&ids=101,102,103,104,105"
hx-confirm="この行を削除します。よろしいですか?"
hx-target="#DEMO_DELETE_1_LIST"
hx-swap="outerHTML"
>削除</button>
</td>
</tr>
<tr>
<td>104</td><td>申請D</td>
<td>
<button
type="button"
class="is-danger"
hx-delete="/htmx/demo/_delete_row_1.php?id=104&ids=101,102,103,104,105"
hx-confirm="この行を削除します。よろしいですか?"
hx-target="#DEMO_DELETE_1_LIST"
hx-swap="outerHTML"
>削除</button>
</td>
</tr>
<tr>
<td>105</td><td>申請E</td>
<td>
<button
type="button"
class="is-danger"
hx-delete="/htmx/demo/_delete_row_1.php?id=105&ids=101,102,103,104,105"
hx-confirm="この行を削除します。よろしいですか?"
hx-target="#DEMO_DELETE_1_LIST"
hx-swap="outerHTML"
>削除</button>
</td>
</tr>
</tbody>
</table>
</div>
<p class="HTMX-NOTE">削除後、一覧全体を差し替えます。</p>
</div>
PHP
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');
// HTMLエスケープ関数
function h(string $s): string {
// 特殊文字をエスケープする
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
// 初期データ(タイトル辞書)
$titles = [
// id => title
101 => '申請A',
// id => title
102 => '申請B',
// id => title
103 => '申請C',
// id => title
104 => '申請D',
// id => title
105 => '申請E',
];
// ids(現在の一覧ID)をGETから受け取る(なければ初期値を使う)
$idsRaw = (string)($_GET['ids'] ?? '');
// 前後空白を除去する
$idsRaw = trim($idsRaw);
// idsが空なら初期IDを使う
if ($idsRaw === '') {
// 初期IDをCSVにする
$idsRaw = '101,102,103,104,105';
}
// id(削除対象)をGETから受け取る(なければ空)
$deleteRaw = (string)($_GET['id'] ?? '');
// 前後空白を除去する
$deleteRaw = trim($deleteRaw);
// idsを配列に分割する
$parts = explode(',', $idsRaw);
// 正常なIDだけを集める配列
$ids = [];
// 分割した値を1つずつ見る
foreach ($parts as $p) {
// 前後空白を除去する
$p = trim($p);
// 数字だけなら採用する
if (preg_match('/^\d+$/', $p)) {
// intに変換する
$ids[] = (int)$p;
}
}
// 重複を消す(キー化して戻す)
$ids = array_values(array_unique($ids));
// 削除対象が数字なら除外する
if (preg_match('/^\d+$/', $deleteRaw)) {
// 削除IDをintにする
$deleteId = (int)$deleteRaw;
// 削除後のID配列
$next = [];
// 1つずつ比較して残す
foreach ($ids as $id) {
// 削除ID以外だけ残す
if ($id !== $deleteId) {
// 残す
$next[] = $id;
}
}
// 差し替える
$ids = $next;
}
// 現在のidsをCSVに戻す(次のボタンURL用)
$idsParam = implode(',', $ids);
// 一覧コンポーネントを丸ごと返す(hx-swap="outerHTML" 用)
echo '<div id="DEMO_DELETE_1_LIST" class="TABLE_WRAPPER">';
// テーブル開始
echo '<table class="TABLE">';
// ヘッダ
echo '<thead><tr><th>ID</th><th>タイトル</th><th>操作</th></tr></thead>';
// 本体開始
echo '<tbody>';
// 行が空ならメッセージ行を出す
if (count($ids) === 0) {
// 空行
echo '<tr><td colspan="3"><span class="HTMX-NOTE">データがありません</span></td></tr>';
} else {
// 各IDで行を出す
foreach ($ids as $id) {
// タイトルを決める(なければ自動)
$title = isset($titles[$id]) ? (string)$titles[$id] : ('申請' . (string)$id);
// この行の削除URLを作る(現在のidsも一緒に送る)
$url = '/htmx/demo/_delete_row_1.php?id=' . (int)$id . '&ids=' . rawurlencode($idsParam);
// 行開始
echo '<tr>';
// ID列
echo '<td>' . h((string)$id) . '</td>';
// タイトル列
echo '<td>' . h($title) . '</td>';
// 操作列開始
echo '<td>';
// 削除ボタン(一覧を再描画)
echo '<button type="button" class="is-danger" '
. 'hx-delete="' . h($url) . '" '
. 'hx-confirm="この行を削除します。よろしいですか?" '
. 'hx-target="#DEMO_DELETE_1_LIST" '
. 'hx-swap="outerHTML"'
. '>削除</button>';
// 操作列終了
echo '</td>';
// 行終了
echo '</tr>';
}
}
// tbody終了
echo '</tbody>';
// table終了
echo '</table>';
// div終了
echo '</div>';
デモ
① 一覧の1行削除(一覧を再描画)
| ID | タイトル | 操作 |
|---|---|---|
| 101 | 申請A | |
| 102 | 申請B | |
| 103 | 申請C | |
| 104 | 申請D | |
| 105 | 申請E |
削除後、一覧全体を差し替えます。
解説
- 削除ボタンに
hx-deleteを付け、削除対象IDをサーバーへ送ります。 hx-confirmで確認ダイアログを出し、OKのときだけ削除処理を実行します。hx-targetは一覧コンポーネント(例:#DEMO_DELETE_1_LIST)を指定し、一覧だけ更新します。hx-swap="outerHTML"で、一覧コンポーネントを丸ごと差し替えます(返すHTMLも同じIDのコンテナで包む)。- デモでは安定動作のため、現在表示中のID一覧(
ids)をURLで受け渡しし、PHP側で「削除後の一覧HTML」を確実に再生成します。
② 削除後に行を消す(行だけ削除)
削除成功後に、その行(<tr>)だけを消して画面を更新する例です。
一覧の再描画が不要な場面で、通信量とDOM更新を最小化できます。
HTML
<div class="DEMO">
<h4>② 削除後に行を消す(行だけ削除)</h4>
<div class="TABLE_WRAPPER">
<table class="TABLE">
<thead>
<tr>
<th class="W20pc">ID</th>
<th>タイトル</th>
<th class="W20pc">操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>201</td>
<td>タスクA</td>
<td>
<button
type="button"
class="is-danger"
hx-delete="/htmx/demo/_delete_row_2.php?id=201"
hx-confirm="この行を削除します。よろしいですか?"
hx-target="closest tr"
hx-swap="outerHTML"
>削除</button>
</td>
</tr>
<tr>
<td>202</td>
<td>タスクB</td>
<td>
<button
type="button"
class="is-danger"
hx-delete="/htmx/demo/_delete_row_2.php?id=202"
hx-confirm="この行を削除します。よろしいですか?"
hx-target="closest tr"
hx-swap="outerHTML"
>削除</button>
</td>
</tr>
<tr>
<td>203</td>
<td>タスクC</td>
<td>
<button
type="button"
class="is-danger"
hx-delete="/htmx/demo/_delete_row_2.php?id=203"
hx-confirm="この行を削除します。よろしいですか?"
hx-target="closest tr"
hx-swap="outerHTML"
>削除</button>
</td>
</tr>
</tbody>
</table>
</div>
<p class="HTMX-NOTE">成功時はサーバーが空レスポンスを返し、<code>outerHTML</code>で行が消えます。</p>
</div>
PHP
<?php
// 型を厳密に扱う
declare(strict_types=1);
// セッションを開始する(デモ用)
session_start();
// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');
// セッションに保存するキー名(このデモ専用)
$key = 'DEMO_DELETE_2_ROWS';
// 初期データがなければ用意する
if (!isset($_SESSION[$key]) || !is_array($_SESSION[$key])) {
// 初期の行データを作る
$_SESSION[$key] = [
// id => title
201 => 'タスクA',
// id => title
202 => 'タスクB',
// id => title
203 => 'タスクC',
];
}
// DELETE対象IDをGETから受け取る
$idRaw = (string)($_GET['id'] ?? '');
// 前後空白を除去する
$idRaw = trim($idRaw);
// IDが数字なら削除する
if (preg_match('/^\d+$/', $idRaw)) {
// 整数に変換する
$id = (int)$idRaw;
// 存在するなら消す
if (isset($_SESSION[$key][$id])) {
// 行を削除する
unset($_SESSION[$key][$id]);
}
}
// 成功時は空を返す(hx-swap="outerHTML" で行が消える)
echo '';
デモ
② 削除後に行を消す(行だけ削除)
| ID | タイトル | 操作 |
|---|---|---|
| 201 | タスクA | |
| 202 | タスクB | |
| 203 | タスクC |
成功時はサーバーが空レスポンスを返し、outerHTMLで行が消えます。
解説
- 削除ボタンに
hx-deleteを付け、削除処理を実行します。 hx-target="closest tr"で、差し替え対象を「押したボタンの行」に限定します。hx-swap="outerHTML"により、サーバーが空(または削除済み表示)を返せば、その行が置き換わって消えます。- PHP側は削除処理を行い、成功時は空レスポンスを返すだけでOKです。
③ 権限がないときは弾く(削除しない)
削除ボタンを押しても、権限がない場合は削除せず、行を残したまま「権限なし」を表示する例です。
削除系は事故が致命的なので、サーバー側で必ず権限チェックする前提で作ります。
HTML
<div class="DEMO">
<h4>③ 権限がないときは弾く(削除しない)</h4>
<div class=" TABLE_WRAPPER">
<table class="TABLE">
<thead>
<tr>
<th class="W20pc">ID</th>
<th class="W20pc">タイトル</th>
<th class="W20pc">操作</th>
<th>状態</th>
</tr>
</thead>
<tbody>
<tr>
<td>301</td>
<td>機密レコードA</td>
<td>
<button
type="button"
class="is-danger"
hx-delete="/htmx/demo/_delete_row_3.php?id=301&role=viewer"
hx-confirm="この行を削除します。よろしいですか?"
hx-target="closest tr"
hx-swap="outerHTML"
>削除</button>
</td>
<td><span class="HTMX-NOTE">-</span></td>
</tr>
<tr>
<td>302</td>
<td>機密レコードB</td>
<td>
<button
type="button"
class="is-danger"
hx-delete="/htmx/demo/_delete_row_3.php?id=302&role=viewer"
hx-confirm="この行を削除します。よろしいですか?"
hx-target="closest tr"
hx-swap="outerHTML"
>削除</button>
</td>
<td><span class="HTMX-NOTE">-</span></td>
</tr>
</tbody>
</table>
</div>
<p class="HTMX-NOTE">
このデモは <code>role=viewer</code> なので拒否されます。<code>role=admin</code> にすると削除成功(行が消える)挙動を確認できます。
</p>
</div>
PHP
<?php
// 型を厳密に扱う
declare(strict_types=1);
// セッションを開始する(デモ用)
session_start();
// 返すのはHTML(UTF-8)
header('Content-Type: text/html; charset=UTF-8');
// HTMLエスケープ関数
function h(string $s): string {
// 特殊文字をエスケープする
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
// セッションに保存するキー名(このデモ専用)
$key = 'DEMO_DELETE_3_ROWS';
// 初期データがなければ用意する
if (!isset($_SESSION[$key]) || !is_array($_SESSION[$key])) {
// 初期の行データを作る
$_SESSION[$key] = [
// id => title
301 => '機密レコードA',
// id => title
302 => '機密レコードB',
];
}
// DELETE対象IDをGETから受け取る
$idRaw = (string)($_GET['id'] ?? '');
// 前後空白を除去する
$idRaw = trim($idRaw);
// role(権限)をGETから受け取る(デモ用)
$role = (string)($_GET['role'] ?? 'viewer');
// 前後空白を除去する
$role = trim($role);
// IDが数字でなければ何もしないで終了する
if (!preg_match('/^\d+$/', $idRaw)) {
// 空を返す
echo '';
// 終了する
exit;
}
// 整数に変換する
$id = (int)$idRaw;
// 行タイトルを取り出す(なければ空)
$title = (string)($_SESSION[$key][$id] ?? '');
// 権限がadminでなければ「弾く」:行を残したままエラー状態の<tr>を返す
if ($role !== 'admin') {
// URL(同じ行をもう一度試せるようにする)
$url = '/htmx/demo/_delete_row_3.php?id=' . $id . '&role=' . rawurlencode($role);
// 行を開始する(hx-swap="outerHTML" でこの<tr>に置き換わる)
echo '<tr>';
// ID列
echo '<td>' . h((string)$id) . '</td>';
// タイトル列
echo '<td>' . h($title !== '' ? $title : ('ID ' . (string)$id)) . '</td>';
// 操作列開始
echo '<td>';
// 削除ボタン(同じ条件で再実行できる)
echo '<button type="button" class="is-danger" '
. 'hx-delete="' . h($url) . '" '
. 'hx-confirm="この行を削除します。よろしいですか?" '
. 'hx-target="closest tr" '
. 'hx-swap="outerHTML"'
. '>削除</button>';
// 操作列を閉じる
echo '</td>';
// 状態列(権限なし)
echo '<td><span class="FORM-RESULT is-ng"><strong>権限なし</strong>:削除できません</span></td>';
// 行を閉じる
echo '</tr>';
// 終了する
exit;
}
// adminなら削除する(存在する場合)
if (isset($_SESSION[$key][$id])) {
// 行を削除する
unset($_SESSION[$key][$id]);
}
// 成功時は空を返す(outerHTMLで行が消える)
echo '';
デモ
③ 権限がないときは弾く(削除しない)
| ID | タイトル | 操作 | 状態 |
|---|---|---|---|
| 301 | 機密レコードA | - | |
| 302 | 機密レコードB | - |
このデモは role=viewer なので拒否されます。role=admin にすると削除成功(行が消える)挙動を確認できます。
解説
- 削除ボタンに
hx-confirmを付け、誤操作を減らします。 hx-target="closest tr"+hx-swap="outerHTML"で、サーバーが返した(権限なし表示)でその行を置き換えます。 - 権限がある場合は削除処理を行い、成功時は空を返して行を消します。
- 権限がない場合は削除せず、同じ行を「エラーステータス付き
」として返し、ユーザーに理由を明示します。 参考リンク
このページの著者
経験:Webアプリ/業務システム
得意:PHP・JavaScript・MySQL・CSS
個人実績:フォーム生成基盤/クイズ学習プラットフォーム 等
詳しいプロフィールはこちら! もちもちみかんのプロフィール
シェアしてね