前フリ

jQueryを使ったテンプレートの実装がいくつかあったが、MicrosoftのASP.NETチームがjQueryチームと協力して、jQuery Templateという素晴らしいプラグインをリリースしていたので、試してみた。

中島聡さんのAjax世代のアーキテクチャはかくあるべき、という議論によると、MVCアーキテクチャのViewはHTML・CSSによって表現されるべきで、今まで仕事でSmartyを使ったり、自分でもDjangoを使ってみた経験から、テンプレートはやっぱりHTMLで表現されるべきだと思う。

この議論はモデルをどう実装すべきか(Model = Business Entity + Business Logic)、が論点な気がするけど、JSONなりRESTなりでコントローラと通信可能なら、確かにPHPなりPythonでテンプレートを書く必要はないわけだし。jQTouchやjQuery mobileを使ってみたり、最近のスマートフォン向けにWebサービスを提供するなら、HTMLテンプレートは合理的かも、と思う。

jQuery Templateについて

${variable}や{{each}}のようなテンプレートタグを使って、HTMLをレンダリングするjQueryプラグイン。まだベータ版ではあるが、2011年1月にリリース予定のjQuery 1.5にはjQuery本体に取り込まれる予定。

実装サンプル

マニュアルによると、テンプレートはscriptタグの中に書いておき、コンテナとなるタグにappendするという使い方を想定しているようだ。まずはマニュアル通りに使ってみる。

読み込むjsファイル。

<script type="text/javascript" src="../res/js/jquery-1.4.4.js"></script>
<script type="text/javascript" src="../res/js/jquery.tmpl.js"></script>

サンプル。

<section>
<h2>data list</h2>
<ul id="list"></ul>
</section>
<script id="template" type="text/x-jquery-tmpl">
<li>${name}</li>
<li>${url}</li>
</script>
<script type="text/javascript">
var list =
{
name: "satooshi",
url: "http://d.hatena.ne.jp/with_no_parachute/"
};
$("#template")
.tmpl( list )
.appendTo( "#list" );
</script>

しかし、テンプレートがscriptタグの中、というのは直観的に理解しづらい。テンプレートはViewであって、Controllerではないから。JSをControllerとして使うなら、Viewであるテンプレートも当然

<section>
<h2>data list</h2>
<ul id="list">
<li>${name}</li>
<li>${url}</li>
</ul>
</section>

のように書きたい。ただし、テンプレートタグが見えるのは良くないので、次のような方針で実装する。

  • 初期状態ではテンプレート部分はdisplay:none;
  • テンプレートにデータをバインドする
  • コンテナとなるタグの子要素をコンパイル済みテンプレートと置き換える
  • display:none;を解除して表示

というわけで、もう一度実装サンプル。

CSS

.template{display:none;}

HTML

<article>
<header>
<h1>jquery template test</h1>
</header>
<section>
<h2>data list</h2>
<ul id="list" class="template">
<li>${name}</li>
<li>${url}</li>
<li>SNS
<ul>
{{each sns}}
<li>${$value}</li>
{{/each}}
</ul>
</li>
<li>E-mail
<ul>
{{each(i,address) email}}
<li>${i+1} ${address.label}: ${address.value}</li>
{{/each}}
</ul>
</li>
</ul>
</section>
<section>
<h2>data table</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>City</th>
<th>twitter</th>
<th>E-mail</th>
</tr>
</thead>
<tbody id="databody" class="template">
<tr>
<td>${name}</td>
<td>${city}</td>
<td>${twitter}</td>
<td>${email}</td>
</tr>
</tbody>
</table>
</section>
</article>

バインドするデータとレンダリング処理。

var list =
{
name: "satooshi",
url: "http://d.hatena.ne.jp/with_no_parachute/",
sns: [
"twitter",
"mixi",
"flickr",
"delicious"
],
email: [
{label:"home", value:"email1@gmail.com"},
{label:"mobile", value:"email2@gmail.com"},
{label:"work", value:"email3@gmail.com"}
]
};
var persons = [
{
name:"satooshi",
city:"Yokohama",
twitter:"satooshi",
email:"email1@gmail.com"
},
{
name:"satooshi_jp",
city:"Kanagawa",
twitter:"satooshi_jp",
email:"email2@gmail.com"
},
{
name:"satooshi_en",
city:"New York",
twitter:"satooshi_en",
email:"email3@gmail.com"
},
{
name:"satooshi_fr",
city:"Paris",
twitter:"satooshi_fr",
email:"email4@gmail.com"
}
];
// render template
$(document).ready(function(){
var bindTo = [
{selector:"#list", data:list},
{selector:"#databody", data:persons}
];
renderAll(bindTo);
});

ここで使っている関数は次のようなもの

/**
* bind jQuery template to selector.
*
* @param {String} selector
* @param {jQuery} template
*/
var bindTemplate = function(selector, template){
$(selector)
.children()
.replaceWith(template)
.end()
.show();
};
/**
* render jQuery template.
*
* @param {String} selector
* @param {Object|Array} data
*/
var render = function(selector, data) {
var $t = $(selector).tmpl(data);
bindTemplate(selector, $t);
};
/**
* render all jQuery template.
*
* @param {Array|Object} bindTo
*/
var renderAll = function(bindTo){
for (i=0; i <=bindTo.length; ++i){
t = bindTo[i];
render(t.selector, t.data);
};
};

Chromeで見た結果はこんな感じになる。

f:id:with\_no\_parachute:20101127200921p:image

まとめ

今さらながら、jQueryって便利。JSONでデータを受け取って表示とか、APIを公開しているWebサービスからデータを取ってくる場合だとか、GAEを使っている場合などなど、すごく簡単に実装できるようになる。SEOって何?みたいなサイトだと、このテンプレートエンジンは確かに強力だ。

{{each}}についてちょっと補足事項

いわゆるforeach文で、コレクションをイテレーションするときに使うもの。引数を渡さない場合、イテレーションされるデータにアクセスするときには${$value}を使う。インデックスには${$index}でアクセスできる。引数を渡すと、これらを別の名前で使うことが可能になる。

上の例でいうと、

{{each(i,address) email}}

の部分。${$index}${$value}がそれぞれ、${i}${address}で使えるようになる。

注意点

上のサンプルを書いていたときに実際にやった失敗例。

tableタグにidを付けて、tableタグ以下をテンプレートとすると、バインドする配列の要素数に応じて、theadやらtbodyやらが出力されてしまう。そのため、テンプレートにバインドするのは、基本的に配列、と考えておいた方が良い。とすると、idを付けるタグもtableではなくtbodyにしなければならないことが分かる。1要素しかない場合だとか、[]じゃなくて{}を渡す場合でも単純なミスが防げる。

問題点

別のサンプルを書いていて、気が付いた問題点。

<section>
<h2>index</h2>
<nav>
<ul id="navigation" class="template">
<li><a href="${path}/${name}" target="_blank">${name}</a></li>
</ul>
</nav>
</section>
<script type="text/javascript">
//<![CDATA[
var index = [
{
path:"./html",
name:"delicious.html"
},
{
path:"./html",
name:"datetime.html"
},
{
path:"./html",
name:"databind.html"
}
];
$(document).ready(function(){
render("#navigation", index);
});
//]]>
</script>

aタグのhrefの値にテンプレート変数を設定すると、firefox3、4では変数の値がうまく展開されず、テンプレート変数がそのまま出力されてしまうことが分かった。Chrome7、IE8、Safari5、Opera10といった、他のブラウザだと平気だった。

環境

  • Windows 7
  • ブラウザ
    • Chrome 7.0.517.44
    • IE8
    • Firefox 3.6.12、4.0b7
    • Safari 5.0.3
    • Opera 10.63

参考

Templates - jQuery API