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 で送信項目を絞れます)。
参考リンク
このページの著者
経験:Webアプリ/業務システム
得意:PHP・JavaScript・MySQL・CSS
個人実績:フォーム生成基盤/クイズ学習プラットフォーム 等
詳しいプロフィールはこちら! もちもちみかんのプロフィール