htmx 逆引きレシピ
別の場所も同時に更新するには?
メイン領域(フォーム結果や一覧)だけでなく、ヘッダや通知、サイドバーも同時に更新したい場面は多くあります。
このページでは、レスポンスに複数の更新断片を同梱し、OOBで別エリアへ反映する手順を3つのデモで整理します。
使用するhtmx属性
タグ:hx-swap-oob / hx-select-oob / hx-target / hx-swap
hx-target:メインで更新する標準の反映先を指定します。hx-swap:メイン更新の差し替え方(innerHTML/outerHTML)を指定します。hx-swap-oob:レスポンス内の別要素を、ターゲット以外の場所へ同時反映します。hx-select-oob:レスポンス全体から必要なOOB断片だけ抽出して反映します。
利用シーン
- 保存直後にトースト通知を出したい(保存結果とは別に通知UIを更新)
- 一覧を更新したタイミングでヘッダ未読件数も合わせたい
- メイン更新と同時にサイドバー集計も最新化したい
更新対象ID(共通)
この3箇所を、各デモのレスポンスから同時更新します。
IDを固定しておくと、OOB更新の受け皿が明確になります。
HTML
_demo_htmx_0.php
<div class="DEMO">
<div class="CARD">
<h4>ヘッダ未読件数</h4>
<div>未読:<?= '<span id="HEADER_UNREAD_BADGE" class="BADGE">5</span>' ?></div>
</div>
<div id="GLOBAL_TOAST" class="CARD" aria-live="polite">
<strong>通知</strong>
<div>ここにトースト通知が表示されます。</div>
</div>
<aside id="SIDEBAR_STATS" class="CARD" aria-live="polite">
<h4>サイドバー集計</h4>
<ul>
<li>未対応:3</li>
<li>完了:12</li>
</ul>
</aside>
</div>
デモ(各デモを動かすと、このデモも更新されます)
ヘッダ未読件数
共通PHP(全文)
XSS対策の htmlspecialchars を共通関数化し、各デモの返却HTMLを統一しています。
トースト/バッジ/サイドバーの描画関数もここに集約します。
PHP(_multi_update_oob_data.php)
/htmx/demo/_multi_update_oob_data.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// HTML断片として返す
header('Content-Type: text/html; charset=UTF-8');
// 文字列を安全に表示する
function multi_update_oob_h(string $s): string
{
// HTML特殊文字をエスケープして返す
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
// 時刻文字列を返す
function multi_update_oob_now(): string
{
// 現在時刻を返す
return date('Y-m-d H:i:s');
}
// DEMO1のメイン結果を返す
function multi_update_oob_demo1_result(string $subject, string $status): string
{
// 件名を安全化する
$subjectEsc = multi_update_oob_h($subject);
// ステータスを安全化する
$statusEsc = multi_update_oob_h($status);
// 時刻を作る
$nowEsc = multi_update_oob_h(multi_update_oob_now());
// 結果HTMLを返す
return '<div id="DEMO1_RESULT" class="CARD is-ok"><strong>保存完了</strong><div>件名:' . $subjectEsc . '</div><div>状態:' . $statusEsc . '</div><div>更新時刻:' . $nowEsc . '</div></div>';
}
// トースト領域を返す
function multi_update_oob_toast(string $message, string $tone = 'ok'): string
{
// 文言を安全化する
$messageEsc = multi_update_oob_h($message);
// 時刻を安全化する
$nowEsc = multi_update_oob_h(multi_update_oob_now());
// toneがngならngクラスにする
$classTone = ($tone === 'ng') ? 'is-ng' : 'is-ok';
// OOBトーストを返す
return '<div id="GLOBAL_TOAST" class="CARD ' . $classTone . '" hx-swap-oob="outerHTML"><strong>通知</strong><div>' . $messageEsc . '</div><div class="HTMX-NOTE">' . $nowEsc . '</div></div>';
}
// ヘッダ未読バッジを返す
function multi_update_oob_badge(int $count, bool $oob = false): string
{
// 件数を0以上に補正する
$count = max(0, $count);
// 件数表示を作る
$countEsc = multi_update_oob_h((string)$count);
// OOB属性を必要時だけ付ける
$oobAttr = $oob ? ' hx-swap-oob="outerHTML"' : '';
// バッジHTMLを返す
return '<span id="HEADER_UNREAD_BADGE" class="BADGE"' . $oobAttr . '>' . $countEsc . '</span>';
}
// サイドバー集計を返す
function multi_update_oob_sidebar(int $waiting, int $done, bool $oob = false): string
{
// 未対応件数を0以上に補正する
$waiting = max(0, $waiting);
// 完了件数を0以上に補正する
$done = max(0, $done);
// OOB属性を必要時だけ付ける
$oobAttr = $oob ? ' hx-swap-oob="outerHTML"' : '';
// 件数を安全化する
$waitingEsc = multi_update_oob_h((string)$waiting);
// 件数を安全化する
$doneEsc = multi_update_oob_h((string)$done);
// サイドバーHTMLを返す
return '<aside id="SIDEBAR_STATS" class="CARD"' . $oobAttr . '><h4>サイドバー集計</h4><ul><li>未対応:' . $waitingEsc . '</li><li>完了:' . $doneEsc . '</li></ul></aside>';
}
// DEMO2のメイン一覧を返す
function multi_update_oob_demo2_list(string $title, array $rows): string
{
// タイトルを安全化する
$titleEsc = multi_update_oob_h($title);
// 先頭HTMLを作る
$html = '<div id="DEMO2_LIST" class="CARD"><strong>' . $titleEsc . '</strong><ul>';
// 行を順番に連結する
foreach ($rows as $row) {
// 1行を安全化する
$rowEsc = multi_update_oob_h((string)$row);
// liを連結する
$html .= '<li>' . $rowEsc . '</li>';
}
// 終端タグを連結する
$html .= '</ul></div>';
// 完成HTMLを返す
return $html;
}
// DEMO3のメインカードを返す
function multi_update_oob_demo3_main(string $action, int $processed): string
{
// アクションを安全化する
$actionEsc = multi_update_oob_h($action);
// 処理件数を安全化する
$processedEsc = multi_update_oob_h((string)$processed);
// 時刻を安全化する
$nowEsc = multi_update_oob_h(multi_update_oob_now());
// メインHTMLを返す
return '<section id="DEMO3_RESULT" class="CARD"><h4>DEMO3 メイン更新</h4><div>処理:' . $actionEsc . '</div><div>更新件数:' . $processedEsc . '</div><div class="HTMX-NOTE">更新時刻:' . $nowEsc . '</div></section>';
}
① 保存→トースト通知(OOBでトーストのみ更新)
メインは #DEMO1_RESULT を通常更新し、通知は #GLOBAL_TOAST だけOOBで差し替えます。
保存結果と通知の表示責務を分離できるため、メインUIを崩さずに反映できます。
HTML
_demo_htmx_1.php
<div class="DEMO">
<h4>保存→トースト通知(OOB)</h4>
<form
id="DEMO1_FORM"
class="FORM"
method="post"
hx-post="/htmx/demo/_multi_update_oob_save.php"
hx-target="#DEMO1_RESULT"
hx-swap="innerHTML"
hx-indicator="#DEMO1_LOADING"
>
<label>
件名
<input type="text" name="subject" value="見積書を保存">
</label>
<label>
状態
<select name="status">
<option value="下書き">下書き</option>
<option value="承認待ち">承認待ち</option>
<option value="完了">完了</option>
</select>
</label>
<button type="submit" class="BTN">保存する</button>
<span id="DEMO1_LOADING" class="LOADING" aria-live="polite">保存中…</span>
</form>
<div id="DEMO1_RESULT" class="CARD" aria-live="polite">ここに保存結果が表示されます。</div>
</div>
PHP(送信先)
/htmx/demo/_multi_update_oob_save.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通関数を読み込む
require_once(__DIR__ . '/_multi_update_oob_data.php');
// リクエストメソッドを受け取る
$method = (string)($_SERVER['REQUEST_METHOD'] ?? '');
// POST以外は拒否する
if ($method !== 'POST') {
// 405を返す
http_response_code(405);
// エラー本文を返す
echo multi_update_oob_demo1_result('保存対象なし', 'POSTで送信してください。');
// OOBでトーストも返す
echo multi_update_oob_toast('POSTでアクセスしてください。', 'ng');
// 処理を終える
exit;
}
// 件名を受け取る
$subject = trim((string)($_POST['subject'] ?? ''));
// 状態を受け取る
$status = trim((string)($_POST['status'] ?? ''));
// 件名が空なら補う
if ($subject === '') {
// 補助文言を入れる
$subject = '(無題)';
}
// 状態が空なら補う
if ($status === '') {
// 補助文言を入れる
$status = '下書き';
}
// メイン更新HTMLを返す
echo multi_update_oob_demo1_result($subject, $status);
// OOBでトースト更新を返す
echo multi_update_oob_toast('「' . $subject . '」を保存しました。');
デモ
保存→トースト通知(OOB)
解説
- メイン更新は
hx-target="#DEMO1_RESULT"+hx-swap="innerHTML"で処理します。 - サーバは同じレスポンスに
#GLOBAL_TOAST(hx-swap-oob付き)を同梱します。 - 結果表示と通知表示を同時に更新しても、1リクエストで完結できます。
② 一覧更新+ヘッダ未読件数(OOB)
一覧更新は #DEMO2_LIST に反映し、同時に #HEADER_UNREAD_BADGE をOOB差し替えします。
ヘッダの件数を手動同期せず、レスポンス一回で整合させる基本形です。
HTML
_demo_htmx_2.php
<div class="DEMO">
<h4>一覧更新+ヘッダ未読件数(OOB)</h4>
<form
id="DEMO2_FORM"
class="FORM"
method="post"
hx-post="/htmx/demo/_multi_update_oob_1.php"
hx-target="#DEMO2_LIST"
hx-swap="outerHTML"
hx-indicator="#DEMO2_LOADING"
>
<label>
更新対象
<input type="text" name="title" value="受信箱">
</label>
<button type="submit" class="BTN">一覧を更新</button>
<span id="DEMO2_LOADING" class="LOADING" aria-live="polite">更新中…</span>
</form>
<div id="DEMO2_LIST" class="CARD" aria-live="polite">ここに一覧更新結果が表示されます。</div>
</div>
PHP(送信先)
/htmx/demo/_multi_update_oob_1.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通関数を読み込む
require_once(__DIR__ . '/_multi_update_oob_data.php');
// リクエストメソッドを受け取る
$method = (string)($_SERVER['REQUEST_METHOD'] ?? '');
// POST以外は拒否する
if ($method !== 'POST') {
// 405を返す
http_response_code(405);
// メイン更新だけ返す
echo multi_update_oob_demo2_list('DEMO2 一覧', ['POSTで送信してください。']);
// OOBで未読バッジを返す
echo multi_update_oob_badge(0, true);
// 処理を終える
exit;
}
// 操作対象を受け取る
$title = trim((string)($_POST['title'] ?? ''));
// 対象が空なら補う
if ($title === '') {
// 補助文言を入れる
$title = '新着メッセージ';
}
// 1〜9の範囲で未読件数を作る
$unread = random_int(1, 9);
// 一覧行を作る
$rows = [
'処理対象:' . $title,
'反映時刻:' . multi_update_oob_now(),
'一覧を更新しつつ、ヘッダ件数も同時更新しました。',
];
// メイン一覧を返す
echo multi_update_oob_demo2_list('DEMO2 一覧更新', $rows);
// OOBでヘッダ未読件数を返す
echo multi_update_oob_badge($unread, true);
デモ
一覧更新+ヘッダ未読件数(OOB)
解説
- メイン領域は通常swap、ヘッダ件数は
hx-swap-oobで別ターゲット更新します。 #HEADER_UNREAD_BADGEを固定ID化しておくと、更新責務が明確になります。- 画面上の異なる領域を同時反映できるため、表示ズレを抑えやすくなります。
③ メイン+ヘッダ+サイドバー(複数OOB)
メイン更新に加え、ヘッダ件数とサイドバー集計も同時更新します。
このデモは hx-select-oob を使い、返却HTMLから必要断片だけOOB反映します。
HTML
_demo_htmx_3.php
<div class="DEMO">
<h4>メイン+ヘッダ+サイドバー(複数OOB)</h4>
<form
id="DEMO3_FORM"
class="FORM"
method="post"
hx-post="/htmx/demo/_multi_update_oob_2.php"
hx-target="#DEMO3_RESULT"
hx-swap="outerHTML"
hx-select-oob="#HEADER_UNREAD_BADGE:outerHTML,#SIDEBAR_STATS:outerHTML"
hx-indicator="#DEMO3_LOADING"
>
<label>
一括操作
<select name="action">
<option value="既読化">既読化</option>
<option value="完了へ移動">完了へ移動</option>
<option value="担当へ通知">担当へ通知</option>
</select>
</label>
<button type="submit" class="BTN">3箇所を同時更新</button>
<span id="DEMO3_LOADING" class="LOADING" aria-live="polite">反映中…</span>
</form>
<section id="DEMO3_RESULT" class="CARD" aria-live="polite">
<h4>DEMO3 メイン更新</h4>
<div>ここにメイン更新結果が表示されます。</div>
</section>
</div>PHP(送信先)
/htmx/demo/_multi_update_oob_2.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 共通関数を読み込む
require_once(__DIR__ . '/_multi_update_oob_data.php');
// リクエストメソッドを受け取る
$method = (string)($_SERVER['REQUEST_METHOD'] ?? '');
// POST以外は拒否する
if ($method !== 'POST') {
// 405を返す
http_response_code(405);
// 補助値を作る
$demo3Action = 'POSTで送信してください。';
// 補助値を作る
$demo3Processed = 0;
// 補助値を作る
$demo3Unread = 0;
// 補助値を作る
$demo3Waiting = 0;
// 補助値を作る
$demo3Done = 0;
// テンプレートを返す
require(__DIR__ . '/_multi_update_oob_3.php');
// 処理を終える
exit;
}
// 操作種別を受け取る
$action = trim((string)($_POST['action'] ?? '既読化'));
// 操作が空なら補う
if ($action === '') {
// 補助文言を入れる
$action = '既読化';
}
// 処理件数を作る
$processed = random_int(1, 5);
// 未読件数を作る
$unread = random_int(0, 12);
// 未対応件数を作る
$waiting = random_int(0, 9);
// 完了件数を作る
$done = random_int(10, 24);
// テンプレート渡し値を作る
$demo3Action = $action;
// テンプレート渡し値を作る
$demo3Processed = $processed;
// テンプレート渡し値を作る
$demo3Unread = $unread;
// テンプレート渡し値を作る
$demo3Waiting = $waiting;
// テンプレート渡し値を作る
$demo3Done = $done;
// レスポンステンプレートを返す
require(__DIR__ . '/_multi_update_oob_3.php');
/htmx/demo/_multi_update_oob_3.php
<?php
// 型を厳密に扱う
declare(strict_types=1);
// 変数が未定義なら補う
$demo3Action = isset($demo3Action) ? (string)$demo3Action : '未設定';
// 変数が未定義なら補う
$demo3Processed = isset($demo3Processed) ? (int)$demo3Processed : 0;
// 変数が未定義なら補う
$demo3Unread = isset($demo3Unread) ? (int)$demo3Unread : 0;
// 変数が未定義なら補う
$demo3Waiting = isset($demo3Waiting) ? (int)$demo3Waiting : 0;
// 変数が未定義なら補う
$demo3Done = isset($demo3Done) ? (int)$demo3Done : 0;
// メイン更新を返す
echo multi_update_oob_demo3_main($demo3Action, $demo3Processed);
// 以下はhx-select-oobで抽出する断片
echo '<section id="DEMO3_OOB_POOL">';
// ヘッダ未読バッジを返す
echo multi_update_oob_badge($demo3Unread, false);
// サイドバー集計を返す
echo multi_update_oob_sidebar($demo3Waiting, $demo3Done, false);
// 断片終端を返す
echo '</section>';
デモ
メイン+ヘッダ+サイドバー(複数OOB)
DEMO3 メイン更新
解説
- メインの
#DEMO3_RESULTは通常どおりhx-target+hx-swapで更新します。 hx-select-oobがレスポンス内の#HEADER_UNREAD_BADGE/#SIDEBAR_STATSを抽出して同時反映します。- 1レスポンスに必要断片をまとめることで、メイン・ヘッダ・サイドバーを同時に整合させられます。
次に読むオススメレシピ
参考リンク
このページの著者
経験:Webアプリ/業務システム
得意:PHP・JavaScript・MySQL・CSS
個人実績:フォーム生成基盤/クイズ学習プラットフォーム 等
詳しいプロフィールはこちら! もちもちみかんのプロフィール