最近また AngularJS が盛り上がってる気がする。 AngularJS のデータバインディングは魅力的だけど、自分は Backbone 派。 でも Backbone でもデータバインディング使いたい。 そこで New York Times 製の Backbone プラグイン backbone.stickit を試してみた。
基本的な使い方
ビューでバインディングを宣言。 CSS セレクタとモデルの属性名をマッピングする。
var TaskView = Backbone.View.extend({ template: _.template($("#task_template").html()), // バインディングの宣言 bindings: { ".js-task-name": "name" }, render: function() { this.$el.html(this.template({})); // テンプレートを描画したあと呼び出す this.stickit(); return this; } });
レンダリングしたあと stickit() を呼ぶと、バインディングが適用される。
DOM 要素の表示/非表示
モデルの属性が変更されたとき、 バインドしている DOM 要素を表示するかどうか指定できる。
bindings: { ".js-edit-view": { observe: "editing", // editing が true のとき表示 visible: function(val, options) { return !!val; } } }
One way バインディング
input にモデルの属性をバインドして表示したいけど、 編集されても即時にモデル反映してほしくないとき、 One way バインディングが使える。
bindings: { ".js-name-input": { observe: "name", updateModel: false // モデルを更新しない } }
複数の属性に依存
「firstName と lastName のどちらかが変更されたら表示を更新する」みたいに、 複数の属性とバインドすることもできる。
bindings: { ".js-name-input": { observe: ["name" "editing"], onGet: function(values) { return values[0]; } } }
TODO アプリ
最後に TODO アプリのサンプルを backbone.stickit を使って書いてみた。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>stickit sample</title> </head> <body> <h1>Todo</h1> <div id="main"> </div> <div id="new_task"> <input class="js-name-input" type="text"/> <button class="js-add-button">Add</button> </div> <!--タスク描画用テンプレート--> <script id="task_template" type="text/template"> <div class="js-normal-view"> <input class="js-completed-input", type="checkbox"> <span class="js-task-name js-task-uncompleted"></span> <del class="js-task-completed"><span class="js-task-name"></span></del> </input> <a class="js-edit-task" href="javascript:void(0);">[edit]</a> </div> <!--その場編集用 DOM 要素--> <div class="js-edit-view"> <input class="js-name-input" type="text"/> <a class="js-save-task" href="javascript:void(0);">[save]</a> <a class="js-delete-task" href="javascript:void(0);">[del]</a> <a class="js-cancel-task" href="javascript:void(0);">[cancel]</a> </div> </script> <script type="text/javascript" src="jquery-1.9.1.js"></script> <script type="text/javascript" src="underscore.js"></script> <script type="text/javascript" src="backbone.js"></script> <script type="text/javascript" src="backbone.stickit.js"></script> <script> // タスク var Task = Backbone.Model.extend({ defaults: { name: "", completed: false, editing: false }, beginEdit: function() { this.set("editing", true); }, endEdit: function() { this.set("editing", false); }, destroy: function() { this.trigger("destroy", this); } }); // タスクのコレクション var TaskList = Backbone.Collection.extend({ model: Task, initialize: function() { this.on("add", this.onAdd, this); }, onAdd: function(model) { model.on("destroy", this.onDestroy, this); }, onDestroy: function(model) { model.off("add", this.onAdd); model.off("destroy", this.onDestroy); this.remove(model); } }); // タスクを表示するビュー var TaskView = Backbone.View.extend({ template: _.template($("#task_template").html()), // バインディングの設定 bindings: { // タスク名を表示する ".js-task-name": "name", // タスクの完了・未完了をチェックボックスで切り替える ".js-completed-input": "completed", // その場編集モードを切り替えたとき input の値をリセットする ".js-name-input": { observe: ["name", "editing"], onGet: function(values) { return values[0]; }, updateModel: false }, // 未完了タスク名表示用 DOM 要素の表示を切り替える ".js-task-uncompleted": { observe: "completed", visible: function(val, options) { return !val; } }, // 完了したタスクの打ち消し線の表示を切り替える ".js-task-completed": { observe: "completed", visible: function(val, options) { return !!val; } }, // 通常モード用要素の表示を切り替える ".js-normal-view": { observe: "editing", visible: function(val, options) { return !val; } }, // その場編集モード用要素の表示を切り替える ".js-edit-view": { observe: "editing", visible: function(val, options) { return !!val } } }, events: { "click .js-edit-task": "onEdit", "click .js-save-task": "onSave", "click .js-delete-task": "onDelete", "click .js-cancel-task": "onCancel" }, // input の値を取り出してモデルを更新 onSave: function(e) { var name = this.$el.find(".js-name-input").val(); this.model.set("name", name); this.model.endEdit(); }, onDelete: function(e) { this.model.destroy(); }, onEdit: function(e) { this.model.beginEdit(); }, onCancel: function(e) { this.model.endEdit(); }, initialize: function() { this.model.on("destroy", function(model) { this.remove(); }, this); }, render: function() { var html = this.template({}); this.$el.html(html); this.stickit(); return this; } }); // タスクの一覧を表示するビュー var TaskListView = Backbone.View.extend({ initialize: function() { this.collection.on("add", this.addOne, this); }, addOne: function(task) { var view = new TaskView({ model: task }); view.render(); this.$el.append(view.el); }, render: function() { this.$el.empty(); this.collection.each(this.addOne, this); return this; } }); // 登録フォーム var TaskFormView = Backbone.View.extend({ el: $("#new_task"), bindings: { ".js-name-input": "name" }, events: { "click .js-add-button": "onAdd" }, initialize: function() { this.model = new Task(); }, onAdd: function(e) { var task = this.model.clone(); this.collection.add(task); this.model.set("name", ""); }, render: function() { this.stickit(); return this; } }); // ルーター // エントリポイントでもある var MainRouter = Backbone.Router.extend({ routes: { "": "index" }, initialize: function() { this.taskList = new TaskList(); this.taskList.add({ name: "foo" }); this.taskList.add({ name: "bar", completed: true }); this.taskForm = new TaskFormView({ collection: this.taskList }); this.taskForm.render(); }, index: function() { var view = new TaskListView({ collection: this.taskList }); view.render(); $("#main").html(view.el); } }); $(function() { window.router = new MainRouter(); Backbone.history.start(); }); </script> </body> </html>
その場編集機能を結構無理やり実装したため、テンプレートがカオスになっている。 もっと良いやり方がありそうだけど思いつかなかった。