勉強会JS編＜9＞ yeoman + backbone.js 実践 - その２


## 関連記事

* [勉強会JS編＜1＞ オブジェクト指向言語としてのJavaScriptを理解](http://qiita.com/park-jh/items/1887d87f33474cf7d733)
* [勉強会JS編＜2＞ クライアントサイドMVCフレームワーク](http://qiita.com/park-jh/items/8b0a21e09223e3a8d0bc)
* [勉強会JS編＜3＞ フロントエンド開発環境構築](http://qiita.com/park-jh/items/319cacc28850c9fd4898)
* [勉強会JS編＜4＞ yeoman + backbone.model + grunt](http://qiita.com/park-jh/items/78bc01b93fe6d4167624)
* [勉強会JS編＜5＞ yeoman + backbone.collection + backbone.localStorage](http://qiita.com/park-jh/items/49fefe30cdf57de7b6f0)
* [勉強会JS編＜6＞ yeoman + backbone.view](http://qiita.com/park-jh/items/44fb4faac997c71201a0)
* [勉強会JS編＜7＞ yeoman + backbone.router](http://qiita.com/park-jh/items/8f7d4275279201d5796b)
* [勉強会JS編＜8＞ yeoman + backbone.js 実践 - その１](http://qiita.com/park-jh/items/bad19e1c6b502ef0d0fa) 
* 勉強会JS編＜9＞ yeoman + backbone.js 実践 - その２

## メモの検索機能の追加

### 仕様

* 検索欄にキーワードを入力するとメモ一覧の表示が絞り込まれる。
* \#notes/search/検索キーワードのようなルートを発行してブラウザの履歴やブックマークに対応する。

### Practice.Views.NoteControlの役割

* ユーザの検索操作によって起こるイベントを検出すること
* 検索ワードを取得してイベントを発生さえて他のオブジェクトに通知すること

### 検索欄の追加

* app/scripts/templates/note_control.js

```diff
diff --git a/app/scripts/templates/note_control.ejs b/app/scripts/templates/note_control.ejs
index 1f428c9..04bdf95 100644
--- a/app/scripts/templates/note_control.ejs
+++ b/app/scripts/templates/note_control.ejs
@@ -1,5 +1,17 @@
 <div class="row">
   <div class="col-sm-6">
+    <!-- 検索欄のHTMLの追加 start -->
+    <form class="form-inline js-search-form">
+      <div class="input-group">
+        <input type="text" class="form-control js-search-query" name="q">
+        <div class="input-group-btn">
+          <button class="btn btn-success" type="submit">
+            <i class="glyphicon glyphicon-search"></i>
+          </button>
+        </div>
+      </div>
+    </form>
+    <!-- 検索欄のHTMLの追加 end -->
   </div>
   <div class="col-sm-6 text-right">
     <a href="#new" class="btn btn-primary btn-small js-new">
```

### submitイベントの監視の追加

* app/scripts/views/note_control.js

```diff
diff --git a/app/scripts/views/note_control.js b/app/scripts/views/note_control.js
index 4ea8475..4e4631c 100644
--- a/app/scripts/views/note_control.js
+++ b/app/scripts/views/note_control.js
@@ -9,6 +9,18 @@ Practice.Views = Practice.Views || {};

     template: JST['app/scripts/templates/note_control.ejs'],

+    // フォームのsubmitイベントの監視を追加する。
+    events: {
+      'submit .js-search-form': 'onSubmit'
+    },
+
+    // submitイベントのハンドラを追加する。
+    onSubmit: function(e) {
+      e.preventDefault();
+      var query = this.$('.js-search-query').val();
+      this.trigger('submit:form', query);
+    },
+
     render: function () {
       this.$el.html(this.template);
       return this;
```

### ルーティングの修正

* app/scripts/routes/note.js
* submit:formイベントの監視の追加
* 検索に関するルーティング情報の追加
* 検索ワードが含まれたnoteを絞るための処理を追加
* 検索ワードを含むnoteリストを表示するように変更
* app/scripts/routes/note.js

```diff
diff --git a/app/scripts/routes/note.js b/app/scripts/routes/note.js
index b9143b6..247685c 100644
--- a/app/scripts/routes/note.js
+++ b/app/scripts/routes/note.js
@@ -10,6 +10,7 @@ Practice.Routers = Practice.Routers || {};
       'notes/:id': 'showNoteDetail',
       'new': 'showNewNote',
       'notes/:id/edit': 'showEditNote',
+      'notes/search/:query': 'searchNote',
       '*actions': 'defaultRoute'
     },

@@ -30,24 +31,51 @@ Practice.Routers = Practice.Routers || {};
       Practice.headerContainer.empty();
     },

-    showNoteList: function() {
-      // コレクションを渡してメモ一覧の親ビューを初期化する。
-      var noteListView = new Practice.Views.NoteList({
-        collection: Practice.noteCollection
-      });
-
-      // 表示領域にメモ一覧を表示する。
-      Practice.mainContainer.show(noteListView);
-
-      // メモ一覧操作ビューを表示するメソッドの呼び出しを追加する。
+    showNoteList: function(models) {
+      // 一覧表示用のコレクションを別途初期化する
+      if (!this.filteredCollection) {
+        this.filteredCollection = new Practice.Collections.Note();
+      }
+
+      // NoteListViewのインスタンスが表示中でないときのみこれを初期化して表示する。
+      if (!Practice.mainContainer.has(Practice.Views.NoteList)) {
+        // 初期化の際に一覧表示用のコレクションを渡しておく
+        var noteListView = new Practice.Views.NoteList({
+          collection: this.filteredCollection
+        });
+        Practice.mainContainer.show(noteListView);
+      }
+
+      // 検索されたモデルの配列が引数に渡されていればそちらを、
+      // そうでなければすべてのモデルを持つPractice.noteCollection
+      // インスタンスのモデルの配列を使用する。
+      models = models || Practice.noteCollection.models;
+
+      // 一覧表示用のコレクションのreset()メソッドに採用したほうのモデルの配列を渡す。
+      this.filteredCollection.reset(models);
       this.showNoteControl();
     },

     showNoteControl: function() {
+      var self = this;
       var noteControlView = new Practice.Views.NoteControl();
+
+      // submit:formイベントの監視を追加する。
+      noteControlView.on('submit:form', function(query) {
+        self.searchNote(query);
+        self.navigate('notes/search/' + query);
+      });
+
       Practice.headerContainer.show(noteControlView);
     },

+    searchNote: function(query) {
+      var filtered = Practice.noteCollection.filter(function(note) {
+        return note.get('title').indexOf(query) !== -1;
+      });
+      this.showNoteList(filtered);
+    },
+
     showNewNote: function() {
       var self = this;
       // テンプレートの<%= title %>などの出力を空文字列で空欄にしておくため、
```

* app/scripts/views/container.js

```diff
diff --git a/app/scripts/views/container.js b/app/scripts/views/container.js
index ca3c5d3..2294229 100644
--- a/app/scripts/views/container.js
+++ b/app/scripts/views/container.js
@@ -24,6 +24,10 @@ Practice.Views = Practice.Views || {};
     empty: function() {
       this.destroyView(this.currentView);
       this.currentView = null;
+    },
+
+    has: function(obj) {
+      return this.currentView instanceof obj;
     }

   });
```

* app/scripts/views/note_list.js

```diff
diff --git a/app/scripts/views/note_list.js b/app/scripts/views/note_list.js
index 6caf869..ae59ecd 100644
--- a/app/scripts/views/note_list.js
+++ b/app/scripts/views/note_list.js
@@ -20,7 +20,7 @@ Practice.Views = Practice.Views || {};
     initialize: function (options) {
       // Backbone.Collectionインスタンスを受け取る
       this.collection = options.collection;
-      // this.listenTo(this.model, 'change', this.render);
+      this.listenTo(this.collection, 'reset', this.render);
     },

     render: function () {
```

### showNoteList()とshowNoteList(models)

* メソッドをshowNoteList(models)に変更したのに、defaultRouteでshowNoteList()をそのまま使ってもエラーが発生しなかった。
* 18番ラインにブレイクポイントを設定した後step intoを押したらshowNoteList(models)メソッドの中に入りながら横にmodels=undefinedというメッセージが表示された。
* showNoteList(models)メソッドをshowNoteList()で呼び出すと引数がundefinedなのだ。
* 宣言されていない変数を呼び出すとundefinedだよな。これと同じ。

### 古いインスタンスの破棄

* developer toolsのProfilesにてRecord Heap Allocationsを実行して一覧画面と検索結果画面を繰り返して実行する。「戻る」と「進む」を20回以上繰り返す。
* HTMLTableRowElement項目にてオブジェクトがたくさん残されていることを確認する。
* 一覧画面が呼ばれるとき、コレクションに対してreset()でモデルの配列を更新する。
* その際NoteListが表示を更新して使われなくなった古いDOMがメモリに残されてしまう。
* 古いインスタンスを破棄する処理を追加

```diff
diff --git a/app/scripts/views/note_list.js b/app/scripts/views/note_list.js
index 6caf869..9772bed 100644
--- a/app/scripts/views/note_list.js
+++ b/app/scripts/views/note_list.js
@@ -20,18 +20,28 @@ Practice.Views = Practice.Views || {};
     initialize: function (options) {
       // Backbone.Collectionインスタンスを受け取る
       this.collection = options.collection;
-      // this.listenTo(this.model, 'change', this.render);
+      this.listenTo(this.collection, 'reset', this.render);
     },

     render: function () {
+      // this.$el.html()が呼び出される前に古いビューを破棄しておく。
+      this.removeNoteViews();
+
       this.$el.html(this.template);
-      this.collection.each(function(note) {
+      this.noteViews = this.collection.map(function(note) {
         var noteView = new Practice.Views.Note({
           model: note
         });
         this.$('#noteList').append(noteView.render().$el);
+        return noteView;
       }, this);
       return this;
+    },
+
+    // すべての子ビューを破棄するメソッドを追加する。
+    removeNoteViews: function() {
+      // 保持しているすべてのビューのremove()を呼び出す。
+      _.invoke(this.noteViews, 'remove');
     }

   });
```

## 参考書籍
* JavaScript徹底攻略
* JavaScriptエンジニア養成読本
* 入門Backbone.js