様々なJavaScriptフレームワークを使って、同じTodoアプリを作る企画TodoMVCをご存知でしょうか?今回はBackBone.js版のソースコードを読み解いていきます。全てを追うのではなく、タスク名を入力して新規タスクがどのように作られるかを掴んでいきましょう。
アプリのメインはどこにある?
まずjs/app.js
を見てください。記述やっていることはapp.AppView
のインスタンスの生成です。これがこのアプリのメインとなります。
/*global $ */
/*jshint unused:false */
var app = app || {};
var ENTER_KEY = 13;
var ESC_KEY = 27;
$(function () {
'use strict';
// kick things off by creating the `App`
new app.AppView();
});
app-view.js
にメインのViewであるapp.AppView
が宣言されています。このViewは画面上はどこを指すかというと次の画像の青い部分です。
これはapp.AppView
のel
プロパティで指定されています。
app.AppView = Backbone.View.extend({
el: '.todoapp',
});
後はこのViewを追えば流れがわかります。見て行きましょう。
EnterでTodo作成
下記画像の赤枠のテキストエディタにタスク名を入力後、Enterを押すとtodoが登録されます。
この赤枠はもともとindex.html
に書き込まれたもので動的に生成はされていません。この赤枠の.new-todo
要素でキー入力が行われた場合に実行される処理はevents
プロパティを見ればわかります。
app.AppView = Backbone.View.extend({
el: '.todoapp',
events: {
'keypress .new-todo': 'createOnEnter',
}
});
crateOnEnter
という関数が実行されると定義されています。イベントハンドラとしてcreateOnEnter
を渡しているということです。ちなみにEnter以外のキー入力が行われた場合もイベントが発生するので、createOnEnter
は実行されます。
app.AppView = Backbone.View.extend({
el: '.todoapp',
events: {
'keypress .new-todo': 'createOnEnter',
},
createOnEnter: function (e) {
if (e.which === ENTER_KEY && this.$input.val().trim()) {
app.todos.create(this.newAttributes());
this.$input.val('');
}
}
});
イベントハンドラcreateOnEnter
では、まず入力されたキーがENTERかを判断します。変数ETNER_KEY
には13という数値が入っています。キー入力は数値で判断されるのです。そしてEnterキーなら次は入力された文字列をトリムします。trim()
は文字列の前後から空白やタブ、改行文字を削除します。この時、次のように書けばいいのではと思うかもしれません。
if (e.which === ENTER_KEY) {
this.$input.val().trim();
app.todos.create(this.newAttributes());
this.$input.val('');
}
なぜif
の条件にtrim
をつなげているかというと、トリムした結果が空文字の場合はif
の中を実行しないためです。テキストフィールドに空白やタブしか入力されなかった時に、空文字だけになります。この場合はタスク名が入力されていないので、todoを作成する必要はないですよね。
if
の中身の2行でtodosコレクションにtodoを追加して、テキストフィールドを空にリセットしています。create
は要素に追加して、save
を実行しています。今回はリソースがlocalStorageが選択されているので、APIにPOSTリクエストを送るのではなく、localStorageに保存ということになります。
app.AppView = Backbone.View.extend({
el: '.todoapp',
events: {
'keypress .new-todo': 'createOnEnter',
},
newAttributes: function () {
return {
title: this.$input.val().trim(),
order: app.todos.nextOrder(),
completed: false
};
},
createOnEnter: function (e) {
if (e.which === ENTER_KEY && this.$input.val().trim()) {
app.todos.create(this.newAttributes());
this.$input.val('');
}
}
});
create
にはハッシュで保存するtodoの値を渡すことが出来ます。新規todoのハッシュはnewAttributes
というメソッドで返すようにしています。title
は入力されたタスク名。completed
は未完了のfalse。orderには既存todoの数 + 1が入ります。
ここまでの流れを図にすると次のようになります。
新規TodoのHTML要素を組み立てる
app.todos.create(this.newAttributes());
はtodo
のadd
とsave
を実行しています。app.todos
でadd
イベントが発生した場合は、this.addOne
が実行するように定義されています。
app.AppView = Backbone.View.extend({
initialize: function () {
this.listenTo(app.todos, 'add', this.addOne);
}
});
addOne
の中身はシンプルです。app.TodoView
を作成してレンダリングしたものを、要素.todo-list
に追加しています。これで新規作成されたtodoがブラウザに表示されます。
app.AppView = Backbone.View.extend({
initialize: function () {
this.$list = $('.todo-list');
this.listenTo(app.todos, 'add', this.addOne);
},
addOne: function (todo) {
var view = new app.TodoView({ model: todo });
this.$list.append(view.render().el);
}
});
new app.TodoView({ model: todo })
は引数でmodel
にtodo
を指定しています。これでViewとModelが結びつきます。app.TodoView
のソースではmodel
プロパティの定義はされていません。これは外部から``new app.TodoView()で生成されるときに先ほどのように、
{ model: myModel }`とハッシュでmodelを差し替えれるようになっています。こうすることで他のモデルを`app.TodoView`で使うことができます。ただし、`app.TodoView`には`this.model.get('completed')`などと、`completed`というプロパティがmodelに設定されていることが期待されています。なので必要な仕様を実装したModelでないと、`app.TodoView`は機能しません。
作成されたViewはthis.$list.append(view.render().el);
のrender()
でHTML要素を組み立てて、el
でその要素を取得しています。render()
はBackbone.jsで用意された何もしないメソッドです。なのでrenderという名前にしなくても構いません。自分でHTMLを組み立てる処理を実装したメソッド用意すればいいのです。Backbone.jsはただrenderというメソッド名は使うのはどう?と提案しているに過ぎないのです。
全体の流れを図にすると次のようになります。
todoのデータはどのコンストラクタに渡すのか?
app.Todo
というModelを作成したので、new app.Todo({title: "test"))
のようにModelに渡します。CollectionやViewには渡しません。CollectionであるTodos
にはdefaults
もinitialize
もありません。app.TodoView
はrenderで結びつけたmodelから値を取得してレンダリングを行っています。
app.TodoView = Backbone.View.extend({
tagName: 'li',
render: function () {
if (this.model.changed.id !== undefined) {
return;
}
this.$el.html(this.template(this.model.toJSON()));
this.$el.toggleClass('completed', this.model.get('completed'));
this.toggleVisible();
this.$input = this.$('.edit');
return this;
}
});