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"で、サーバーが返した(権限なし表示)でその行を置き換えます。
  • 権限がある場合は削除処理を行い、成功時は空を返して行を消します。
  • 権限がない場合は削除せず、同じ行を「エラーステータス付き」として返し、ユーザーに理由を明示します。

このページの著者

もちもちみかん(システムエンジニア)

社内SEとしてグループ企業向けの業務アプリを要件定義〜運用まで一気通貫で担当しています。

経験:Webアプリ/業務システム

得意:PHP・JavaScript・MySQL・CSS

個人実績:フォーム生成基盤クイズ学習プラットフォーム

詳しいプロフィールはこちら!  もちもちみかんのプロフィール

もちもちみかん0系くん
TOPへ

もちもちみかん.comとは


このサイトでは、コーディングがめんどうくさい人向けのお助けツールとして、フォームやCSSをノーコードで生成できる、
 もちもちみかん.forms
 もちもちみかん.css1
 もちもちみかん.css2
と言ったジェネレーターを用意してます。

また、このサイトを通じて、「もちもちみかん」のかわいさを普及したいとかんがえてます!