hx-get の利用


hx-get は、リンクやボタンなどの要素から GET リクエストを発行し、返ってきた HTML をページの一部に差し替えるための属性です。ページ全体のリロードを避けつつ、一覧の追加読み込み、タブ切り替え、カレンダーの月送りなどを実装できます。

a-blog cms Ver. 3.2 では標準の 組み込み JavaScript に htmx が含まれているため、hx-get を記述するだけで利用できます。詳細は「事前準備について」を参照してください。

基本構文

<a href="{url}" 
   hx-get="{url}tpl/include/entry/summary.html"
   hx-target="this"
   hx-swap="outerHTML"
   hx-push-url="{url}">リンク</a>

より実践的にイメージしやすいように a-blog cms の変数 {url} を記述して例をあげています。

通常 {url} には、リンク先となるページの URL が入ります。a-blog cms が生成する URL は、カテゴリーやエントリー、カスタムフィールドの条件などを組み合わせた URLコンテキスト と呼ばれる形式になっています。URLコンテキストとは、URLによって表現されるページの文脈情報のことで、この URL 自体が表示する内容の条件を表しています。

そして、その URL の後ろに /tpl/ を付与し、さらに include/entry/summary.html のようなテンプレートパスを指定することで、同じ条件で取得したコンテンツを、指定したレイアウトで部分的に置き換えることができます。

hx-targethx-swap では特別な設定を行っていないため、ここでの説明は省略します。

hx-push-url は、部分更新を行った際にブラウザのアドレスバーや履歴(History API)を更新するための属性です。公式ドキュメントでは hx-push-url="true" のように指定する例が紹介されていますが、この場合は hx-get に指定した URL がそのまま履歴に記録され、例えば https://example.com/tpl/entry/summary.html のように tpl パス付きの URL になってしまいます。

a-blog cms では、hx-push-url実際のページ URL(a タグのリンク先) を指定することで、htmx が使われなかった場合の通常遷移と同じ URL を履歴に残すことができます

部分更新後も履歴や URL 表示がユーザーにとって自然な形となり、戻る・進むの操作やブックマーク時の挙動も通常遷移と同様になります。


hx-get 実装例


siteテーマ「お知らせ」の一覧(もっと見る)ボタン

お知らせ一覧ページ にある、最も一般的な「もっと見る」ボタンの実装例です。

通常の Entry_Summary モジュールではページャー部分に /include/parts/pager.html をインクルードしますが、「お知らせ」の部分だけは以下の pager-read-more.html を読み込むようにしています。

/include/parts/pager-read-more.html

<!-- BEGIN pager:veil -->
<!-- BEGIN forwardLink -->
<div id="search-results">
  <div class="read-more-pager">
    <a href="{url}" class="button is-width-lg"
    hx-get="{url}tpl/include/htmx/entry/summary-more.html"
    hx-target="#search-results"
    hx-swap="outerHTML">もっと見る</a>
  </div>
</div>
<!-- END forwardLink -->
<!-- END pager:veil -->

この例では、{url} にページャーの次ページ URL が入り、その後ろに /tpl/include/htmx/entry/summary-more.html を付けて、次ページの一覧部分だけを返すテンプレートを指定しています。

hx-target="#search-results" としているため、このラッパー要素全体を hx-swap="outerHTML" で置き換えます。次ページにも同じ構造の「もっと見る」ボタンが含まれるので、連続クリックでページを進められます。


「よくある質問」のアコーディオン内の A コンテンツ読み込み

これまでは post include を利用し、質問(Q)部分に <form> タグを設置して、回答(A)を非同期で読み込んでいました。

a-blog cms Ver. 3.2 から標準搭載された htmx により、hx-get が利用できるようになったため、<a> タグでよりシンプルに読み込みが可能になりました。

変更前(post include 利用)

<form action="{url}" method="post" class="js-post_include" target="#jsIncludeTo{eid}">
  <h3 class="faq-question">
    <button type="submit" name="ACMS_POST_2GET_Ajax" class="toggle-button faq-question-button js-toggle-button" aria-expanded="false" aria-controls="answer{eid}">
      <span class="faq-icon">Q</span>
      <span class="faq-title">{title}</span>
      <span class="toggle-icon" aria-hidden="false">
        <img src="/images/arrow-down-gray.svg" width="18" height="18" alt="">
      </span>
    </button>
  </h3>
  <input type="hidden" name="tpl" value="faq/post-entry.html" />
</form>
<div id="answer{eid}" class="toggle-body js-toggle-body" aria-hidden="true">
  <div class="faq-answer">
    <p class="faq-icon"><a href="{url}">A</a></p>
    <div class="faq-body">
      <div id="jsIncludeTo{eid}"></div>
    </div>
  </div>
</div>

変更後(htmx 利用)

<a href="{url}" class="toggle-button faq-question-button js-toggle-button"
   hx-trigger="click" hx-get="{url}/tpl/include/htmx/entry/body-faq.html" 
   hx-target="#jsIncludeTo{eid}" 
   hx-swap="innerHTML">
  <span class="faq-icon">Q</span>
  <span class="faq-title">{title}</span>
</a>
<div id="answer{eid}" class="toggle-body js-toggle-body">
  <div class="faq-answer">
    <p class="faq-icon">A</p>
    <div class="faq-body">
      <div id="jsIncludeTo{eid}"></div>
    </div>
  </div>
</div>

このように hx-get を使うことで、フォーム構造を使わずにリンクだけで部分更新が実現でき、マークアップとコードの両方を簡潔にできます。


「ブログ」のカレンダーの月送り

a-blog cms の標準動作では、月送りリンクをクリックすると URL に /YYYY/MM/URLコンテキスト が付与され、サブカラムのカレンダーだけでなくメインコンテンツまでその月の表示に切り替わってしまいます。サブカラムのカレンダーだけを更新したい場合、この挙動は望ましくありません。

そこで htmx を使い、hx-get でカレンダー部分の断片だけを取得して差し替えることで、メインの表示状態を保持したままカレンダーのみを更新します。この場合には、hx-push-url 属性は利用しないようにしておきます。

blog@site/include/calendar/month.html

<!-- BEGIN_MODULE Calendar_Month id="{{module_id}}" -->
<section class="section-side" id="calendar">

<!-- ナビゲーション部分だけ -->
<a href="{url}" hx-target="#calendar" hx-get="{prevUrl}tpl/include/calendar/month.html" hx-swap="outerHTML">←</a>
<a href="{url}" hx-target="#calendar" hx-get="{nextUrl}tpl/include/calendar/month.html" hx-swap="outerHTML">→</a>

</section>
<!-- END_MODULE Calendar_Month -->

この例では、hx-get/tpl/include/calendar/month.html を指定することで、クリック時にカレンダー断片だけを取得して #calendar に差し替えています。これにより、メインコンテンツを変えずにサブカラムのカレンダーだけを更新 できます。


siteテーマ「キーワード検索」ページでのイクリメンタルサーチ

標準ではオフのため、利用する場合は site/index.html のインクルード先を次のように変更します。
変更前:@include("/include/parts/search-general.html")
変更後:@include("/include/htmx/parts/search-general.html")

この切り替えにより、フッターの キーワード検索フォーム が htmx 対応版に変わり、入力のたびに結果が更新される インクリメンタルサーチ が有効になります。

<input type="search" id="search-{{search-id}}" name="keyword" value="%{KEYWORD}" size="15" class="form-search-input" placeholder="キーワードを入力"
hx-get="/include/htmx/entry/summary-list.html" hx-trigger="keyup changed delay:500ms" hx-target="#summary_list">

フォーム本体では <form>hx-post の設定を行っていますが、インクリメンタルサーチは 入力欄のイベント によって hx-get が発火する実装です。hx-trigger="keyup changed delay:500ms" により、キー入力の確定や値の変更から 500ms 後にリクエストが送られ、#summary_list に差し替えられます。Enter での確定やボタン送信時は、従来どおりフォームの hx-post が動作し、結果表示用テンプレートに基づいた部分更新が実行されます。

hx-post の利用


a-blog cms を長く利用されている方であれば、POST リクエストで部分的に HTML を取得・表示する post include 機能を使った経験があるかもしれません。hx-post は、この post include の発想を引き継ぎつつ、より汎用的で拡張性の高い 上位互換 と考えて差し支えありません。

post include が POST メソッド限定だったのに対し、htmx の hx-post では hx-get をはじめとする他のメソッドとも共通の書き方で扱えるため、これまで実現できなかった部分更新のパターンや UI を、同じシンプルな記述で実装できるようになりました。


基本構文

<form action="" method="post"
      hx-post=""
      hx-target="#search-result"
      hx-swap="outerHTML">
  <input type="hidden" name="tpl" value="/include/entry/search-result.html">
  <input type="text" name="keyword" placeholder="キーワード">
  <button type="submit" name="ACMS_POST_2GET_Ajax">検索</button>
</form>

<div id="search-result">
  <!-- 送信結果がここに表示される -->
</div>

この例では、もともと a-blog cms で動作している検索機能に htmx を組み合わせ、ページ遷移なしで検索結果を表示します。

変更点は次のとおりです。

  1. hx-post / hx-target / hx-swap を付与
    hx-post="" で現在のページに POST 送信し、返ってきた HTML を #search-result の要素ごと(outerHTML)置き換えます。

  2. name="tpl" の hidden フィールドを追加
    取得した HTML を描画するテンプレート(例:/include/entry/search-result.html)を指定します。

  3. 送信ボタンの name を変更
    通常の検索フォームでは name="ACMS_POST_2GET" となっていますが、これを name="ACMS_POST_2GET_Ajax" に変更します。これにより、指定したテンプレートを使った部分的な HTML を返すモードになります。

この方法を使えば、既存の検索フォームを大きく書き換えることなく、スムーズな Ajax 型の検索に置き換えられます。


hx-push-url と ACMS_POST_2GET_Ajax の拡張設定

hx-posthx-push-url="true" を指定すると、フォーム送信後の履歴には hx-post に指定した URL がそのまま記録されます。そのため、例えば次のような URL になることがあります。

https://example.com/keyword/検索文字列/tpl/include/entry/search-result.html

この URL をブラウザで直接リロードすると、tpl パラメータ以降を解釈できず、404 Not Found となってしまいます。この状態では、履歴やブックマークからの再アクセスが正常に動作しないため、hx-push-url のメリットが活かせません。

この問題を解消するために、a-blog cms では ACMS_POST_2GET_Ajax に URL を組み立て直す機能が追加されました。この機能を有効にすると、履歴には tpl 以降を除いた URL が保存され、リロードやブックマーク時にも正しくページが表示されるようになります。

利用方法

<div data-acms-hx-push-url="true">
  <!-- 検索結果の内容 -->
</div>

この属性を持つ要素がテンプレート内に含まれている場合、ACMS_POST_2GET_Ajax が履歴用の URL を組み立て直し、tpl 以降を除外して履歴に反映します。

最終的に履歴へ保存される URL

data-acms-hx-push-url="true" を返却テンプレートに指定し、hx-posthx-push-url="true" を用いた場合、履歴には tpl 以降を除いた 正規化された URL が保存されます。

https://example.com/keyword/検索文字列/

この形であれば、リロードやブックマーク、共有時にも期待通りのページが表示されます。


hx-post 実装例

siteテーマ「事例紹介」検索機能

https://utsuwa3m.ablogcms.org/works/htmx.html#works-search

通常のグローバルナビゲーションからアクセスするページでは、htmx を利用しない通常の検索機能として実装されていますが、htmx.html に実装サンプルが用意されています。

詳しくは hx-swap-oob 実装例 をご覧ください。

複数のエリアの更新


通常、htmx では hx-target に指定した 1 つの要素だけが更新されます。しかし、hx-swap-oob 属性を利用すると、メインの差し替え対象とは別の要素も同時に更新することができます。これにより、1 回のリクエストで複数のエリアを並行して差し替えられるため、メインコンテンツ更新と同時に件数表示、通知領域、タイトル、ナビゲーションなどをまとめて更新することが可能になります。

レスポンス側の HTML に、更新対象と同じ id を持つ要素を用意し、その要素に hx-swap-oob を付与するだけで、メイン更新後にアウト・オブ・バンド(OOB)で差し替え処理が行われます。


基本構文

通常、htmx では hx-target に指定した 1 つの要素だけが更新されます。例えば次のような構造の場合、クリック時に更新されるのは id="summary_result" のみです。

<nav id="topicpath">
 <!-- パンくずリスト -->
</nav>

<main>
 <div id="summary_result">
  <!-- エントリー一覧など -->
 </div>

 <a href="{url}" hx-get="{url}/tpl/htmx/include/entry_summary.html"
            hx-target="#summary_result">クリック</a>
</main>

しかし、hx-swap-oob 属性を使うと、メイン差し替え対象とは別の場所にある要素、ここでは id="topicpath" も同時に更新できます。これにより、一覧部分の差し替えと同時にパンくずリストや件数表示、タイトルなど複数箇所をまとめて更新することが可能になります。

呼び出される entry_summary.html

<div id="summary_result">
 <!-- エントリー一覧など -->
</div>

<!-- 通常は上記まで -->

<nav id="topicpath" hx-swap-oob="true">
 <!-- パンくずリスト -->
</nav>

通常の部分更新では、hx-target="#summary_result" の指定により #summary_result だけが差し替えられます。しかし、レスポンス側に id="topicpath" の要素を追加し、hx-swap-oob="true" を付与することで、ページ内の該当要素も同時に更新されます。

ポイント

id の一致が必須

ページ側とレスポンス側の id が同じである必要があります。ここでは topicpath が一致しているため、OOB 更新が適用されます。

true は outerHTML 相当

hx-swap-oob="true" は、その要素ごと(outerHTML)置き換える動作になります。中身だけを差し替えたい場合は、hx-swap-oob="innerHTML" のように指定します。

複数併用可能

1 回のレスポンス内に、複数の hx-swap-oob 要素を含めて複数エリアを同時更新できます。 hx-swap-oob 属性の無い部分、上記であれば <!-- 通常は上記まで --> も含め hx-target のエリアに更新されます。も


hx-swap-oob 実装例

siteテーマの「事例紹介」の検索

https://utsuwa3m.ablogcms.org/works/htmx.html#works-search

以下は、a-blog cms で事例紹介の一覧(summary-works.html)をメイン更新しながら、同時にパンくずリスト(topicpath.html)や <title> を置き換える例です。

htmx.html ( 通常は index.html )

  <!-- サマリー:事例紹介一覧、検索条件ボックス -->
  @include("/include/htmx/summary-works.html", {"multi_swap": "off"})

  <!-- 条件から検索する -->
  @include("/include/htmx/search-works.html")

summary-works.html をインクルードする際に、変数 {{multi_swap}}off を渡しています。

summary-works.html

<!-- BEGIN_MODULE Entry_Summary id="summary_works" -->
<div id="summary_works" data-acms-hx-push-url="true">
 <!-- エントリー一覧 -->
</div>
<!-- END_MODULE Entry_Summary -->

<!-- BEGIN_IF [{{multi_swap}}/neq/off] -->

 @include("/include/htmx/topicpath.html")

 <!-- BEGIN_MODULE Ogp id="ogp" -->
 <title>{title}</title>
 <!-- END_MODULE Ogp -->

<!-- END_IF -->

通常表示では IF ブロック内は出力されませんが、htmx で呼び出されるときだけ topicpath.html<title> を含めて返します。このように IF ブロックで分岐することで、通常表示用の summary-works.html と htmx 用の summary-works.html を共通化 でき、テンプレートの重複を避けることができます。

<title> タグは hx-swap-oob 属性を付けなくても置き換えられるため、OOB 更新に合わせて記述しておくとタイトルも更新されます。

topicpath.html

<!-- BEGIN_MODULE Topicpath id="topicpath" -->
<nav class="topicpath" aria-label="現在位置" id="topicpath" hx-swap-oob="true">
 <!-- パンクくずリスト -->
</nav>
<!-- END_MODULE Topicpath -->

hx-swap-oob="true" により、#summary_works の更新時にパンくずリストも同時に置き換えられます。

この仕組みで、一覧とナビゲーションの整合 を 1 回のリクエストで保つことができます。