htmx 逆引きレシピ
レスポンスから必要な部分だけ採用するには?(準備中)
サーバが「ページ丸ごとHTML」を返しても、クライアント側で必要な部分だけ採用して反映できます。
このページでは、hx-select と hx-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 で更新
#DEMO3_MAIN が入ります。
解説
- メインの更新対象IDを
#DEMO3_MAINに固定し、責務を明確化しています。 - 通知数は
#HEADER_BADGEへOOB反映するため、1リクエストで整合が取れます。
次に読むオススメレシピ
参考リンク
このページの著者
経験:Webアプリ/業務システム
得意:PHP・JavaScript・MySQL・CSS
個人実績:フォーム生成基盤/クイズ学習プラットフォーム 等
詳しいプロフィールはこちら! もちもちみかんのプロフィール