yutaponのブログ

javascript界隈の興味あるネタを備忘録的に残しておく場所

Backbone.jsに入門してみる【Model+Collection+View連携編】

今回はModel, Collection, Viewの連携をしてみます。

設計

電話帳をずっと例にしてきたので今回も電話帳で。

電話帳は複数の電話情報の集合なので、

  • 電話情報(Model)
  • 電話帳(Collection)

と整理することができます。
これらを表示するために、

  • 1件あたりの電話情報を表示するView
  • 電話情報の集合をラップするようなView

が必要になります。

CollectionにModelが追加されたら
Viewの方にも自動的に反映されるようにしたいところです。


実装

前に作ったコードを使いながら書いてみました。

htmlの部分はこんな感じ。

<!-- 電話情報が個々に埋め込まれる -->
<div id="addressBook"></div>

<!-- jsRenderのテンプレート -->
<script id="address-template" type="text/x-jsrender">
  <div class="address">
    <ul>
      <li>{{>name}}</li>
      <li>{{>tel}}</li>
    </ul>
  </div>
</script>

<!-- js読み込み -->
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.2/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.0.0/backbone-min.js"></script>
<script src="javascripts/lib/jsrender.min.js"></script>
<script src="javascripts/address.js"></script>


Backboneで書いた部分はこんな感じ。

// 電話情報を管理するModel
var Address = Backbone.Model.extend({
    defaults: function(){
        return {
            language: 'jp'
        };
    },
    initialize: function(){
        _.bindAll(this);
        this.on('initialize', this.setRegisterDate);
        this.trigger('initialize');
    },
    setRegisterDate: function(){
        this.set('registerDate', new Date());
    }
});

// 電話情報をまとめて管理するCollection
var AddressBook = Backbone.Collection.extend({
    model: Address,
    comparator: 'name',
    initialize: function(){
        _.bindAll(this);
        this.on('change', this.cbChange);
    },
    cbChange: function(model, collection){
        var index = this.indexOf(model);
    }
});

// Model用のView
var AddressView = Backbone.View.extend({
    initialize: function(){
        _.bindAll(this);
    },
    render: function(){
        var json = this.model.attributes;
        var html = $.templates(AddressView.tmplate).render(json);
        $(this.el).html(html);
        return this;
    }
}, {
    tmplate: $('#address-template').html()
});

// Collection用のView
var AddressBookView = Backbone.View.extend({
    el: '#addressBook',
    initialize: function(){
        _.bindAll(this);
        this.listenTo(this.collection, 'add', this.addAddressView);
    },
    addAddressView: function(model){
        var addressView = new AddressView({ model: model });
        this.$el.append(addressView.render().el);
    }
});


// インスタンスを作って電話情報を追加する
var addressBook = new AddressBook();
var addressBookView = new AddressBookView({
    collection: addressBook
});

addressBook.add([
    { name: 'yutapon', tel: '090-xxxx-xxxx' },
    { name: 'hogepon', tel: '080-xxxx-xxxx' }
]);

 

実行結果

CollectionにModelが追加されると
<div id="addressBook">の中身がこのようになります。

<div id="addressBook">
    <div>
        <div class="address">
            <ul>
                <li>yutapon</li>
                <li>090-xxxx-xxxx</li>
            </ul>
        </div>
    </div>
    <div>
        <div class="address">
            <ul>
                <li>hogepon</li>
                <li>080-xxxx-xxxx</li>
            </ul>
        </div>
    </div>
</div>

 

連携部分について

AddressView

まずはModel用のViewについて見てみます。

var AddressView = Backbone.View.extend({
    initialize: function(){
        _.bindAll(this);
    },
    render: function(){
        var json = this.model.attributes;  // 【1】
        var html = $.templates(AddressView.tmplate).render(json); // 【2】
        $(this.el).html(html);  // 【3】
        return this;  // 【4】
    }
}, {
    tmplate: $('#address-template').html()
});

render()によってModelのHTMLを生成しています。
【1】ではこのViewのインスタンスの属性modelからattributesを取得しています。
attributesというのは属性値のことなので、model.get('hoge')などで取得できる値のことになります。
【2】ではjsRenderのテンプレートに変数を埋め込み、コンパイルしてHTMLを取得しています。
【3】では【2】で生成したHTMLをthis.elに埋め込んでいます。
この時、AddressViewにはel属性が無いので、デフォルトの<div></div>に埋め込まれます。
【4】ではAddressViewのインスタンスをリターンしています。

AddressBookView

つぎに、Collection用のViewについて見てみます。

var AddressBookView = Backbone.View.extend({
    el: '#addressBook',
    initialize: function(){
        _.bindAll(this);
        this.listenTo(this.collection, 'add', this.addAddressView); // 【1】
    },
    addAddressView: function(model){  // 【2】
        var addressView = new AddressView({ model: model }); // 【3】
        this.$el.append(addressView.render().el);  // 【4】
    }
});

【1】ではコレクションのaddイベントを購読して、
addイベントが起きたらaddAddressViewメソッドを実行するようにしています。
【2】は【1】で設定されたメソッドです。コレクションに追加されたモデルは
addイベントの引数として取得できます。
【3】ではコレクションに追加されたモデルをViewのコンストラクタにセットし、
Viewのインスタンスを生成しています。
【4】ではaddressViewのrenderメソッドを実行した後にel属性を取得し、
それを#addressBookのセレクタにマッチするDOM上に追加しています。

アプリケーション実行部分

今までのは処理のフローを定義したビジネスロジック部分でした。
実際にデータの入出力をしてアプリケーションとして動作させる部分は
次の部分です。

// インスタンスを作って電話情報を追加する
var addressBook = new AddressBook();  // Modelのインスタンス生成
var addressBookView = new AddressBookView({ // Collectionのインスタンス生成
    collection: addressBook  // addressBookのCollectionであることを指定
});

addressBook.add([  // addメソッドでModelを追加する
    { name: 'yutapon', tel: '090-xxxx-xxxx' },  // addressBookのコンストラクタ引数に渡される
    { name: 'hogepon', tel: '080-xxxx-xxxx' }
]);


addressBook.add()が起点となり、Modelが生成されCollectionにaddイベントが発火し、
それをCollection用のViewが検出して、Model用のViewのrenderメソッドを走らせ
DOMに追加するといった一連の流れが始まります。


おわりに

現状ではaddイベントにしか対応していないですが、
本来ならばremoveイベントにも対応するべきでしょう。

また、addressBook.add()の引数に指定している配列を
サーバのAPIから受け取ったデータにすれば、動的にViewを変化させることができます。

そしてBackbone.jsではCollection.fetch()などで実現できます。

次回、サーバ連携編といきたいところですが、
肝心のサーバができていないので久しぶりにnode.js回をやります。

そういえばRouter編もまだやってませんが近いうちに。