Knockout.js で DOM 要素をバインドするためのカスタムバインディングを作ってみた

Knockout.js には text や value など、組み込みでバインディングが一通り用意されています。

これで十分かというと、そんなことはない。組み込みバインディングでは実現できない場面は多々あります。

例えば、ビューモデルが持っている DOM 要素を、ビューの DOM 要素に子としてバインドするやつとか。具体的には、先読みした画像をビューに表示したい。

無いなら作ればいいじゃない。それがエンジニア。
f:id:griefworker:20120307212229j:image

Knockout.js ではカスタムバインディングを作れます。しかも結構手軽に。試しに、DOM 要素をバインドするためのカスタムバインディングを作ってみました。

<!DOCTYPE html>
<html>
    <head>
        <title>Custom Binding Sample</title>
    </head>
    <body>
        <h1>Cutom Binding Sample</h1>
        <button data-bind="click: showMeat">ハンバーグ</button>
        <button data-bind="click: showSoup">スープ</button>

        <!--
        dom カスタムバインディングを使って、
        ビューモデルのプロパティを子要素としてバインドする。
        -->
        <div id="main" data-bind="dom: image">
        </div>

        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
        <script type="text/javascript" src="https://github.com/downloads/SteveSanderson/knockout/knockout-2.0.0.js"></script>
        <script type="text/javascript">
            (function() {

            // DOM 要素をバインドするカスタムバインディング
            ko.bindingHandlers.dom = {
                // 初期化。最初に1回だけ呼ばれる。
                init: function(element, valueAccessor, allBindingAccessor, viewModel) {
                    // ko.observable を取得
                    var value = valueAccessor();

                    // ko.observable から値を取り出す
                    var valueUnwrapped = ko.utils.unwrapObservable(value);

                    // 子要素をすべて削除
                    element.innerHTML = "";

                    // DOM 要素を追加
                    element.appendChild(valueUnwrapped);
                },

                // バインドしたプロパティの値が変わったとき呼ばれる
                update: function(element, valueAccessor, allBindingAccessor, viewModel) {
                    // やってる事は init と同じ
                    var value = valueAccessor();
                    var valueUnwrapped = ko.utils.unwrapObservable(value);
                    element.innerHTML = "";
                    element.appendChild(valueUnwrapped);
                }
            };

            var app = {
                imageUrl: ko.observable(),
                showMeat: function() {
                    this.imageUrl("http://img.f.hatena.ne.jp/images/fotolife/g/griefworker/20120303/20120303123605.jpg");
                },
                showSoup: function() {
                    this.imageUrl("http://img.f.hatena.ne.jp/images/fotolife/g/griefworker/20120303/20120303121722.jpg");
                }
            };
            
            // 画像を表示する Image オブジェクトを取得
            app.image = ko.dependentObservable(function() {
                var img = new Image();
                img.src = this.imageUrl();
                return img;
            }, app);

            ko.applyBindings(app);

            })();
        </script>
    </body>
</html>

さっそくブラウザで表示してみます。
f:id:griefworker:20120307210553p:image
最初は画像の URL を設定していないから、当然画像は表示されていません。
[ハンバーグ]ボタンをクリックすると
f:id:griefworker:20120307210552p:image
ハンバーグ画像が表示されます。旨そう。
[スープ]ボタンをクリックすると、
f:id:griefworker:20120307210554p:image
スープ画像に切り替わりました。実験成功。

上記サンプルはエラー処理とかまったく書いていない手抜き実装なので、実際に組み込むときはちゃんと作り込みます。それにしても Knockout.js でバインディングを簡単に作れてしまいましたね。この部分は本家の .NET を超えてるかも。