htmx 逆引きレシピ
行をその場で編集するには?

公開日:
最終更新日:

一覧の「編集」は、別画面に遷移させるよりも、その場で編集できると操作が速くなります。
htmxなら、行を編集フォームに差し替え、保存で表示に戻し、キャンセルで元に戻す流れを“行だけ更新”でシンプルに作れます。

このページでは、インライン編集の定番である「行→編集フォームに差し替え」「保存→表示に戻す」「キャンセルで元に戻す」を1つのデモで実装します。
hx-get / hx-put / hx-target / hx-swap / hx-include を使い、業務UIでそのまま流用できる形にまとめます。

使用するhtmx属性

  • hx-get:行の現在値を送って、編集フォーム(行のHTML)を取得して差し替える(編集開始/キャンセル時に便利)
  • hx-put:編集後の値を送って、保存結果の行HTML(表示モード)を取得して差し替える(更新向き)
  • hx-target:差し替える要素を指定(このレシピでは closest tr で行だけ更新)
  • hx-swap:差し替え方を指定(このレシピでは outerHTML で行を丸ごと置換)
  • hx-include:リクエストに含める要素を指定(このレシピでは closest tr を送り、行の値をまとめて送信)

利用シーン

  • 「申請一覧の編集」:担当者やステータスなど、一覧の一部項目だけを素早く更新したい
  • 「顧客/商品マスタの軽微修正」:名前やタグなど、1行の項目をその場で編集して保存したい
  • 「設定一覧(管理画面)」:行ごとに編集→保存/キャンセルを繰り返し、作業効率を上げたい

行をその場で編集するには?(インライン編集)

一覧の「編集」を別画面に飛ばさず、その行の中だけで完結させると操作が一気に速くなります。
このデモでは、行→編集フォームに差し替え、保存で表示に戻し、キャンセルで元に戻す“インライン編集”を最小構成で実装します。

HTML

<div class="DEMO">

	<h4>行をその場で編集するには?(インライン編集)</h4>

	<p>
		「編集」を押すと、その行が編集フォーム(input/select)に差し替わります。<br>
		「保存」で表示行に戻し、「キャンセル」で編集前の表示に戻します。
	</p>

	<div class="TABLE_WRAPPER">
		<table class="TABLE">
			<thead>
				<tr>
					<th class="W5pc">ID</th>
					<th class="W30pc">タイトル</th>
					<th class="W15pc">担当</th>
					<th class="W15pc">ステータス</th>
					<th>操作</th>
				</tr>
			</thead>

			<tbody>

				<!-- 行(表示モード) -->
				<tr>
					<td>401</td>
					<td>申請:アカウント追加</td>
					<td>tanaka</td>
					<td><span class="BADGE">OPEN</span></td>
					<td>
						<!-- 状態(この行の現在値)をDOMに保持(hx-includeで送る) -->
						<input type="hidden" name="id" value="401">
						<input type="hidden" name="title" value="申請:アカウント追加">
						<input type="hidden" name="owner" value="tanaka">
						<input type="hidden" name="status" value="OPEN">

						<button
							type="button"
							hx-get="/htmx/demo/_inline_edit_row_edit.php"
							hx-include="closest tr"
							hx-target="closest tr"
							hx-swap="outerHTML"
						>編集</button>
					</td>
				</tr>

				<tr>
					<td>402</td>
					<td>申請:権限変更</td>
					<td>sato</td>
					<td><span class="BADGE">PENDING</span></td>
					<td>
						<input type="hidden" name="id" value="402">
						<input type="hidden" name="title" value="申請:権限変更">
						<input type="hidden" name="owner" value="sato">
						<input type="hidden" name="status" value="PENDING">

						<button
							type="button"
							hx-get="/htmx/demo/_inline_edit_row_edit.php"
							hx-include="closest tr"
							hx-target="closest tr"
							hx-swap="outerHTML"
						>編集</button>
					</td>
				</tr>

				<tr>
					<td>403</td>
					<td>申請:端末貸与</td>
					<td>suzuki</td>
					<td><span class="BADGE">DONE</span></td>
					<td>
						<input type="hidden" name="id" value="403">
						<input type="hidden" name="title" value="申請:端末貸与">
						<input type="hidden" name="owner" value="suzuki">
						<input type="hidden" name="status" value="DONE">

						<button
							type="button"
							hx-get="/htmx/demo/_inline_edit_row_edit.php"
							hx-include="closest tr"
							hx-target="closest tr"
							hx-swap="outerHTML"
						>編集</button>
					</td>
				</tr>

			</tbody>
		</table>
	</div>

</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');
}

// idを受け取る
$id = (string)($_GET['id'] ?? '');

// titleを受け取る
$title = (string)($_GET['title'] ?? '');

// ownerを受け取る
$owner = (string)($_GET['owner'] ?? '');

// statusを受け取る
$status = (string)($_GET['status'] ?? '');

// 前後空白を除去する
$id = trim($id);

// 前後空白を除去する
$title = trim($title);

// 前後空白を除去する
$owner = trim($owner);

// 前後空白を除去する
$status = trim($status);

// idが数字でなければダミーにする
if (!preg_match('/^\d+$/', $id)) $id = '0';

// status候補
$statuses = ['OPEN', 'PENDING', 'DONE'];

// statusが候補外ならOPENにする
if (!in_array($status, $statuses, true)) $status = 'OPEN';

// 編集行(tr)を返す(outerHTMLで置換される)
echo '<tr>';

// ID列
echo '<td>' . h($id) . '</td>';

// タイトル列(編集input)
echo '<td>';

// title入力
echo '<input type="text" name="title" value="' . h($title) . '" class="ALT_INPUT">';

// タイトル列を閉じる
echo '</td>';

// 担当列(編集input)
echo '<td>';

// owner入力
echo '<input type="text" name="owner" value="' . h($owner) . '" class="ALT_INPUT">';

// 担当列を閉じる
echo '</td>';

// ステータス列(select)
echo '<td>';

// select開始
echo '<select name="status">';

// OPEN
echo '<option value="OPEN"' . ($status === 'OPEN' ? ' selected' : '') . '>OPEN</option>';

// PENDING
echo '<option value="PENDING"' . ($status === 'PENDING' ? ' selected' : '') . '>PENDING</option>';

// DONE
echo '<option value="DONE"' . ($status === 'DONE' ? ' selected' : '') . '>DONE</option>';

// select終了
echo '</select>';

// ステータス列を閉じる
echo '</td>';

// 操作列
echo '<td>';

// idをhiddenで持つ(保存/キャンセルで使う)
echo '<input type="hidden" name="id" value="' . h($id) . '">';

// キャンセル用に「編集前の値」をhiddenで保持する
echo '<input type="hidden" name="orig_title" value="' . h($title) . '">';

// キャンセル用に「編集前の値」をhiddenで保持する
echo '<input type="hidden" name="orig_owner" value="' . h($owner) . '">';

// キャンセル用に「編集前の値」をhiddenで保持する
echo '<input type="hidden" name="orig_status" value="' . h($status) . '">';

// 保存ボタン(PUTで送る)
echo '<button type="button" class="is-ok W48pc" '
	. 'hx-put="/htmx/demo/_inline_edit_row_save.php" '
	. 'hx-include="closest tr" '
	. 'hx-target="closest tr" '
	. 'hx-swap="outerHTML"'
	. 'style="width:47.5%;"'
	. '>保存</button> ';

// キャンセルボタン(編集前に戻す)
echo '<button type="button" class="is-ghost" '
	. 'hx-get="/htmx/demo/_inline_edit_row_view.php" '
	. 'hx-include="closest tr" '
	. 'hx-target="closest tr" '
	. 'hx-swap="outerHTML"'
	. 'style="width:47.5%;"'
	. '>キャンセル</button>';

// 操作列を閉じる
echo '</td>';

// 行を閉じる
echo '</tr>';

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');
}

// PUTの生データを読む
$raw = (string)file_get_contents('php://input');

// パース先の配列
$data = [];

// application/x-www-form-urlencoded を配列にする
parse_str($raw, $data);

// idを受け取る
$id = (string)($data['id'] ?? '0');

// titleを受け取る
$title = (string)($data['title'] ?? '');

// ownerを受け取る
$owner = (string)($data['owner'] ?? '');

// statusを受け取る
$status = (string)($data['status'] ?? 'OPEN');

// 前後空白を除去する
$id = trim($id);

// 前後空白を除去する
$title = trim($title);

// 前後空白を除去する
$owner = trim($owner);

// 前後空白を除去する
$status = trim($status);

// idが数字でなければ0にする
if (!preg_match('/^\d+$/', $id)) $id = '0';

// titleが空なら仮の値にする(デモ用)
if ($title === '') $title = '(未入力)';

// ownerが空なら仮の値にする(デモ用)
if ($owner === '') $owner = '(未入力)';

// status候補
$statuses = ['OPEN', 'PENDING', 'DONE'];

// statusが候補外ならOPENにする
if (!in_array($status, $statuses, true)) $status = 'OPEN';

// バッジ表示用(簡易)
$badge = $status;

// 表示行(tr)を返す(outerHTMLで置換される)
echo '<tr>';

// ID列
echo '<td>' . h($id) . '</td>';

// タイトル列
echo '<td>' . h($title) . '</td>';

// 担当列
echo '<td>' . h($owner) . '</td>';

// ステータス列(バッジ)
echo '<td><span class="BADGE">' . h($badge) . '</span></td>';

// 操作列
echo '<td>';

// 現在値をhiddenに保持(次回の編集で使う)
echo '<input type="hidden" name="id" value="' . h($id) . '">';

// 現在値をhiddenに保持(次回の編集で使う)
echo '<input type="hidden" name="title" value="' . h($title) . '">';

// 現在値をhiddenに保持(次回の編集で使う)
echo '<input type="hidden" name="owner" value="' . h($owner) . '">';

// 現在値をhiddenに保持(次回の編集で使う)
echo '<input type="hidden" name="status" value="' . h($status) . '">';

// 編集ボタン(GETで編集行に差し替える)
echo '<button type="button" class="" '
	. 'hx-get="/htmx/demo/_inline_edit_row_edit.php" '
	. 'hx-include="closest tr" '
	. 'hx-target="closest tr" '
	. 'hx-swap="outerHTML"'
	. '>編集</button>';

// 操作列を閉じる
echo '</td>';

// 行を閉じる
echo '</tr>';

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');
}

// idを受け取る
$id = (string)($_GET['id'] ?? '0');

// 編集前のtitleを受け取る(キャンセル用)
$title  = (string)($_GET['orig_title'] ?? ($_GET['title'] ?? ''));

// 編集前のownerを受け取る(キャンセル用)
$owner  = (string)($_GET['orig_owner'] ?? ($_GET['owner'] ?? ''));

// 編集前のstatusを受け取る(キャンセル用)
$status = (string)($_GET['orig_status'] ?? ($_GET['status'] ?? 'OPEN'));

// 前後空白を除去する
$id = trim($id);

// 前後空白を除去する
$title = trim($title);

// 前後空白を除去する
$owner = trim($owner);

// 前後空白を除去する
$status = trim($status);

// idが数字でなければ0にする
if (!preg_match('/^\d+$/', $id)) $id = '0';

// titleが空なら仮の値にする(デモ用)
if ($title === '') $title = '(不明)';

// ownerが空なら仮の値にする(デモ用)
if ($owner === '') $owner = '(不明)';

// status候補
$statuses = ['OPEN', 'PENDING', 'DONE'];

// statusが候補外ならOPENにする
if (!in_array($status, $statuses, true)) $status = 'OPEN';

// 表示行(tr)を返す(outerHTMLで置換される)
echo '<tr>';

// ID列
echo '<td>' . h($id) . '</td>';

// タイトル列
echo '<td>' . h($title) . '</td>';

// 担当列
echo '<td>' . h($owner) . '</td>';

// ステータス列
echo '<td><span class="BADGE">' . h($status) . '</span></td>';

// 操作列
echo '<td>';

// 現在値をhiddenに保持(次回の編集で使う)
echo '<input type="hidden" name="id" value="' . h($id) . '">';

// 現在値をhiddenに保持(次回の編集で使う)
echo '<input type="hidden" name="title" value="' . h($title) . '">';

// 現在値をhiddenに保持(次回の編集で使う)
echo '<input type="hidden" name="owner" value="' . h($owner) . '">';

// 現在値をhiddenに保持(次回の編集で使う)
echo '<input type="hidden" name="status" value="' . h($status) . '">';

// 編集ボタン
echo '<button type="button" class="" '
	. 'hx-get="/htmx/demo/_inline_edit_row_edit.php" '
	. 'hx-include="closest tr" '
	. 'hx-target="closest tr" '
	. 'hx-swap="outerHTML"'
	. '>編集</button>';

// 操作列を閉じる
echo '</td>';

// 行を閉じる
echo '</tr>';

デモ

行をその場で編集するには?(インライン編集)

「編集」を押すと、その行が編集フォーム(input/select)に差し替わります。
「保存」で表示行に戻し、「キャンセル」で編集前の表示に戻します。

ID タイトル 担当 ステータス 操作
401 申請:アカウント追加 tanaka OPEN
402 申請:権限変更 sato PENDING
403 申請:端末貸与 suzuki DONE

解説

1) 行→編集フォームに差し替え(編集開始)

  • 表示行(<tr>)の中に、現在値を <input type="hidden"> として持たせます。
  • 「編集」ボタンで hx-get を呼び、hx-include="closest tr" でその行の値をまとめて送ります。
  • サーバーは「編集フォームになった行(<tr>)」を返し、hx-target="closest tr"hx-swap="outerHTML" で行だけ差し替えます。

2) 保存→表示に戻す(更新)

  • 編集行の「保存」ボタンで hx-put を呼び、編集後の値(title/owner/status)を送ります。
  • PHP側は受け取った値を整形・検証し(デモでは簡易)、表示モードの行HTMLを返します。
  • 返ってきた表示行が outerHTML で置換され、編集が確定した状態になります。

3) キャンセルで元に戻す(ロールバック)

  • 編集開始時点の値(orig_*)を編集行に hidden として保持しておきます。
  • 「キャンセル」ボタンは hx-get で表示行を取り直し、hx-include="closest tr"orig_* をサーバーへ送ります。
  • サーバーは orig_* を使って“編集前の表示行”を返し、行が元に戻ります。

ポイント:行単位で outerHTML 置換に統一すると、対象がズレにくく実装が安定します。
また hx-include="closest tr" は「行の値をまとめて送る」用途で強力です(必要なら hx-params で送信項目を絞れます)。

このページの著者

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

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

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

得意:PHP・JavaScript・MySQL・CSS

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

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

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

もちもちみかん.comとは


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

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