htmx 逆引きレシピ
レスポンスから必要な部分だけ採用するには?(準備中)

公開日:
最終更新日:

サーバが「ページ丸ごとHTML」を返しても、クライアント側で必要な部分だけ採用して反映できます。

このページでは、hx-selecthx-select-oob を使って断片抽出する3つの実装パターンを整理します。

使用するhtmx属性

タグ:hx-select / hx-select-oob / hx-target / hx-swap

  • hx-select:targetに入れる断片をレスポンスから選びます。
  • hx-select-oob:OOB更新する断片をレスポンスから選びます。
  • hx-target:選んだ断片を反映する場所を指定します。
  • hx-swap:反映時の差し替え方式(innerHTML/outerHTML)を指定します。

利用シーン

  • 共通レイアウトごと返して、#main だけ差す
  • 同じAPIでも画面ごとに抜き出す

共通PHP(全文)

サーバは共通レイアウト込みのHTMLを返し、必要な断片だけをクライアント側で採用します。
XSS対策は htmlspecialchars を共通関数化して統一しています。

PHP(_select_fragment_data.php)

/htmx/demo/_select_fragment_data.php
<?php
// 型を厳密に扱う
declare(strict_types=1);

// HTML特殊文字を安全化する
function select_fragment_h(string $value): string
{
	// エスケープ済み文字列を返す
	return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}

// 現在時刻を文字列で返す
function select_fragment_now(): string
{
	// 日本語UI向けの時刻形式で返す
	return date('Y-m-d H:i:s');
}

// リクエスト値を安全に取り出す
function select_fragment_request_text(array $source, string $key, string $fallback): string
{
	// 生値を文字列化して取り出す
	$raw = trim((string)($source[$key] ?? ''));

	// 空文字なら既定値を返す
	if ($raw === '') {
		// 既定値を返す
		return $fallback;
	}

	// 入力値を返す
	return $raw;
}

// 共通レイアウト込みHTMLを組み立てる
function select_fragment_layout(string $demoTitle, string $mainHtml, int $badgeCount): string
{
	// タイトルを安全化する
	$titleEsc = select_fragment_h($demoTitle);

	// 件数を0以上に補正する
	$badgeCount = max(0, $badgeCount);

	// 件数を安全化する
	$badgeEsc = select_fragment_h((string)$badgeCount);

	// 時刻を安全化する
	$nowEsc = select_fragment_h(select_fragment_now());

	// ページ丸ごとHTMLを返す
	return '<!doctype html><html lang="ja"><head><meta charset="UTF-8"><title>' . $titleEsc . '</title></head><body><article id="SERVER_LAYOUT" class="CARD"><header class="CARD"><h4>共通ヘッダ</h4><p>通知 <span id="HEADER_BADGE" class="BADGE">' . $badgeEsc . '</span></p></header><main class="CARD">' . $mainHtml . '</main><footer class="CARD"><small>共通フッタ / 生成時刻:' . $nowEsc . '</small></footer></article></body></html>';
}

// DEMO1用のページ丸ごとHTMLを返す
function select_fragment_demo1_page(string $topic): string
{
	// 入力値を安全化する
	$topicEsc = select_fragment_h($topic);

	// 時刻を安全化する
	$nowEsc = select_fragment_h(select_fragment_now());

	// メイン断片を組み立てる
	$mainHtml = '<section id="DEMO1_MAIN" class="CARD"><h4>DEMO1_MAIN</h4><p>共通レイアウトごと返却し、必要なメイン断片だけ採用しました。</p><ul><li>トピック:' . $topicEsc . '</li><li>反映時刻:' . $nowEsc . '</li></ul></section>';

	// レイアウト込みHTMLを返す
	return select_fragment_layout('DEMO1 レイアウト返却', $mainHtml, 3);
}

// DEMO2用のページ丸ごとHTMLを返す
function select_fragment_demo2_page(string $topic): string
{
	// 入力値を安全化する
	$topicEsc = select_fragment_h($topic);

	// 時刻を安全化する
	$nowEsc = select_fragment_h(select_fragment_now());

	// メイン断片を組み立てる
	$mainHtml = '<section id="DEMO2_MAIN" class="CARD"><h4>DEMO2_MAIN</h4><p>同じAPIレスポンスに複数断片を同梱しています。</p><section id="CONTENT_A" class="CARD"><h5>CONTENT_A</h5><p>A案を採用:概要を短く表示します。</p><p class="HTMX-NOTE">対象:' . $topicEsc . ' / 更新:' . $nowEsc . '</p></section><section id="CONTENT_B" class="CARD"><h5>CONTENT_B</h5><p>B案を採用:詳細をカードで表示します。</p><ul><li>対象:' . $topicEsc . '</li><li>更新:' . $nowEsc . '</li><li>備考:同じレスポンスから別断片を抽出</li></ul></section></section>';

	// レイアウト込みHTMLを返す
	return select_fragment_layout('DEMO2 同一レスポンス抽出', $mainHtml, 5);
}

// DEMO3用のページ丸ごとHTMLを返す
function select_fragment_demo3_page(string $action, int $badgeCount): string
{
	// 入力値を安全化する
	$actionEsc = select_fragment_h($action);

	// 時刻を安全化する
	$nowEsc = select_fragment_h(select_fragment_now());

	// メイン断片を組み立てる
	$mainHtml = '<section id="DEMO3_MAIN" class="CARD"><h4>DEMO3_MAIN</h4><p>メイン更新と同時にヘッダ通知数をOOB更新します。</p><ul><li>操作:' . $actionEsc . '</li><li>反映時刻:' . $nowEsc . '</li></ul></section>';

	// レイアウト込みHTMLを返す
	return select_fragment_layout('DEMO3 メイン+ヘッダ更新', $mainHtml, $badgeCount);
}

① 共通レイアウトごと返し、#DEMO1_MAIN だけ採用する(hx-select)

送信先は /htmx/demo/_select_fragment_page.php?demo=1 です。
返却側はヘッダ/メイン/フッタを返し、画面側は hx-select="#DEMO1_MAIN" だけ採用します。

HTML(view全文)

_demo_htmx_1.php
<div class="DEMO">

	<h4>共通レイアウト返却から #DEMO1_MAIN だけ採用</h4>

	<form
	  id="DEMO1_FORM"
	  class="FORM"
	  method="get"
	  hx-get="/htmx/demo/_select_fragment_page.php?demo=1"
	  hx-target="#DEMO1_MAIN"
	  hx-select="#DEMO1_MAIN"
	  hx-swap="outerHTML"
	  hx-indicator="#DEMO1_LOADING"
	>
	  <label>
	    トピック
	    <input type="text" name="topic" value="請求書の最新状態">
	  </label>

	  <button type="submit" class="BTN">#DEMO1_MAIN を差し替える</button>
	  <span id="DEMO1_LOADING" class="LOADING" aria-live="polite">取得中…</span>
	</form>

	<section id="DEMO1_MAIN" class="FORM-RESULT" aria-live="polite">
		まだ更新していません。ここに <code>#DEMO1_MAIN</code> が入ります。
	</section>
</div>

PHP(送信先)

/htmx/demo/_select_fragment_page.php
<?php
// 型を厳密に扱う
declare(strict_types=1);

// HTML断片として返す
header('Content-Type: text/html; charset=UTF-8');

// 共通関数を読み込む
require_once(__DIR__ . '/_select_fragment_data.php');

// demo番号を受け取る
$demo = (int)($_GET['demo'] ?? 1);

// demo番号を1〜3に補正する
$demo = max(1, min(3, $demo));

// DEMO1なら処理する
if ($demo === 1) {
	// topic入力値を受け取る
	$topic = select_fragment_request_text($_GET, 'topic', '共通レイアウトごと返却');

	// DEMO1のページ丸ごとHTMLを返す
	echo select_fragment_demo1_page($topic);

	// 処理を終える
	exit;
}

// DEMO2なら処理する
if ($demo === 2) {
	// topic入力値を受け取る
	$topic = select_fragment_request_text($_GET, 'topic', '同じAPIレスポンス');

	// DEMO2のページ丸ごとHTMLを返す
	echo select_fragment_demo2_page($topic);

	// 処理を終える
	exit;
}

// action入力値を受け取る
$action = select_fragment_request_text($_GET, 'action', '通知数を更新');

// 1〜99の通知件数を作る
$badgeCount = random_int(1, 99);

// DEMO3のページ丸ごとHTMLを返す
echo select_fragment_demo3_page($action, $badgeCount);
/htmx/demo/_select_fragment_1.php
<?php
// 型を厳密に扱う
declare(strict_types=1);

// HTML断片として返す
header('Content-Type: text/html; charset=UTF-8');

// 共通関数を読み込む
require_once(__DIR__ . '/_select_fragment_data.php');

// topic入力値を受け取る
$topic = select_fragment_request_text($_GET, 'topic', '共通レイアウトごと返却');

// DEMO1のページ丸ごとHTMLを返す
echo select_fragment_demo1_page($topic);

デモ

共通レイアウト返却から #DEMO1_MAIN だけ採用

取得中…
まだ更新していません。ここに #DEMO1_MAIN が入ります。

解説

  • サーバは共通レイアウト込みHTMLを返し、クライアントが #DEMO1_MAIN だけ抽出します。
  • レイアウトを共通化したまま、必要箇所だけ更新したいときの基本形です。

② 同じAPI/同じHTMLでも hx-select を変えるだけで採用断片を切り替える

送信先はどちらも /htmx/demo/_select_fragment_page.php?demo=2 です。
一方は #CONTENT_A、もう一方は #CONTENT_B を採用して反映します。

HTML(view全文)

_demo_htmx_2.php
<div class="DEMO">

	<h4>同じAPIでも hx-select を変えて採用断片を切り替える</h4>

	<p class="HTMX-NOTE">
		どちらのボタンも <code>/htmx/demo/_select_fragment_page.php?demo=2</code> に送信します。<br>
		違いは <code>hx-select</code> の指定だけです。
	</p>

	<div class="FLEX ROWGAP16 COLUMNGAP16 FLEX_WRAP">
		<form
		  id="DEMO2_FORM_A"
		  class="FORM"
		  method="get"
		  hx-get="/htmx/demo/_select_fragment_page.php?demo=2"
		  hx-target="#DEMO2_MAIN"
		  hx-select="#CONTENT_A"
		  hx-swap="innerHTML"
		  hx-indicator="#DEMO2_LOADING_A"
		>
		  <input type="hidden" name="topic" value="受注一覧">
		  <button type="submit" class="BTN">CONTENT_A を採用</button>
		  <span id="DEMO2_LOADING_A" class="LOADING" aria-live="polite">取得中…</span>
		</form>

		<form
		  id="DEMO2_FORM_B"
		  class="FORM"
		  method="get"
		  hx-get="/htmx/demo/_select_fragment_page.php?demo=2"
		  hx-target="#DEMO2_MAIN"
		  hx-select="#CONTENT_B"
		  hx-swap="innerHTML"
		  hx-indicator="#DEMO2_LOADING_B"
		>
		  <input type="hidden" name="topic" value="受注一覧">
		  <button type="submit" class="BTN">CONTENT_B を採用</button>
		  <span id="DEMO2_LOADING_B" class="LOADING" aria-live="polite">取得中…</span>
		</form>
	</div>

	<section id="DEMO2_MAIN" class="FORM-RESULT" aria-live="polite">
		まだ更新していません。ここに <code>#CONTENT_A</code> または <code>#CONTENT_B</code> を挿入します。
	</section>
</div>

PHP(送信先)

/htmx/demo/_select_fragment_page.php
<?php
// 型を厳密に扱う
declare(strict_types=1);

// HTML断片として返す
header('Content-Type: text/html; charset=UTF-8');

// 共通関数を読み込む
require_once(__DIR__ . '/_select_fragment_data.php');

// demo番号を受け取る
$demo = (int)($_GET['demo'] ?? 1);

// demo番号を1〜3に補正する
$demo = max(1, min(3, $demo));

// DEMO1なら処理する
if ($demo === 1) {
	// topic入力値を受け取る
	$topic = select_fragment_request_text($_GET, 'topic', '共通レイアウトごと返却');

	// DEMO1のページ丸ごとHTMLを返す
	echo select_fragment_demo1_page($topic);

	// 処理を終える
	exit;
}

// DEMO2なら処理する
if ($demo === 2) {
	// topic入力値を受け取る
	$topic = select_fragment_request_text($_GET, 'topic', '同じAPIレスポンス');

	// DEMO2のページ丸ごとHTMLを返す
	echo select_fragment_demo2_page($topic);

	// 処理を終える
	exit;
}

// action入力値を受け取る
$action = select_fragment_request_text($_GET, 'action', '通知数を更新');

// 1〜99の通知件数を作る
$badgeCount = random_int(1, 99);

// DEMO3のページ丸ごとHTMLを返す
echo select_fragment_demo3_page($action, $badgeCount);
/htmx/demo/_select_fragment_2.php
<?php
// 型を厳密に扱う
declare(strict_types=1);

// HTML断片として返す
header('Content-Type: text/html; charset=UTF-8');

// 共通関数を読み込む
require_once(__DIR__ . '/_select_fragment_data.php');

// topic入力値を受け取る
$topic = select_fragment_request_text($_GET, 'topic', '同じAPIレスポンス');

// DEMO2のページ丸ごとHTMLを返す
echo select_fragment_demo2_page($topic);

デモ

同じAPIでも hx-select を変えて採用断片を切り替える

どちらのボタンも /htmx/demo/_select_fragment_page.php?demo=2 に送信します。
違いは hx-select の指定だけです。

取得中…
取得中…
まだ更新していません。ここに #CONTENT_A または #CONTENT_B を挿入します。

解説

  • 同じAPIレスポンスに複数候補(#CONTENT_A / #CONTENT_B)を入れておけます。
  • 画面ごとに hx-select だけ変えると、採用断片を差し替えられます。

③ メインは hx-select、ヘッダ通知数は hx-select-oob で同時更新

メインは #DEMO3_MAIN を採用し、同時に #HEADER_BADGE をOOBで更新します。
hx-select-oob="#HEADER_BADGE:outerHTML" でヘッダ断片だけ抽出できます。

HTML(view全文)

_demo_htmx_3.php
<div class="DEMO">

	<h4>メインは hx-select、ヘッダ通知数は hx-select-oob で更新</h4>

	<div class="CARD">
		<strong>画面側ヘッダ</strong>
		<span id="HEADER_BADGE" class="BADGE">0</span>
	</div>

	<form
	  id="DEMO3_FORM"
	  class="FORM"
	  method="get"
	  hx-get="/htmx/demo/_select_fragment_page.php?demo=3"
	  hx-target="#DEMO3_MAIN"
	  hx-select="#DEMO3_MAIN"
	  hx-select-oob="#HEADER_BADGE:outerHTML"
	  hx-swap="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">メイン+通知数を同時更新</button>
	  <span id="DEMO3_LOADING" class="LOADING" aria-live="polite">更新中…</span>
	</form>

	<section id="DEMO3_MAIN" class="FORM-RESULT" aria-live="polite">
		まだ更新していません。ここに <code>#DEMO3_MAIN</code> が入ります。
	</section>
</div>

PHP(送信先)

/htmx/demo/_select_fragment_page.php
<?php
// 型を厳密に扱う
declare(strict_types=1);

// HTML断片として返す
header('Content-Type: text/html; charset=UTF-8');

// 共通関数を読み込む
require_once(__DIR__ . '/_select_fragment_data.php');

// demo番号を受け取る
$demo = (int)($_GET['demo'] ?? 1);

// demo番号を1〜3に補正する
$demo = max(1, min(3, $demo));

// DEMO1なら処理する
if ($demo === 1) {
	// topic入力値を受け取る
	$topic = select_fragment_request_text($_GET, 'topic', '共通レイアウトごと返却');

	// DEMO1のページ丸ごとHTMLを返す
	echo select_fragment_demo1_page($topic);

	// 処理を終える
	exit;
}

// DEMO2なら処理する
if ($demo === 2) {
	// topic入力値を受け取る
	$topic = select_fragment_request_text($_GET, 'topic', '同じAPIレスポンス');

	// DEMO2のページ丸ごとHTMLを返す
	echo select_fragment_demo2_page($topic);

	// 処理を終える
	exit;
}

// action入力値を受け取る
$action = select_fragment_request_text($_GET, 'action', '通知数を更新');

// 1〜99の通知件数を作る
$badgeCount = random_int(1, 99);

// DEMO3のページ丸ごとHTMLを返す
echo select_fragment_demo3_page($action, $badgeCount);
/htmx/demo/_select_fragment_3.php
<?php
// 型を厳密に扱う
declare(strict_types=1);

// HTML断片として返す
header('Content-Type: text/html; charset=UTF-8');

// 共通関数を読み込む
require_once(__DIR__ . '/_select_fragment_data.php');

// action入力値を受け取る
$action = select_fragment_request_text($_GET, 'action', '通知数を更新');

// 1〜99の通知件数を作る
$badgeCount = random_int(1, 99);

// DEMO3のページ丸ごとHTMLを返す
echo select_fragment_demo3_page($action, $badgeCount);

デモ

メインは hx-select、ヘッダ通知数は hx-select-oob で更新

画面側ヘッダ 0
更新中…
まだ更新していません。ここに #DEMO3_MAIN が入ります。

解説

  • メインの更新対象IDを #DEMO3_MAIN に固定し、責務を明確化しています。
  • 通知数は #HEADER_BADGE へOOB反映するため、1リクエストで整合が取れます。

次に読むオススメレシピ

このページの著者

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

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

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

得意:PHP・JavaScript・MySQL・CSS

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

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

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

もちもちみかん.comとは


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

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