TodoMVCのBackbone.js版はどのようにtodoを新規作成して描画しているのか


様々なJavaScriptフレームワークを使って、同じTodoアプリを作る企画[TodoMVC](http://todomvc.com/)をご存知でしょうか？今回は[BackBone.js版](https://github.com/tastejs/todomvc/tree/gh-pages/examples/backbone)のソースコードを読み解いていきます。全てを追うのではなく、タスク名を入力して新規タスクがどのように作られるかを掴んでいきましょう。

### アプリのメインはどこにある？

まず`js/app.js`を見てください。記述やっていることは`app.AppView`のインスタンスの生成です。これがこのアプリのメインとなります。

```js
/*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は画面上はどこを指すかというと次の画像の青い部分です。

![todomvc_backbonejs_01_150911.jpg](https://qiita-image-store.s3.amazonaws.com/0/62881/c821aef7-3bda-1be8-1c92-daa7cecbdd6e.jpeg "todomvc_backbonejs_01_150911.jpg")


これは`app.AppView`の`el`プロパティで指定されています。

```js
app.AppView = Backbone.View.extend({
    el: '.todoapp',
});
```

後はこのViewを追えば流れがわかります。見て行きましょう。

### EnterでTodo作成

下記画像の赤枠のテキストエディタにタスク名を入力後、Enterを押すとtodoが登録されます。

![todomvc_backbonejs_02_150911.jpg](https://qiita-image-store.s3.amazonaws.com/0/62881/dd49bb27-0a29-59b2-1094-7796131419f1.jpeg "todomvc_backbonejs_02_150911.jpg")


この赤枠はもともと`index.html`に書き込まれたもので動的に生成はされていません。この赤枠の`.new-todo`要素でキー入力が行われた場合に実行される処理は`events`プロパティを見ればわかります。

```js
app.AppView = Backbone.View.extend({
    el: '.todoapp',
    events: {
        'keypress .new-todo': 'createOnEnter',
    }
});
```

`crateOnEnter`という関数が実行されると定義されています。イベントハンドラとして`createOnEnter`を渡しているということです。ちなみにEnter以外のキー入力が行われた場合もイベントが発生するので、`createOnEnter`は実行されます。

```js
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()`は文字列の前後から空白やタブ、改行文字を削除します。この時、次のように書けばいいのではと思うかもしれません。

```js
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に保存ということになります。

```js
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が入ります。

ここまでの流れを図にすると次のようになります。

![todomvc_backbonejs_03_150911.jpg](https://qiita-image-store.s3.amazonaws.com/0/62881/b7c124e3-7ac6-ee47-b941-0510c4a1ba44.jpeg "todomvc_backbonejs_03_150911.jpg")

### 新規TodoのHTML要素を組み立てる

`app.todos.create(this.newAttributes());`は`todo`の`add`と`save`を実行しています。`app.todos`で`add`イベントが発生した場合は、`this.addOne`が実行するように定義されています。

```js
app.AppView = Backbone.View.extend({
    initialize: function () {
        this.listenTo(app.todos, 'add', this.addOne);
    }
});
```

`addOne`の中身はシンプルです。`app.TodoView`を作成してレンダリングしたものを、要素`.todo-list`に追加しています。これで新規作成されたtodoがブラウザに表示されます。

```js
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というメソッド名は使うのはどう？と提案しているに過ぎないのです。

全体の流れを図にすると次のようになります。

![todomvc_backbonejs_04_150911.jpg](https://qiita-image-store.s3.amazonaws.com/0/62881/cc7f5c67-0e3a-0c27-86e7-faab5ba5669a.jpeg "todomvc_backbonejs_04_150911.jpg")

### todoのデータはどのコンストラクタに渡すのか？

`app.Todo`というModelを作成したので、`new app.Todo({title: "test"))`のようにModelに渡します。CollectionやViewには渡しません。Collectionである`Todos`には`defaults`も`initialize`もありません。`app.TodoView`はrenderで結びつけたmodelから値を取得してレンダリングを行っています。

```js
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;
    }
});
```