最近また 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",
visible: function(val, options) {
return !!val;
}
}
}
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 を使って書いてみた。
<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",
".js-name-input": {
observe: ["name", "editing"],
onGet: function(values) {
return values[0];
},
updateModel: false
},
".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"
},
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>
その場編集機能を結構無理やり実装したため、テンプレートがカオスになっている。
もっと良いやり方がありそうだけど思いつかなかった。