概要
htmxはクライアントサイドテンプレート拡張機能を導入することでいくつかのテンプレート(mustache,handlebars,nunjucks,xslt)をサーバーから取得したjsonやxmlに適用できるのですが、これはテンプレートをhtmx内に直接記述する前提のもので、外部のテンプレートを適用する手段は用意されていません。
これをどうにかして無理やり別ファイルのテンプレートをロードして適用してみようという試みです。
方法
やり方はいたって単純で、hx-getでテンプレートをロードするだけ…なんですがいくつか注意点があります。
多分.html(text/html)しかロードできない
例えばmustacheテンプレートを取り込もうとして.mustacheファイルを直接hx-getしても上手くいかないっぽいです。これはxhrの制限じゃないかな?
templateタグを直接挿入したりtemplateタグ内に挿入したりはできない
<ul>
{{#.}}
<li>{{id}} : {{name}}</li>
{{/.}}
</ul>
という拡張子を.htmlにしたmustacheテンプレートファイルを
<div
hx-get="./template.html"
hx-trigger="load"
hx-swap="innerHTML"
hx-target="#temp"
></div>
<template id="test">
<div id="temp"></div>
</template>
このようにしてtemplateタグ内に挿入しようとしても上手くいきません。また、
<template id="test">
<ul>
{{#.}}
<li>{{id}} : {{name}}</li>
{{/.}}
</ul>
</template>
このようにmustacheテンプレートをtemplateタグで囲んだ.htmlファイルを
<div
hx-get="./template.html"
hx-trigger="load"
hx-swap="innerHTML"
hx-target="#temp"
></div>
<div id="temp"></div>
このようにして挿入しようとしても上手くいきません。(Chrome stable版で確認)
理由は深く追ってませんが、多分template要素はjavascriptでは特殊な扱いになっているんじゃないですかね?
なぜか無関係のdivタグをtemplateタグの前につけたり、中身が何もないdivでtemplateタグを囲んだhtmlにするとうまくいきます…謎です。
テンプレートが反映された後でデータを読み込む必要がある
当たり前ですが、テンプレートがロードされてDOMに反映される前にデータを読み込んでテンプレートを適用しても上手くいきません。ロード直後にテンプレートを適用したい場合には適切なイベント処理が必要になります。
とりあえずafter-settle(DOM確定後に発火するhtmx独自イベント)をトリガーにdocument.dispatcheEventで独自イベントを発火させるのがいちばん簡単なのかな?
<div style="display:none" hx-on:htmx:after-settle="document.dispatchEvent(new Event('testLoaded'))">
テンプレート
</div>
<input
type="search"
name="search"
hx-get="./test.json"
hx-swap="innerHTML"
hx-target="#content"
hx-trigger="testLoaded from:document, keyup changed delay:500ms, search"
/>
<div id="content"></div>
<div
hx-get="./loaded.html"
hx-trigger="load"
hx-swap="outerHTML"
hx-target="#load"
></div>
<div id="load"></div>
完成品
とりあえずこんな感じにして上手くいきました(Chrome stable版で確認)
<div style="display:none" hx-on:htmx:after-settle="document.dispatchEvent(new Event('testLoaded'))">
<template id="test">
<ul>
{{#.}}
<li>{{id}} : {{name}}</li>
{{/.}}
</ul>
</template>
</div>
[
{
"id": 1,
"name": "一番目"
},
{
"id": 2,
"name": "にばんめ"
},
{
"id": 3,
"name": "参番"
},
{
"id": 4,
"name": "number 4"
}
]
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/htmx.org"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/client-side-templates.js"></script>
<script src="https://unpkg.com/mustache@latest"></script>
<title>after load template</title>
</head>
<body>
<div hx-ext="client-side-templates">
<input
type="search"
name="search"
hx-get="./test.json"
hx-swap="innerHTML"
hx-target="#content"
mustache-template="test"
hx-trigger="testLoaded from:document, keyup changed delay:500ms, search"
/>
<div id="content"></div>
<div
hx-get="./template.html"
hx-trigger="load"
hx-swap="outerHTML"
hx-target="#temp"
></div>
<div id="temp"></div>
</div>
</body>
</html>
使い道は…複数のhtmxで共通のテンプレートを使いたい場合くらいですかね…
バックエンドを絡める場合
バックエンドのビュー機能で直接テンプレートファイルを埋め込めばいいじゃんって気がしないでもないですが、htmxでロードさせたい場合はテンプレートファイルをhtmx用に加工して返す感じのスクリプトを書けばいいんじゃないでしょうか。
<?php
$mustachedir = './';
$tempname = '';
if (array_key_exists('tempname', $_GET)) $tempname = $_GET['tempname'];
if ($tempname == '' || !file_exists($mustachedir.$tempname.'.mustache')) {
header('HTTP/1.0 404 Not Found');
echo 'file not found';
exit;
}
header('Content-Type: text/html');
?>
<div style="display:none;" hx-on:htmx:after-settle="document.dispatchEvent(new Event('<?php echo $tempname?>Loaded'))">
<template id="<?php echo $tempname?>">
<?php echo file_get_contents($tempname.'.mustache')?>
</template>
</div>
<ul>
{{#.}}
<li>{{id}} : {{name}}</li>
{{/.}}
</ul>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/htmx.org"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/client-side-templates.js"></script>
<script src="https://unpkg.com/mustache@latest"></script>
<title>after load template</title>
</head>
<body>
<div hx-ext="client-side-templates">
<input
type="search"
name="search"
hx-get="./test.json"
hx-swap="innerHTML"
hx-target="#content"
mustache-template="test"
hx-trigger="testLoaded from:document, keyup changed delay:500ms, search"
/>
<div id="content"></div>
<div
hx-get="./template.php?tempname=test"
hx-trigger="load"
hx-swap="outerHTML"
hx-target="#temp"
></div>
<div id="temp"></div>
</div>
</body>
</html>
がんばれば初回ロード時はバックエンド側でテンプレートを適用したHTMLを出力し、更新時はjsonだけリクエストしてhtmx側で同じテンプレートを適用して出力するといったことができるかもしれません…
参考