しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

knockout.js 2.0.0 がリリースされたので試してみた

1.3.0 のベータかと思ったら 2.0.0 の RC がリリースされていましたが、ついに 2.0.0 の正式版がリリースされたようです。

NuGet でも 2.0.0 が配布されているので、急いで以下のコマンドを Package Manager Console に打ち込みましょう。

Install-Package knockoutjs

まずは作者である Steven Sandarson さんのブログエントリを確認しておきましょう。

ちなみに 1.3.0 から 2.0.0 になった理由もしっかりと説明されていますね。

Why is it called 2.0.0? Why not 1.3.0?
For a long time, we were planning this next version to be called 1.3.0. However,

  • Quite a few community members are keen on adopting SemVer-style versioning. 2.0.0 is a good place to start (expermentally) with that versioning convention.
  • It’s such a big set of core changes that if this doesn’t count as 2.0, I guess nothing ever would…

1.3.0 はコア部分に大きな変更が行われたこと。セマンティックなバージョン管理を求められていたので、それなら 2.0.0 から始めたら綺麗だよね。ということみたいです(自信なし

そしてメインである New Features を確認しておきましょう。全て紹介は難しいので、今回はよく使うであろう機能だけ紹介したいと思います。

  1. Control flow bindings
  2. Containerless control flow
  3. Access to parent binding contexts
  4. Cleaner event handling
  5. Binding providers (and hence external bindings)
  6. Throttling

ここには書かれていないですが、超重要な点としてテンプレートエンジンが内蔵されたことがあげられます。jQuery Templates は残念ながら時代遅れな存在になりつつあります…。

Control flow bindings

新しいバインディングとして foreach, if, ifnot, with という 4 つが追加されています。まずは foreach と if を使ってみます。

<ul data-bind="foreach: items">
    <li><span data-bind="text: name"></span><span data-bind="if: new_flg"> - NEW</span></li>
</ul>

<script type="text/javascript">
var viewModel = {
    items: ko.observableArray([
        { name: "はうはう", new_flg: false },
        { name: "ほげほげ", new_flg: true }
    ])
};

ko.applyBindings(viewModel);
</script>

サンプルはこちら → http://jsfiddle.net/shibayan/Zm7bK/

items の中に入ってる値をリストとして表示するだけの簡単なコードですが、テンプレートを使っているようには見えませんね。data-bind で foreach を指定した場合には子要素がテンプレートとして扱われるので、普通の HTML と同じように書くことが出来、可読性も上がっています。

そして span に if を付けていますが、これは new_flg が true の時にその要素の中身を出力するという機能です。visible と変わらないんじゃ?と思われる方もいると思いますが、出力された DOM が異なってきます。

if の値が false の場合はテキストノードが生成されていないことがお分かり頂けると思います。visible では display: none を指定するだけなので要素の中身は保持されるので、挙動が決定的に異なっています。

注意点としては、data-bind 属性を指定している要素自体は残るので、ボーダーなどのスタイルを当てている場合は表示されてしまいます。要素ごと出力したくない場合には、次で紹介する Containerless control flow を使います。

残った with ですが、これはコンテキストを切り替える機能を持っています。例えば 2 つ以上の小さなビューを持つ場合は、ViewModel の中に別々の ViewModel を持たせたりすると思います。

with を使うと以下のように綺麗に書くことが出来ます。ネタが思いつかなかったので Twitter クライアント風味です。

<div data-bind="with: home">
    <h2 data-bind="text: name"></h2>
    <ul data-bind="foreach: tweets">
        <h3 data-bind="text: name"></h3>
        <p data-bind="text: text"></p>
    </ul>
</div>

<div data-bind="with: mentions">
    <h2 data-bind="text: name"></h2>
    <ul data-bind="foreach: tweets">
        <h3 data-bind="text: name"></h3>
        <p data-bind="text: text"></p>
    </ul>
</div>

<script type="text/javascript">
var viewModel = {
    home: {
        name: "ホーム",
        tweets: ko.observableArray([
            { name: ko.observable("naoki0311"), text: ko.observable("ぎゃるげー") },
            { name: ko.observable("naoki0311"), text: ko.observable("kdwft") }
        ])
    },
    mentions: {
        name: "返信",
        tweets: ko.observableArray([
            { name: ko.observable("normalian"), text: ko.observable("割と普通") },
            { name: ko.observable("harutama"), text: ko.observable("抱き枕") }
        ])
    }
};

ko.applyBindings(viewModel);
</script>

サンプルはこちら → http://jsfiddle.net/shibayan/eegpj/

この例では home と mentions という二つの ViewModel をそれぞれの div 毎に割り当てています。WPF/Silverlight での DataContext へのバインディングと同じですね。

Containerless control flow

名前の通りコンテナが不要なフローです。ここでいうコンテナとは data-bind 属性を持った要素を指しています。

先程のコントロールフローのサンプルでは ul や span 要素に data-bind 属性で foreach や if を指定していましたが、これだと配列が空の場合や値が false の場合でも ul と span 要素は残ってしまっていました。これを回避するために 2.0.0 では HTML コメントを利用した方法が用意されています。

<!-- ko foreach: items -->
    <h2><span data-bind="text: name"></span><!-- ko if: new_flg --> - NEW<!-- /ko --></h2>
<!-- /ko -->

<script type="text/javascript">
var viewModel = {
    items: ko.observableArray([
        { name: "はうはう", new_flg: false },
        { name: "ほげほげ", new_flg: true }
    ])
};

ko.applyBindings(viewModel);
</script>

サンプルはこちら → http://jsfiddle.net/shibayan/Mc4th/

ASPX など一般的なテンプレートエンジンのような書き方ですよね。多くの場合は Containerless control flow の方が書きやすいかもしれません。

注意点としては開始タグと閉じタグが必要ということですね。この対応がちゃんと出来ていないと、もちろんエラーになります。

Access to parent binding contexts

data-bind 属性内で使える特殊な変数として $data / $parent / $parents / $root が追加されました。それぞれ現在のデータ、親のデータ、レベルごとの親、ルートという感じです。

$parents は配列なので添え字でアクセスします。ちなみに $parents[0] は $parent と同じになります。

$root は殆どの場合 ko.applyBindings で指定した ViewModel になりますが、applyBindings は DOM 要素ごとに実行することが出来るので注意です。

<!-- $parent で親のプロパティを参照する -->
<h2><span data-bind="text: $parent.category"></span> - <span data-bind="text: title"></span></h2>

Cleaner event handling

data-bind 属性の中に function() { ... } を直接書くことが出来ますが、これはロジックをビューに埋め込んでしまうことになるので、メンテナンスもやりにくいしそもそも美しくないですね。しかし、レンダリングに使われたデータを取得するためには、関数を埋め込む方法しかありませんでした。

2.0.0 からは ko.dataFor / ko.contextFor メソッドが追加され、DOM 要素からデータとコンテキストを取得することが出来るようになりました。

<ul data-bind="foreach: items">
    <li><span data-bind="text: name"></span> - <a href="#" class="remove-item">削除</a></li>
</ul>

名前: <input type="text" data-bind="value: inputName" /> <input type="button" value="追加" data-bind="click: append" />

<script type="text/javascript">
var viewModel = {
    items: ko.observableArray(),
    inputName: ko.observable(),
    append: function() {
        this.items.push({ name: this.inputName() });

        this.inputName("");
    }
};

$(".remove-item").live("click", function() {
    // 削除対象のデータを取得して
    var data = ko.dataFor(this);

    // 配列から削除!
    viewModel.items.remove(data);
});

ko.applyBindings(viewModel);
</script>

サンプルはこちら → http://jsfiddle.net/shibayan/xuKQw/

jQuery の live メソッドを使って削除リンクがクリックされた時のイベントを登録しています。クリックされた時には ko.dataFor メソッドを使って削除対象のデータを取得し、配列から削除しています。イベントハンドラを外に出すことで可読性が良くなりましたね。

1.2.1 までは更新されない jQuery Templates を使っていたのがちょっとネックでしたが、2.0.0 からは依存関係も減ったので採用しやすくなったと思います。

動的な DOM の生成は本当に手間がかかる割に需要がある部分だと思うので、knockout.js を使って楽しちゃってください。