Knockout.js 3.2の新機能Componentsを一足先に使う

## 概要

現在、開発が進められているKnocuout.js 3.2では、新しい機能としてComponentsが提供されます。
Componentsでは、テンプレートとビューモデルをまとめて独自タグに関連付けて、再利用可能なUIコンポーネントを実現します。[Web Components](http://webcomponents.org/)のCustom ElementsにKnockoutの機能が融合されたものと思ってもよいかもしれません。

## インストール

執筆時点(2014.07.28現在)では、3.2.0 Betaがリリースされているので、Bowerでバージョン指定してインストールします。

```shell-session
$ bower install "knockout#3.2.0-beta"
```

## 使い方

コンポーネントの登録には、ko.components.register()を使います。
必要な要素として templateとviewModelを、それぞれ以下のように設定します。ここでは、x-countdown-days というタグ名で、指定した日付までの残り日数を表示するコンポーネントを登録しました。

```js
ko.components.register('x-countdown-days', {
    template: '<span data-bind="text:days"></span>',
    viewModel: function(params) {
        self.days = parseInt((new Date(params.target).getTime() - Date.now()) / 86400000);
    }
});
ko.applyBindings();
```

これを、HTML側で読み込みます。Componentsに値を渡すにはparams属性を使用します。

```html
劇場版アイカツ！まで、あと <x-countdown-days params="target:'2014/12/13'"></x-countdown-days> 日
```

これで、劇場版アイカツ！までの日数を表示することができました（[デモ](http://jsfiddle.net/castor_4bit/FL3Nn/)）。
公開が楽しみですね。

さて、このままではKnockout.jsのメリットがあまり活かされていないので、data bindingを利用してリアルタイムにカウントダウンされるよう作り変えてみます。JavaScriptとHTMLをそれぞれ以下のように書き換えました。

```js
ko.components.register('x-countdown-days', {
    template: '<span>'
        + '<span data-bind="text:days"></span> 日 '
        + '<span data-bind="text:hours"></span> 時間 '
        + '<span data-bind="text:minutes"></span> 分 '
        + '<span data-bind="text:seconds"></span> 秒'
        + '</span>',
    viewModel: function(params) {
        var self = this, values;
        var target = new Date(params.target).getTime();
        
        var updateValues = function() {
            var diff = (target - Date.now()) / 1000;
            
            self.days( parseInt(diff / 86400) );
            self.hours( parseInt((diff % 86400) / 3600) );
            self.minutes( parseInt((diff % 3600) / 60) );
            self.seconds( parseInt(diff % 60) );
        }

        self.days = ko.observable();
        self.hours = ko.observable();
        self.minutes = ko.observable();
        self.seconds = ko.observable();
        
        updateValues();
        setInterval(updateValues, 1000);
    }
});
ko.applyBindings();
```

```html
劇場版アイカツ！まで、あと <x-countdown-days params="target:'2014/12/13'"></x-countdown-days>
```

[デモはこちらです](http://jsfiddle.net/castor_4bit/FL3Nn/3/)。
実際にコンポーネント化するのであれば、不正な入力や期間超過後の処理、出力フォーマットの変更などにも気を使いたいですが、今回はここまでにします。

## 応用

テンプレートやモデルを別ファイルに切り出したい場合、RequireJSなどを使って下記のように記述することができます。独自の custom component loader を登録することもできますが、どのみちproductionでは分離したファイルを結合することになるので、既存のAMDライブラリに任せてしまったほうが良い気もします。

```js
ko.components.register('x-countdown-days', {
    viewModel: { require: 'src/js/x-countdown-days' },
    template: { require: 'text!src/template/x-countdown-days.html' }
});
```

テンプレートの指定には、DOMのID指定やElement自体を渡すこともできる予定のようですが、試した限りではうまく動作していなかったように思われました。

## Web Componentes/Polymerとの相違

Knockout.js 3.2のComponentsでは、主にWeb ComponentsのCustom Elementsに相当する機能のみが提供されます。lyfecycle callbackは今のところ用意されていませんが、作者の意向としては提供予定とのことです。また、Shadow DOMがサポートされないことから、既存cssなどとの相互影響は今後も気を使いながら実装することになるでしょう。

data bindingの機能についても、Web Componentsが当面Polymerとともに使われるのであれば、[Polymerの提供するdata binding](http://www.polymer-project.org/docs/polymer/databinding.html)(MDVって言わなくなった?)がその役割を担ってくれるため、単純な機能レベルでのメリットは徐々に薄れていく可能性があります。

ただし、IE10以降のみをサポートするPolymerに対して、Knockout.jsでは本体同様にIE6以降でComponentsの機能を使うことができます<sub><a href="#1" name="#1-ret">1</a></sub>。また、プラグインでの拡張やcustom bindingといった機能は引き続き有用に使えるため、内部的にはWeb Componentsとうまく共存しつつ、軽量ライブラリとしての役割は今後も活かされるのではないかと思っています<sub><a href="#2" name="#2-ret">2</a></sub>。

最後に、Polymerで最初のサンプルを実現した場合の例を貼っておきます。

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <script src="platform.js"></script>
    <script src="polymer.js"></script>
    <link rel="import" href="x-countdown-days.html">
  </head>
  <body>
    劇場版アイカツ！まであと  <x-countdown-days target="2014/12/13"></x-countdown-days> 日
  </body>
</html>
```

```html:x-countdown-days.html
<polymer-element name="x-countdown-days" noscript>
  <template>
    <span>{{days}}</span>
  </template>
  <script>
    Polymer('x-countdown-days', {
      ready: function() {
        var target = this.getAttribute('target');
        this.days = parseInt((new Date(target).getTime() - Date.now()) / 86400000);
      }
    });
  </script>
</polymer-element>
```


## 参考
- [Knockout.js 3.2 Preview : Components - Knock Me Out](http://www.knockmeout.net/2014/06/knockout-3-2-preview-components.html)
- [Architecting large Single Page Applications with Knockout.js   - Steve Sanderson’s blog - As seen on YouTube™](http://blog.stevensanderson.com/2014/06/11/architecting-large-single-page-applications-with-knockout-js/)

---------

1. IE6〜8ではHTMLパーサがカスタムタグを解釈する前にregister()されている必要が有るため、初期化のタイミングに少し工夫が必要です. IE9以降では特に対策不要です<a href="#1-ret" name="1">↩</a>
2. なお、この記事は天下一クライアントサイドJS MV*フレームワーク武道会でKnockout.jsがオワコンというコメントがあったので書かれました<a href="#2-ret" name="2">↩</a>