jQuery Mobile と Knockout.js を組み合わせてみた

先日、Backbone.js と比較しながら Knockout.js を試してみました。

Knockout.js よりも Backbone.js のほうが多機能だけど、コードの書きやすさは Knockout.js の方が上、というのが個人的な評価です。特にデータバインディングとテンプレートはスバラシイ。

たった一度サンプルを作っただけでおしまい、というのは寂しいので、Knockout.js でもう少し遊ぶことにします。今回は Knockout.js と jQuery Mobile を組み合わせることに挑戦。お題は先日同様 ToDo アプリで。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8"/>
        <meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1"/>
        <title>Knockout Mobile</title>
        <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0.1/jquery.mobile-1.0.1.css" />
        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
        <script type="text/javascript" src="http://code.jquery.com/mobile/1.0.1/jquery.mobile-1.0.1.min.js"></script>
        <script type="text/javascript" src="https://github.com/downloads/SteveSanderson/knockout/knockout-2.0.0.js"></script>
    </head>
    <body>
        <!--タスク一覧ページ-->
        <div id="list" data-role="page">
            <div data-role="header">
                <h1>タスクリスト</h1>
                <a href="#" data-bind="event: { click: newTask }" data-icon="plus">追加</a>
                <a href="#" data-bind="event: { click: toggleShowAll }"><!--
                    ko ifnot:isShowAll-->すべて<!--
                    /ko--><!--
                    ko if:isShowAll-->未完了<!--
                    /ko--></a>
            </div>
            <div data-role="content">
                <ul id="tasklist" data-role="listview" data-bind="foreach: tasks">
                    <!--ko if:isVisible-->
                    <li><a href="#" data-bind="text: name, event: { click: edit }"></a></li>
                    <!--/ko-->
                </ul>
            </div>
        </div>

        <!--タスク編集ページ-->
        <div id="edit" data-role="page" data-bind="with: currentTask">
            <div data-role="header">
                <h1><!--ko ifnot:isNew-->タスク編集<!--/ko--><!--ko if:isNew-->タスク登録<!--/ko--></h1>
            </div>
            <div data-role="content">
                <div class="input">
                    <label for="name">タスクの内容:</label>
                    <input type="text" data-bind="value: name, valueUpdate: 'keyup'" />
                </div>
                <div class="actions">
                    <!--ko if:isNew-->
                    <!--新規作成のとき表示されるボタン-->
                    <input type="button" value="登録" data-bind="event: { click: save }" />
                    <!--/ko-->

                    <!--ko ifnot:isNew-->
                    <!--編集のとき表示されるボタン群-->
                    <input type="button" value="編集終了" data-bind="event: { click: save }" />
                    <!--ko ifnot:completed-->
                    <input type="button" value="完了" data-bind="event: { click: done }" />
                    <!--/ko-->
                    <!--ko if:completed-->
                    <input type="button" value="完了取消" data-bind="visible: completed, event: { click: undone }" />
                    <!--/ko-->
                    <input type="button" value="削除" data-bind="event: { click: destroy }" />
                    <!--/ko-->
                </div>
            </div>
        </div>

        <script type="text/javascript">
        (function() {

        /**
         * タスクを表します。
         */
        var TaskViewModel = function(taskList) {
            // 登録先タスクリスト
            this.taskList_ = taskList;
            
            // タスク名
            this.name = ko.observable("");

            // 完了かどうか
            this.completed = ko.observable(false);

            // 新規作成中かどうか
            this.isNew = ko.observable(true);

            // 一覧に表示するかどうか
            this.isVisible = ko.dependentObservable(function() {
                // 未完了なら表示。
                // 完了済みで、全表示が有効なら表示。
                if (this.completed()) {
                    return this.taskList_.isShowAll();
                } else {
                    return true;
                }
            }, this);
        };
        /**
         * タスク一覧ページに移動します。
         */
        TaskViewModel.prototype.moveList = function() {
            $("#tasklist").listview("refresh");
            $.mobile.changePage("#list");
        };
        /**
         * タスクを保存します。
         */
        TaskViewModel.prototype.save = function() {
            if (this.isNew()) {
                this.isNew(false);
                this.taskList_.tasks.push(this);
            }
            this.moveList();
        };
        /**
         * タスクを削除します。
         */
        TaskViewModel.prototype.destroy = function() {
            this.taskList_.tasks.remove(this);
            this.moveList();
        };
        /**
         * タスクの編集画面に移動します。
         */
        TaskViewModel.prototype.edit = function() {
            this.taskList_.currentTask(this);

            // jQuery Mobile のテーマを適用し直す。
            // 動的に DOM を書き変えたら、jQuery Mobile のテーマが適用されないので、
            // page メソッドでテーマを適用し直す。
            // 同じ要素に2回以上 page は作用しないので、テーマを解除してから適用し直す。
            $("#edit").removeData("page");
            $("#edit").page();
            $.mobile.changePage("#edit");
        };
        /**
         * タスクを完了します。
         */
        TaskViewModel.prototype.done = function() {
            this.completed(true);
            this.moveList();
        };
        /**
         * タスクの完了を取り消します。
         */
        TaskViewModel.prototype.undone = function() {
            this.completed(false);
            this.moveList();
        };

        /**
         * タスクリストを表します。
         */
        var taskListViewModel = {
            /**
             * 編集中のタスク
             */
            currentTask: ko.observable(),
            /**
             * 登録されたタスク一覧
             */
            tasks: ko.observableArray(),
            /**
             * 完了済みも表示するかどうか
             */
            isShowAll: ko.observable(false),
            /**
             * 新しいタスクを作成します。
             */
            newTask: function() {
                var task = new TaskViewModel(this)
                task.edit();
                return false;
            },
            /**
             * 完了済みを表示するかどうかを切り替えます。
             */
            toggleShowAll: function() {
                this.isShowAll(!this.isShowAll());
                $("#tasklist").listview("refresh");
            }
        };

        // ビューにバインド
        ko.applyBindings(taskListViewModel);

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

コピペで動くはずです。今回もコードはスッキリしています。ビューモデルを変更したら自動でビューに反映されるので、HTML を更新するコードを書かなくていいのが大きい。要素の表示・非表示でさえも自動でやってくれます。データバインディングはクセになりますよ。

ただ、ビューモデルにページ移動のコードを書くしかなかったのが無念です。jQuery Mobile のスタイルを適用し直している部分も同様。でも、これしか実装が思いつきませんでした。力不足。本当はビューモデルで DOM を操作したくなかったんだ。

Backbone.js だとビューのクラスを作成するので、 ページ移動やスタイルの適用はビューに書けるんですけどね。一応、Knockut.js では data-bind の中に複雑な JavaScript を書けるみたいですけど、それをやってしまったら HTML がカオスになってしまいます。ダメ、ゼッタイ。まぁ、今回は私の力が足りなかっただけなんでしょうけど。