htmxで検索機能を作ってみた - HTMLだけでAjaxを実現 -

はじめに

こんにちは。キャッチアップでエンジニアをしている隈部です。
だんだんと蒸し暑くなってきましたね。体調管理に気をつけながら過ごしていきたいところです。

Web開発をしていると、検索機能や絞り込み機能を実装したい場面に遭遇します。近年ではReactやVueなどのフロントエンドフレームワークを利用するケースが増えているようですが、

  • なかなか敷居が高い
  • 検索機能だけのために大規模なフロントエンド環境を導入したくない

  • JavaScriptの実装量を減らしたい

と感じる方も多いのではないでしょうか。

そこで今回はHTML の属性だけで簡単に Ajax 通信を実現できる「htmx」を利用して、シンプルな検索機能を実装する方法をご紹介します。

htmxとは?

htmx は、HTML の属性を利用して非同期通信(Ajax)や画面の一部更新を実現できる JavaScript ライブラリです。

通常、検索機能や絞り込み機能を非同期で実装する場合は、JavaScript を用いてイベントの監視や API 通信、取得したデータの画面反映などを行う必要があります。

例えば、JavaScript の fetch() を利用して通信を行う場合は、以下のようなコードを記述します。


<input type="text" id="keyword">

<div id="result"></div>

<script>
document.getElementById('keyword').addEventListener('input', function () {
    fetch('/search?keyword=' + encodeURIComponent(this.value))
        .then(response => response.text())
        .then(html => {
            document.getElementById('result').innerHTML = html;
        });
});
</script>

一方、htmx を利用すると HTML に属性を追加するだけで同様の処理を実現できます。


<input
    type="text"
    name="keyword"
    hx-get="/search"
    hx-trigger="keyup changed delay:500ms"
    hx-target="#result"
>

<div id="result"></div>

上記では、入力欄に文字が入力されるたびにサーバーへリクエストが送信され、取得した結果が #result の内容として自動的に反映されます。 React や Vue のような大規模なフロントエンド環境を構築することなく、比較的少ない学習コストで導入できるため、既存のサーバーサイドアプリケーションにも組み込みやすいのが特徴です。 検索機能やページネーション、絞り込みなど「画面の一部のみ更新したい」という場面で特に力を発揮します。

htmxを使うメリット

従来のJSと比較した場合、htmxのメリットは以下になります。

観点 htmxを使う場合 従来のJS(fetch / axios)を使う場合
実装量 HTML属性のみでOK JSでイベント処理・API通信・DOM更新を実装
学習コスト 低い(HTML中心) 中〜高(JavaScriptやフレームワークの知識が必要)
サーバー側 HTMLを返すだけ JSONを返し、画面描画はクライアント側で実施
依存関係 htmx.jsのみ axios・React・Vueなどを利用する場合もある
SEO・アクセシビリティ サーバーレンダリングと相性が良い 追加のSEO対策やSSR対応が必要な場合がある

htmxの主要属性一覧

htmxの主要属性は以下となります。

属性 役割
hx-get GETリクエストを送信するURLを指定
hx-post POSTリクエストを送信するURLを指定
hx-target レスポンスHTMLを挿入する対象要素を指定(CSSセレクタ)
hx-swap レスポンスの挿入方法を指定(innerHTML / outerHTML / beforeend など)
hx-trigger リクエストを発火するイベントを指定(click / change / load など)
hx-include リクエストに含める追加のフォームフィールドを指定
hx-push-url ブラウザのURLを更新するかどうかを指定
hx-indicator ローディング表示に使用する要素を指定

実装例

今回はsmartBlogFilterというプラグインとして実装しました。

フィルターUIと記事一覧エリアを Element として分離し、htmxで結びつけています。テンプレートにはフィルター Element と記事一覧 Element を配置し、チェックボックスの変更時に記事一覧部分だけを差し替えます。記事一覧はテーマプラグイン内/templates/plugin/SmartBlogFilter/element/smart_blog_posts.phpに配置することでスタイルやクラス名を調整できる様にしています。

また、htmxの導入についてはCDNを使用せず、htmx.min.jsをプラグイン内に梱包し利用しています。

フォルダ構成は以下の様になります。

 

smart-blog-filter/
├── README.md
├── VERSION.txt
├── config
│   └── routes.php
├── config.php
├── src
│   ├── Controller
│   │   └── SmartBlogFilterController.php
│   ├── Service
│   │   └── BlogFilterService.php
│   └── SmartBlogFilterPlugin.php
├── templates
│   ├── SmartBlogFilter
│   │   └── filter_posts.php
│   └── element
│       ├── smart_blog_filter.php
│       └── smart_blog_posts.php
└── webroot
    ├── css
    │   └── smart_blog_filter.css
    └── js
        └── htmx.min.js
フロントの完成イメージはこの様になります。

/plugins/ThemeSample/templates/Blog/default/index.php内で以下の様に今回作成したプラグインのelementを呼び出すようにしています。

利用側のテンプレートでは基本的に フィルターUI用・記事一覧用のElementに引数を渡すだけで表示できるようになっています。

フィルターUI側では使用するブログ、カテゴリを利用するかどうか、or検索かand検索か、出力先のターゲットIDを引数で指定できるようにしています。

▼ /Plugins/ThemeSample/templates/Blog/default/index.php


<?php
$this->BcBaser->element('SmartBlogFilter.smart_blog_filter', [
'blogContentId' => $blogContent->id,
'categoryMode' => 'or',
'tagMode' => 'or',
'useCategory' => true,
'useTag' => true,
'resultTarget' => '#smart-blog-filter-posts',
]);
?>

<?= $this->element('SmartBlogFilter.smart_blog_posts', [
'posts' => $posts,
'showPagination' => true,
]) ?>

呼び出されるフィルターUIのコードは以下の様になっています。

▼/plugins/SmartBlogFilter/templates/element/smart_blog_filter.php


<form
  class="sbf-filter"
  hx-get="/smart-blog-filter/filter"
  hx-target="#smart-blog-filter-posts"
  hx-swap="outerHTML"
  hx-trigger="change"
>
  <input type="hidden" name="blog_content_id" value="<?= h((string)$blogContentId) ?>">
  <input type="hidden" name="category_mode" value="<?= h($categoryMode) ?>">
  <input type="hidden" name="tag_mode" value="<?= h($tagMode) ?>">
  <input type="hidden" name="show_categories" value="<?= $showCategories ? '1' : '0' ?>">
  <input type="hidden" name="show_tags" value="<?= $showTags ? '1' : '0' ?>">

  <fieldset class="sbf-fieldset">
    <legend>カテゴリ</legend>
    <!-- カテゴリを出力する処理 -->
  </fieldset>

  <fieldset class="sbf-fieldset">
    <legend>タグ</legend>
    <!-- タグを出力する処理 -->
  </fieldset>
</form>

記事一覧部分のelementは /plugins/SmartBlogFilter/templates/element/smart_blog_posts.phpです。

初期表示ページと検索結果表示ページを共通elementとして呼び出しています。

初回アクセス時(絞り込みなしの状態)はコントローラーから取得した $posts をElement に渡して表示し、絞り込み実行時も同じElement を利用して検索結果を描画しています。

HTMLを変更したい場合でもプラグイン側のelementファイルを/plugins/ThemeSample/templates/plugin/SmartBlogFilter/element/smart_blog_posts.phpに配置することで上書きができ、レイアウトの調整も可能となっています。

▼/plugins/SmartBlogFilter/templates/element/smart_blog_post.php


<div class="blog-test" id="smart-blog-filter-posts">
<?php foreach ($posts as $post): ?>
    <div class="post">
        <h3><?php $this->Blog->postTitle($post) ?></h3>
        <div class="meta">
            <?php $this->Blog->category($post) ?>
        </div>
    </div>
<?php endforeach; ?>
</div>

サーバー側では何をしているのか

チェックボックスが変更されると、htmx によって /smart-blog-filter/filter へ GET リクエストが送信されます。

コントローラーでは受け取ったカテゴリIDやタグIDなどの検索条件をサービスクラスへ渡し、条件に一致する記事を取得しています。

取得した記事データは $posts として View に渡され、filter_posts.php が描画されます。

filter_posts.php では記事一覧表示用の Element (smart_blog_posts.php) を呼び出してHTMLを生成します。この Element は初期表示時にも利用しており、表示部分を共通化しています。

生成された記事一覧のHTMLが htmx のレスポンスとして返却され、#smart-blog-filter-posts が新しい内容に置き換えられます。

以下が大まかな処理の流れとなります。

流れ 処理内容
① チェックボックスを変更 htmx が GET リクエストを送信
② コントローラーで受信 検索条件(カテゴリ・タグなど)を取得
③ サービスクラスで検索 条件に一致する記事を取得
④ Viewへデータを渡す 取得した記事を $posts として渡す
⑤ HTMLを生成 filter_posts.phpsmart_blog_posts.php を描画
⑥ HTMLを返却 記事一覧のHTMLをレスポンスとして返す
⑦ htmxが反映 #smart-blog-filter-posts を差し替える

実際に絞り込みを行った画面です。(静止画のためわかりにくいですが、、)

htmxを使用することで簡単に画面遷移なしの絞り込みを実装することができました。

まとめ

シンプルなプラグインですが、htmxを体感するのにちょうどよい題材でした。

htmxを使う一番の恩恵はJavaScriptをほぼ書かずに非同期UIを実現することだと感じました。

他にも使えそうな機能がたくさんある様なのでぜひ試してみてください。

baserCMSやWebサイト構築のご相談など
お気軽にお問い合わせください

お問い合わせ
  • このエントリーをはてなブックマークに追加

AUTHOR

隈部 奨麻

隈部 奨麻 エンジニア

7月11日生まれ。(セブンイレブンと覚えてください)
佐賀県出身福岡県在住

学生時代はバスケに打ち込んだり、バンドのライブに足を運ぶのが好きでした。
一番好きな食べ物はうどんです!

趣味は、言語学習、海外ドラマ鑑賞、音楽鑑賞、ベース演奏、ゲーム、バイク、そしてお酒を楽しむことです。
また、休みの日には、たまにガンプラ作りに没頭しています。ガンプラを作る時間は、自分だけの世界に入り込める貴重なひとときです
「諦めたら試合終了」という安○先生の教えを胸に、日々努力しながら知識を深め、成長を目指しています!

こんな記事も書いています