はじめに
こんにちは。キャッチアップでエンジニアをしている隈部です。
だんだんと蒸し暑くなってきましたね。体調管理に気をつけながら過ごしていきたいところです。
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.php と smart_blog_posts.php を描画 |
| ⑥ HTMLを返却 | 記事一覧のHTMLをレスポンスとして返す |
| ⑦ htmxが反映 | #smart-blog-filter-posts を差し替える |
まとめ
シンプルなプラグインですが、htmxを体感するのにちょうどよい題材でした。
htmxを使う一番の恩恵はJavaScriptをほぼ書かずに非同期UIを実現することだと感じました。
他にも使えそうな機能がたくさんある様なのでぜひ試してみてください。
baserCMSやWebサイト構築のご相談など
お気軽にお問い合わせください