KnockoutでHandlebarsを使ってみる

これはKnockoutJSアドベントカレンダー22日目の記事です。


KnockoutJS Advent Calendar 2014 - Qiita



Knockoutでは外部のテンプレートエンジンを組み込んで使うことができます。

組み込み方はソースコード内に記載してあります。

knockout/templateEngine.js at master · knockout/knockout · GitHub


要約(ちょっと後半の訳があやしい)

// 独自のテンプレートエンジンを作りたい場合は次のようにしてください。
//
// [1] ko.nativeTemplateEngineと同じようにこのクラスを継承します。
// [2] 'renderTemplateSource' をオーバーライドし、次のシグネチャのメソッドを実装します。
//
//        function (templateSource, bindingContext, options) {
//            // - templateSource.text() はテンプレートでレンダリングするテキストです
//            // - bindingContext.$data はテンプレートに渡すデータです
//            //   - 他にも bindingContext.$parent, bindingContext.$parents, bindingContext.$root がテンプレートで使えます
//            // - options は "data-bind: { template: options }" で与えられたプロパティにアクセスすることができます
//            // - templateDocument はテンプレートの documentオブジェクトです
//            //
//            // Return value: DOM要素の配列
//        }
//
// [3] 'createJavaScriptEvaluatorBlock' をオーバーライドし、 次のシグネチャのメソッドを実装します。
//
//        function (script) {
//            // Return value: Whatever syntax means "Evaluate the JavaScript statement 'script' and output the result"
//            //      'script' を評価して結果を出力します。
//            //     例えば、 jquery.tmpl テンプレートエンジンは 'someScript' を '${someScript} に変換します
//        }
//
//     This is only necessary if you want to allow data-bind attributes to reference arbitrary template variables.
//     これはあなたがデータバインドの属性を任意のテンプレート変数が参照する場合にのみ必要です。
//     もしこれを許可したくない場合は 'allowTemplateRewriting' プロパティを false にします(ko.nativeTemplateEngineと同じように)
//     その場合は 'createJavaScriptEvaluatorBlock' をオーバーライドする必要はありません。


ko.templateSourcesあたりのソースコードに renderTemplateSource の引数の templateSource の詳しい内容が書かれています。


というわけで KnockoutJS から Handlebars を使ってみましょう。

公式ドキュメントの template バインディングの一番下にUnderscoreのサンプルがあるのでこれを真似てみます。

Knockout : The "template" binding


ソースはこんな感じ。

<div data-bind="foreach: people">
  <div data-bind="template: { name: 'person-template' }"></div>
  <hr>
</div>

<script type="text/html" id="person-template">
<table>
<tr>
  <th>Name</th>
  <td>{{name}}</td>
</tr>
<tr>
  <th>Gender</th>
  <td>{{gender}}</td>
</tr>
<tr>
  <th>Profile</th>
  <td>
  {{#each profiles}}
    <span>{{@index}} : {{{this}}}</span>
  {{/each}}
  </td>
</tr>
{{#if country}}
<tr>
  <th>Country</th>
  <td>{{country}}</td>
</tr>
{{/if}}
</table>    
</script>

<script type="text/javascript">
ko.handlebarsTemplateEngine = function () { }
ko.handlebarsTemplateEngine.prototype = ko.utils.extend(new ko.templateEngine(), {
    renderTemplateSource: function (templateSource, bindingContext, options) {
        var precompiled = templateSource['data']('precompiled');
        if (!precompiled) {
            precompiled = Handlebars.compile(templateSource.text());
        }
        var renderedMarkup = precompiled(bindingContext.$data).replace(/\s+/g, " ");            
        return ko.utils.parseHtmlFragment(renderedMarkup);
    },
    allowTemplateRewriting: false,
    createJavaScriptEvaluatorBlock: function(script) {
       return "{{" + script + "}}";
    }
});
ko.setTemplateEngine(new ko.handlebarsTemplateEngine());

function Person(name, gender, profiles, country){
  this.name = name;
  this.gender = gender;
  this.profiles = profiles;
  if(country) {
    this.country = country;
  }
}
function ViewModel() {
  this.people = ko.observableArray([
    new Person("Bob", "Male", ["Hello", "World"]),
    new Person("Mike", "Male", ["Foo"]),
    new Person("Katherine", "Female", []),
    new Person("Ken", "Male", [], "Japan"),
  ]);
}
ko.applyBindings(new ViewModel());
</script>

jsfiddleで見れます。
http://jsfiddle.net/vjj309sz/


明日は @motoyan_k さんの 「Knockout.hxについて」です。
よろしくおねがいします!