勉強会JS編＜5＞ yeoman + backbone.collection + backbone.localStorage


## 関連記事

* [勉強会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
* [勉強会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 実践 - その２](http://qiita.com/park-jh/items/55d62bc58b94b12835c3)

## Backbone.Collectionとは

* 複数のBackbone.Modelを保持し、管理するコレクション
* extend()を使ってBackbone.Collectionの機能を継承するオブジェクトを作成して使う。

## 実習

### オブジェクトの定義

* yeomanでcontactというcollectionを生成する。

```sh
  [devnote@hooni:~/documents/study/js/sample] % yo backbone:collection contact
     create app/scripts/collections/contact.js
     create test/collections/contact.spec.js
```

* コレクションの定義

```javascript
  // app/scripts/collections/contact.js
  /*global Sample, Backbone*/

  Sample.Collections = Sample.Collections || {};

  (function () {
    'use strict';

    Sample.Collections.Contact = Backbone.Collection.extend({

      // modelプロパティにどのモデルを管理するかを宣言する。
      // この宣言によって、コレクションが保持するモデルはContactのインスタンスとなる。
      model: Sample.Models.Contact,

      // initialize()メソッドを定義できる点はBackbone.Modelと同様
      initialize: function() {
        console.log('ContactCollectionが初期化されました。');
      }

    });

  })();
```

### モデルの追加

* add()

```javascript
  // app/scripts/main.js
  collection_ex01: function(contact) {
    var contactCollection = new Sample.Collections.Contact();
    contactCollection.add(contact);

    var ichiro = new Sample.Models.Contact({
      firstname: '一郎',
      lastname: '鈴木',
      email: 'suzuki@example.com'
    });
    contactCollection.add(ichiro);
    console.log(JSON.stringify(contactCollection, null, 2));
    console.log(contactCollection.length);
  },

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.collection_ex01(c);
  });
```

* 同じモデルを追加する場合

```javascript
  // app/scripts/main.js
  // すでに追加されているモデルが渡された場合は、何も行わない。
  collection_ex02: function(contact) {
    'use strict';
    var contactCollection = new Sample.Collections.Contact();
    contactCollection.add(contact);
    contactCollection.add(contact);
    console.log(JSON.stringify(contactCollection, null, 2));
  },

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.collection_ex02(c);
  });
```

* モデルのインスタンスではなく、オブジェクトを渡す場合

```javascript
  // app/scripts/main.js
  // オブジェクトを渡しても結果は同様 
  collection_ex03: function(contact) {
    'use strict';
    var contactCollection = new Sample.Collections.Contact();
    contactCollection.add(contact);
    contactCollection.add({
      firstname: '一郎',
      lastname: '鈴木',
      email: 'suzuki@example.com'
    });
    console.log(JSON.stringify(contactCollection, null, 2));
  },

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.collection_ex03(c);
  });
```

* モデルを一括追加する場合

```javascript
  // app/scripts/main.js
  // オブジェクトの配列を渡すことで一度に複数のモデルを生成する。
  collection_ex04: function(contact) {
    'use strict';
    var contactCollection = new Sample.Collections.Contact();
    contactCollection.add([
      {
        firstname: '太郎',
        lastname: '山田',
        email: 'yamada@example.com'
      }, {
        firstname: '一郎',
        lastname: '鈴木',
        email: 'suzuki@example.com'
      }
    ]);
    console.log(JSON.stringify(contactCollection, null, 2));
  },

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.collection_ex04(c);
  });
```

* コレクションを初期化する際にモデルを渡す場合

```javascript
  // app/scripts/main.js
  collection_ex05: function(contact) {
    'use strict';
    var ichiro = new Sample.Models.Contact({
      firstname: '一郎',
      lastname: '鈴木',
      email: 'suzuki@example.com'
    });
    var contactCollection = new Sample.Collections.Contact([contact, ichiro]);
    console.log(JSON.stringify(contactCollection, null, 2));

    return contactCollection;
  },

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.collection_ex05(c);
  });
```

### モデルの削除

* remove()

```javascript
  // app/scripts/main.js
  collection_ex06: function(contact, contactCollection) {
    'use strict';
    contactCollection.remove(contact);
    console.log(JSON.stringify(contactCollection, null, 2));
  }, 

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    var cc = Sample.collection_ex05(c);
    Sample.collection_ex06(c, cc);
  });
```

### モデルのリセット

* reset()
  - 保持しているモデルをすべて新しいものに入れ替える。

```javascript
  // app/scripts/main.js
  collection_ex07: function(contact, contactCollection) {
    'use strict';
    var tanaka = new Sample.Models.Contact({
      firstname: '田中',
      lastname: '将大',
      email: 'tanaka@example.com'
    });
    contactCollection.reset([tanaka]);
    console.log(JSON.stringify(contactCollection, null, 2));
  },

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    var cc = Sample.collection_ex05(c);
    Sample.collection_ex07(c, cc);
  });
```

### イベント

* add

```javascript
  // app/scripts/main.js
  collection_ex08: function(contact, contactCollection) {
    'use strict';
    contactCollection.on('add', function(contact) {
      console.log('モデルが追加されました。', contact.get('firstname'));
    });
    var tanaka = new Sample.Models.Contact({
      firstname: '田中',
      lastname: '将大',
      email: 'tanaka@example.com'
    });
    contactCollection.add(tanaka);
    console.log(JSON.stringify(contactCollection, null, 2));
  }, 

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    var cc = Sample.collection_ex05(c);
    Sample.collection_ex08(c, cc);
  });
```

### Underscore.jsの機能

* JavaScriptを拡張するユーティリティライブラリ
* オブジェクトを操作するための便利な機能を提供する。
* 本来は_.each(someArray, function() {...})ように使用するが、コレクションに対して直接呼び出すこともできる。

```javascript
  ContactCollection.each(function(contact) {
    // ...
  })
```

* 使用例：filter()
  - 条件に合致するモデルを配列にいて返す。

```javascript
  // app/scripts/main.js
  collection_ex09: function() {
    'use strict';
    var ichiro = new Sample.Models.Contact({
      firstname: '一郎',
      lastname: '鈴木',
      age: 42,
      email: 'suzuki@example.com'
    });
    var tanaka = new Sample.Models.Contact({
      firstname: '田中',
      lastname: '将大',
      age: 27,
      email: 'tanaka@example.com'
    });
    var aoki = new Sample.Models.Contact({
      firstname: '青木',
      lastname: '宣親',
      age: 34,
      email: 'aoki@example.com'
    });
    var contactCollection = new Sample.Collections.Contact([ichiro, tanaka, aoki]);
    var filtered = contactCollection.filter(function(contact) {
      // Contactモデルがage（年齢）属性を持っていたとして、それ年齢が30以上のモデルだけを抽出した配列を返す。
      return contact.get('age') >= 30;
    });
    console.log(JSON.stringify(filtered, null, 2));
  }

  ...snip...

  $(document).ready(function () {
    'use strict';
    Sample.collection_ex09();
  });
```

* http://backbonejs.org/#Collection-Underscore-Methods

### データの永続化

* データの取得
  - fetch()
      + Backbone.ModelとBackbone.Collectionのインスタンスから使用できる。
      + fetch()は内部で$.ajaxメソッドを使うので、successやerrorオプションにコールバック関数を渡すことができる。
      + 戻り値はjQueryのPromiseオブジェクト
      + fetch()完了後、コレクションはsyncイベントを発生させる。

* データの保存
  - save()
      + Backbone.Modelのインスタンスから使える。
  - create()
      + Backbone.Collectionのインスタンスから使える。
      + モデルの生成、コレクションへの追加、サーバへの送信をまとめて行う。

* データ保存の仕組み
  - モデルがid属性を持っていない場合、コレクションに設定されているurlプロパティに対してPOST形式のHTTPリクエストが行われる。
  - モデルがid属性を持っている場合、コレクションのurlプロパティとモデルのid属性値を連結したパスにPUTリクエストが行われる。

* データの削除
  - destroy()
      + Backbone.Modelのインスタンスから使える。
      + 該当モデルはコレクションやサーバから削除される。
      + 該当モデルと対応するURLに対して実際にDELETEのHTTPリクエストが送信される。
      + 該当モデルを保持していたコレクションがremoveとdestroyイベントを発生させる。

* backbone.localStorage
  - RESTインターフェイスを通じてサーバ側にデータを保持することではなくBackbone.Syncを上書きしデータストアとしてHTML5ローカルストレージを使う。
  - 設置（localStorageではなくbackbone.localStorageである）

```sh
  [devnote@hooni:~/Documents/study/js/sample] % bower install backbone.localStorage --save
  bower backbone.localStorage#*   cached git://github.com/jeromegn/Backbone.localStorage.git#1.1.16
  bower backbone.localStorage#* validate 1.1.16 against git://github.com/jeromegn/Backbone.localStorage.git#*
  bower backbone.localStorage#~1.1.16          install backbone.localStorage#1.1.16

  backbone.localStorage#1.1.16 app/bower_components/backbone.localStorage
  └── backbone#1.1.2
  [devnote@hooni:~/Documents/study/js/sample] % bower list
  bower check-new     Checking for new versions of the project dependencies...
  sample#0.0.0 /Users/devnote/Documents/study/js/sample
  ├─┬ backbone#1.1.2 (latest is 1.2.3)
  │ └── underscore#1.8.3
  ├─┬ backbone.localStorage#1.1.16 extraneous
  │ └── backbone#1.1.2 (1.2.3 available)
  ├─┬ bootstrap-sass-official#3.3.6
  │ └── jquery#2.1.4 (3.0.0-beta1 available)
  ├── chai#3.4.2
  ├── jquery#2.1.4 (latest is 3.0.0-beta1)
  ├── lodash#2.4.2 (latest is 4.0.1)
  └── mocha#2.3.4
```

  - bower.jsonを確認する。dependenciesにbackbone.localStorageが追加されている。

```json
  {
    "name": "sample",
    "version": "0.0.0",
    "dependencies": {
      "bootstrap-sass-official": "~3.3.1",
      "jquery": "~2.1.0",
      "backbone": "~1.1.0",
      "lodash": "~2.4.1",
      "backbone.localStorage": "~1.1.16"
    },
    "devDependencies": {
      "chai": "~3.4.2",
      "mocha": "~2.3.4"
    }
  }
```

  - app/index.htmlにてbackbone.localStorageを読み込む。

```html
  <!-- build:js scripts/vendor.js -->
  <script src="bower_components/jquery/dist/jquery.js"></script>
  <script src="bower_components/lodash/dist/lodash.compat.js"></script>
  <script src="bower_components/backbone/backbone.js"></script>
  <script src="bower_components/backbone.localStorage/backbone.localStorage.js"></script>
  <!-- endbuild -->
```

  - app/scripts/collections/contact.jsにてlocalStorageのオブジェクトを生成する。

```javascript
  // app/scripts/collections/contact.js
  ...snip...

  model: Sample.Models.Contact,

  // localStorage
  localStorage: new Backbone.LocalStorage("Sample.Collections.Contact"),

  ...snip...
```

  - Backbone.Collectionのcreate()

```javascript
  // app/scripts/main.js
  collection_ex10: function() {
    'use strict';
    var ichiro = new Sample.Models.Contact({
      firstname: '一郎',
      lastname: '鈴木',
      age: 42,
      email: 'suzuki@example.com'
    });
    var tanaka = new Sample.Models.Contact({
      firstname: '田中',
      lastname: '将大',
      age: 27,
      email: 'tanaka@example.com'
    });
    var aoki = new Sample.Models.Contact({
      firstname: '青木',
      lastname: '宣親',
      age: 34,
      email: 'aoki@example.com'
    });
    var contactCollection = new Sample.Collections.Contact();
    contactCollection.create(ichiro);
    contactCollection.create(tanaka);
    contactCollection.create(aoki);
  },

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.collection_ex10();
  });
```

  - Local Storageに保持されているデータは、developer toolsにて Resources → Local Storage → http://localhost:9000 順でクリックすることで確認できる。
  - grunt serveが動作されている状態でLocal Storageを確認すると、データが重複で保持されている。
  - Local Storage中のデータを全部削除しページをリロードしたら、予想通りデータが保持されていた。

```
  Sample.Collections.Contact  0c375b69-007a-a992-c8dd-eb9c1c4aa49e,f7aca131-ed31-7d66-0715-eb86d9865c5f,e6aab275-ebbb-9d72-13d6-3545c71bfe41
  Sample.Collections.Contact-0c375b69-007a-a992-c8dd-eb9c1c4aa49e {"firstname":"一郎","lastname":"鈴木","age":42,"email":"suzuki@example.com","id":"0c375b69-007a-a992-c8dd-eb9c1c4aa49e"}
  Sample.Collections.Contact-e6aab275-ebbb-9d72-13d6-3545c71bfe41 {"firstname":"青木","lastname":"宣親","age":34,"email":"aoki@example.com","id":"e6aab275-ebbb-9d72-13d6-3545c71bfe41"}
  Sample.Collections.Contact-f7aca131-ed31-7d66-0715-eb86d9865c5f {"firstname":"田中","lastname":"将大","age":27,"email":"tanaka@example.com","id":"f7aca131-ed31-7d66-0715-eb86d9865c5f"}
```

  - Backbone.Collectionのfetch()

```javascript
  // app/scripts/main.js
  collection_ex11: function() {
    var contactCollection = new Sample.Collections.Contact();
    contactCollection.localStorage = new Backbone.LocalStorage("Sample.Collections.Contact");
    contactCollection.fetch();
    // console.log(contactCollection.pluck('lastname'));
    contactCollection.each(function(contact) {
      console.log(JSON.stringify(contact, null, 2));
    });
  },

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.collection_ex10();
    Sample.collection_ex11();
  });
```

  - developer tools → Consoleにて確認する。

```
  Hello from Backbone!
  contact.js:19 ContactCollectionが初期化されました。
  contact.js:19 ContactCollectionが初期化されました。
  main.js:285 {
    "firstname": "一郎",
    "lastname": "鈴木",
    "age": 42,
    "email": "suzuki@example.com",
    "id": "07e8e44a-55f8-ff5e-860a-9a8d1b763b39"
  }
  main.js:285 {
    "firstname": "田中",
    "lastname": "将大",
    "age": 27,
    "email": "tanaka@example.com",
    "id": "100c3d73-ddd4-59ff-a299-cc800bbb649d"
  }
  main.js:285 {
    "firstname": "青木",
    "lastname": "宣親",
    "age": 34,
    "email": "aoki@example.com",
    "id": "2cb06f4e-33cf-fb91-cc8e-fe390ac47e29"
  }
```

* 参考
  - https://github.com/jeromegn/Backbone.localStorage

## 参考書籍

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