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>

デモ(各デモを動かすと、このデモも更新されます)

ヘッダ未読件数

未読:5
通知
ここにトースト通知が表示されます。

共通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_TOASThx-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レスポンスに必要断片をまとめることで、メイン・ヘッダ・サイドバーを同時に整合させられます。

次に読むオススメレシピ

このページの著者

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

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

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

得意:PHP・JavaScript・MySQL・CSS

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

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

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

もちもちみかん.comとは


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

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