Knockoutの template バインディング

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


KnockoutJS Advent Calendar 2014 - Qiita




KnockoutJSには template バインディングなるものがあります。

これは何かというと HTMLの一部を切り出してテンプレート化して使える機能です。

利用用途としては「Componentsにするほどでもないけどこの画面でよく使うので切り出しておきたい」と

いった場合などでしょうか。

それではさっそく公式サイトのドキュメントからかいつまんで説明していきたいと思います。

http://knockoutjs.com/documentation/template-binding.html


公式ドキュメントにあるように、 template バインディングの書き方は以下のようになります。

<h2>Participants</h2>
Here are the participants:
<div data-bind="template: { name: 'person-template', data: buyer }"></div>
<div data-bind="template: { name: 'person-template', data: seller }"></div>
 
<script type="text/html" id="person-template">
    <h3 data-bind="text: name"></h3>
    <p>Credits: <span data-bind="text: credits"></span></p>
</script>
 
<script type="text/javascript">
function MyViewModel() {
    this.buyer = { name: 'Franklin', credits: 250 };
     this.seller = { name: 'Mario', credits: 5800 };
}
ko.applyBindings(new MyViewModel());
</script>

テンプレート化したい部分を で切り出して ID を振ります。
そしてその振ったIDを templateバインディングの name プロパティに指定してバインドさせます。

data プロパティにはテンプレートにレンダリングしたいオブジェクトを設定します。
このオブジェクトのプロパティがテンプレート内の変数として利用できます。

ドキュメントには data に値を設定しない場合は foreach のパラメータか現在のモデルオブジェクトを使用するとあります。

早速試してみましょう。dataを省いた以下のようなコードでもちゃんと動作しました。

http://jsfiddle.net/2jcuzg4r/

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

<script type="text/html" id="person-template">
    <h3 data-bind="text: name"></h3>
    <p><span data-bind="text: gender"></span></p>
</script>
 
<script type="text/javascript">
function Person(name, gender){
  this.name = name;
  this.gender = gender;
}
function ViewModel() {
  this.people = ko.observableArray([
    new Person("Bob", "Male"),
    new Person("Mike", "Male"),
    new Person("Katherine", "Female")
  ]);
}
ko.applyBindings(new ViewModel());
</script>

紹介した name, data プロパティ以外にもtemplateバインディングには
以下のようなプロパティが使えます。

if, foreach, as, afterRender, afterAdd, beforeRemove


if はプロパティにの値が true だったら表示、そうじゃなかったら非表示っていう感じです。

簡単なサンプル作ってみました。

http://jsfiddle.net/tan_go238/uyhtd1k7/

<h2>Participants</h2>
Here are the participants:<br>
<input type="checkbox" data-bind="checked: show1"> <br>
<input type="checkbox" data-bind="checked: show2"> <br>
<div data-bind="template: { name: 'person-template', data: buyer, if: show1 }"></div>
<div data-bind="template: { name: 'person-template', data: seller, if: show2 }"></div>
 
<script type="text/html" id="person-template">
    <h3 data-bind="text: name"></h3>
    <p>Credits: <span data-bind="text: credits"></span></p>
</script>

<script type="text/javascript">
function MyViewModel() {
    this.show1 = ko.observable();
    this.show2 = ko.observable();
    this.buyer = { name: 'Franklin', credits: 250 };
    this.seller = { name: 'Mario', credits: 5800 };
}
ko.applyBindings(new MyViewModel());
</script>


foreach はオブジェクトの配列をぐるぐる回す感じで使います。

テンプレート内の変数になるのは(配列の中の)それぞれオブジェクトのプロパティです。

また "foreach: someExpression" は "template: { foreach: someExpression }" と同じになります。

使い方は公式サイトにサンプルがあるのでそれを参考にしてください。


foreachの中でこのようにテンプレートをネストすることもできます。

http://jsfiddle.net/tan_go238/1gwh5a45/1/

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

<script type="text/html" id="parent-template">
    <h3 data-bind="text: name"></h3>
    <div data-bind="template: { name: 'child-template', foreach: address }"></div>
</script>
<script type="text/html" id="child-template">
    <p><span data-bind="text: region"></span>, <span data-bind="text: country"></span></p>
</script>

<script type="text/javascript">
function Address(region, country) {
  this.region = region;
  this.country = country;
}
function Person(name, address){
  this.name = name;
  this.address = address;
}
function ViewModel() {
  this.people = ko.observableArray([
    new Person("Bob", new Address("Kyoto", "Japan")),
    new Person("Mike", new Address("Tokyo", "Japan")),
    new Person("Katherine", new Address("New York", "America"))
  ]);
}
ko.applyBindings(new ViewModel());
</script>

as は foreach バインディング内で使う $parent のバインディングコンテキストのかっこいい書き方です。

公式サイトのサンプルはこんな感じです。

http://jsfiddle.net/tan_go238/70pd11kp/

<ul data-bind="template: { name: 'seasonTemplate', foreach: seasons, as: 'season' }"></ul>
 
<script type="text/html" id="seasonTemplate">
    <li>
        <strong data-bind="text: name"></strong>
        <ul data-bind="template: { name: 'monthTemplate', foreach: months, as: 'month' }"></ul>
    </li>
</script>
 
<script type="text/html" id="monthTemplate">
    <li>
        <span data-bind="text: month"></span>
        is in
        <span data-bind="text: season.name"></span>
    </li>
</script>
 
<script>
    var viewModel = {
        seasons: ko.observableArray([
            { name: 'Spring', months: [ 'March', 'April', 'May' ] },
            { name: 'Summer', months: [ 'June', 'July', 'August' ] },
            { name: 'Autumn', months: [ 'September', 'October', 'November' ] },
            { name: 'Winter', months: [ 'December', 'January', 'February' ] }
        ])
    };
    ko.applyBindings(viewModel);
</script>

ネストしたテンプレート内で親の変数が取得できます。

ではもうちょっと頑張って上のサンプルをもうひとつネストして子の子の子のテンプレートから同じように取得できるか試してみましょう。

http://jsfiddle.net/tan_go238/70pd11kp/1/

<ul data-bind="template: { name: 'seasonTemplate', foreach: seasons, as: 'season' }"></ul>
 
<script type="text/html" id="seasonTemplate">
    <li>
        <strong data-bind="text: name"></strong>
        <ul data-bind="template: { name: 'monthTemplate', foreach: months, as: 'month' }"></ul>
    </li>
</script>
 
<script type="text/html" id="monthTemplate">
    <li>
        <span data-bind="template: { name: 'detailTemplate', data: month }"></span>
    </li>
</script>

<script type="text/html" id="detailTemplate">
    <li>
        <span data-bind="text: name"></span>
        is in
        <span data-bind="text: season.name"></span>
    </li>
</script>
<script>
var viewModel = {
    seasons: ko.observableArray([
        { name: 'Spring', months: [ {name:'March'}, {name:'April'}, {name:'May'} ] },
        { name: 'Summer', months: [ {name:'June'}, {name:'July'}, {name:'August'} ] },
        { name: 'Autumn', months: [ {name:'September'}, {name:'October'}, {name:'November'} ] },
        { name: 'Winter', months: [ {name:'December'}, {name:'January'}, {name:'February'} ] }
    ])
};
ko.applyBindings(viewModel);
</script>

動作しました。asを使うといくつネストしようが親のプロパティの値を同じようにして取得できるみたいですね。

長くなってきましたので最後は一気にいきます。

afterRender, afterAdd, beforeRemove

これはテンプレート化したHTMLがレンダリングされたとき(afterRender)や、
foreachで追加されたとき(afterAdd)や削除された前(beforeRemove)に呼び出されるメソッドを指定する感じです。

http://jsfiddle.net/tan_go238/qncja7b8/1/

<h2>Participants</h2>
Here are the participants:<br>
<input type="checkbox" data-bind="checked: show1"> <br>
<input type="checkbox" data-bind="checked: show2"> <br>
<div data-bind="template: { name: 'person-template', data: buyer, if: show1 , afterRender: rendered }"></div>
<div data-bind="template: { name: 'person-template', data: seller, if: show2 }"></div>
 
<script type="text/html" id="person-template">
    <h3 data-bind="text: name"></h3>
    <p>Credits: <span data-bind="text: credits"></span></p>
</script>

<script>
function MyViewModel() {
    this.rendered = function(){ alert("rendered!") };
    this.show1 = ko.observable();
    this.show2 = ko.observable();
    this.buyer = { name: 'Franklin', credits: 250 };
    this.seller = { name: 'Mario', credits: 5800 };
}
ko.applyBindings(new MyViewModel());
</script>


今日は templateバインディングを紹介しました。

templateバインディングはデフォルトで ko.nativeTemplateEngine を使用してレンダリングします。
もし jquery.tmpl を読み込んでいたら jquery.tmpl 用のテンプレートエンジン(ko.jqueryTmplTemplateEngine)を使用します。
※判定方法は ko.jqueryTmplTemplateEngine の jQueryTmplVersion あたりを参考にしてください。




明日は @isoden_ さんの「案件で使ってみてあーだったこーだった的な話です」です!
よろしくおねがいします!