CakePHP + Backbone.jsで作るJavaScript Web Application ~ Viewの利用 ~

Thursday, April 12th, 2012

以前、記事「CakePHP + Backbone.jsで作るJavaScript Web Application」を書きましたが、このときは、CakePHPとBackbone.jsをどう連携させるかに焦点を当てていたので、Backbone.jsの重要な機能であるViewを全く使っていませんでした。ということで、今回はViewを使った場合のCakePHPとBackbone.jsの連携について説明します。

なお、ソースコードはgithubのこちらのリポジトリのbackbone_client2となります。

1. Backbone.jsにおけるView

Backbone.jsのViewは、その名前とは異なり、実際にはViewを表示するというより、ModelとViewをつなぐ部品としての役割が主となります。なので、実際の表示、つまりDOMの操作にどの方法を使うかは開発者に任されています。今回のアプリケーションでは前回と同じくjQueryを使うこととします。なので、Backbone.jsが提供しているViewクラス(Backbone.View)には、あまり多くの機能はなく、どちらかと言えばViewを作るための土台となるクラスを提供しているというイメージです。

今回のアプリケーションで使うモデルは、以前の記事と変わらず、一つの顧客モデルを表すCustomerと、複数の顧客モデルを含むCustomersコレクションを定義します。そして、二つのモデルに対応するViewクラスであるCustomerViewとCustomersViewを新たに定義します。(わかりづらいですが単数形と複数形の違い)

アプリケーションの構成としては、一つのCustomersコレクションのインスタンスと、それに対応するCustomersViewのインスタンスがあり、その二つがバインドされます。そして、一覧表示、追加、削除、編集といった処理で、一つの顧客を表すCustomerモデルとCustomerViewが生成、更新、削除されていくこととなります。このため、CustomersViewとCustomerViewとでは、同じBackbone.Viewを継承しているViewクラスですが、結構実装の内容が異なる点に注意してください。

なお、先ほども触れたようにBackbone.jsのViewは、土台程度の部分しか提供されていないので、実装の仕方には、いろいろな選択肢があります。今回は、Backbone.jsのサイトにあるTodo List Applicationに近い形式を選択しています。

2. 大まかな実装の流れ

Viewの実装の流れは、大きく以下となります。

  1. tagNameプロパティもしくはelで、Viewと対となるHTML要素を生成もしくは指定する。
  2. eventsプロパティで、UIイベントとViewをバインドする。
  3. initializeメソッドで、ModelのイベントとViewをバインドする。
  4. renderメソッドで$elを編集する処理を記述する。
  5. その他のイベントに対するコールバックメソッドを実装する。

3. CustomerViewクラスの定義

Customerモデルと対になるCustomerViewクラスは以下のような内容となります。

var CustomerView = Backbone.View.extend({
  tagName : 'tr',
  events : {
    "click .edit-button" : "onEditButton",
    "click .delete-button" : "onDeleteButton"
  },
  initialize : function() {
    this.model.on('change', this.render, this);
    this.model.on('destroy', this.remove, this);
  },
  render : function() {
    this.$el.html('<td>' + this.model.get('id') + '</td><td>'
        + this.model.get('name') + '</td><td>'
        + this.model.get('address')
        + '</td><td>'
        + '<input class="edit-button" type="button" value="Edit"/>'
        + '<input class="delete-button" type="button" value="Delete"/>'
        + '</td>');
    return this;
  },
  onEditButton : function() {
    $('#dialog-name').val(this.model.get('name'));
    $('#dialog-address').val(this.model.get('address'));
    $('#dialog').bind('dialogclose', {model: this.model},
      function(event) {
        event.data.model.set('name', $('#dialog-name').val());
        event.data.model.set('address', $('#dialog-address').val());
        event.data.model.save();
        $('#dialog').unbind('dialogclose');
      }
    );
    $('#dialog').dialog('open');
  },
  onDeleteButton : function() {
    this.model.destroy();
  }
});

Backbone.jsのViewは、モデルを表すビューのDOMツリーの起点(ルート)となる要素と対になります。たとえば、table要素の一つの行が一つのモデルに対応する場合は、tr要素とViewが対になります。そしてViewクラスがインスタンス化される際に、対になるtr要素がBackbone.View内部で自動的に生成されます。このため、ビューに対してどうやって要素を生成させるかの情報を、ビューを定義する際に与えてあげる必要があります。そのための指定が以下のtagNameの指定です。つまり、ここではtr要素が生成されます。tagName以外にも、オプションとしてclassNameやidを指定することもできます。

  tagName : 'tr'

そして、生成された要素はelというプロパティに設定されます。さらに$elでjQueryオブジェクトとしてアクセスできます。

次にeventsオプションで、HTML要素のイベントとViewをバインドします。

events : {
  "click .edit-button" : "onEditButton",
  "click .delete-button" : "onDeleteButton"
},

指定の仕方は「”イベント セレクタ” : “コールバック”」となります。ですので、最初の指定ではedit-buttonクラスを持つ要素のclickイベントにonEditButtonコールバックをバインドします。

イベントのバインドにはjQueryのdelegateを使っています。具体的には「this.$el.delegate(セレクタ, イベント, コールバック)」でバインドしています。ですので、$elを起点としたDOMツリー内からセレクタで選択される要素にバインドされます。つまり、$el以下にない要素とはバインドできません。

initializeメソッドは、Viewがインスタンス化される際に自動的に呼び出されます。ここでは、モデルのイベントとコールバックを結び付けています。

initialize : function() {
  this.model.on('change', this.render, this);
  this.model.on('destroy', this.remove, this);
},

Backbone.jsのモデルには、イベントをバインドするためのon(もしくはbind)というメソッドが定義されています。そして、イベントの種類として以下が定義されています。

イベント名 契機 対象
add コレクションにモデルが追加された場合 モデル、コレクション
remove コレクションからモデルが削除された場合 モデル、コレクション
reset コレクションの内容がすべて入れ代った際 コレクション
change モデルの属性が変更された際 モデル、オプション
change:[attribute] 特定の属性が変更された際 モデル、値、オプション
destroy モデルが破棄された際 モデル、コレクション
sync モデルのサーバへの同期に成功した際 モデル、コレクション
error モデルのバリデーションが失敗した際か、サーバへの保存に失敗した際 モデル、コレクション
route:[name] ルーターのルートに一致した際 ルーター
all この特別なイベントは、すべてのイベントで発生し、第一引数にイベント名を設定する

一つ目の「this.model.on(‘change’, this.render, this)」の場合、モデル「this.model」で「change」イベント(つまり、モデルの属性の変更)が発生した場合、ビューの「render」メソッドが呼び出されます。なお、第三引数の「this」は、コールバックメソッド側でthisとして扱うオブジェクトを指定します。ここでは、thisなのでビュー自身を指定しています。

二つ目の「this.model.on(‘destroy’, this.remove, this)」の場合、「destroy」イベント(つまり、モデルの破棄)が発生した場合、ビューの「remove」メソッドが呼び出されます。なお、このremoveメソッドは、Backbone.Viewにあらかじめ定義されており、「this.$el.remove()」が実行され、このビューと対になるHTML要素の削除が行われます。

なお、「this.model」の値は、Viewを生成する際にオプションとしてmodelを指定することで設定されます。今回はCustomersViewのonAddEventで指定しています。

次のrenderがビューの中心となる部分です。

render : function() {
  this.$el.html('<td>' + this.model.get('id') + '</td><td>'
      + this.model.get('name') + '</td><td>' + this.model.get('address')
      + '</td><td>'
      + '<input class="edit-button" type="button" value="Edit"/>'
      + '<input class="delete-button" type="button" value="Delete"/>'
      + '</td>');
  return this;
},

renderメソッド内では、$elをモデルのデータを基に編集します。$el自身はtagNameから生成されているので、行うのは$elの属性の変更や、子要素の追加などとなります。

最後の「return this」は、他のビューがCustomerViewのrenderの結果が必要となる際にアクセスしやすいように、このような記述の仕方をしています(ちなみに、Backbone.Viewのrenderメソッドの内容は「return this」のみ)。実際に、この後、CustomersViewのrenderで使っています。

Backbone.jsで用意しているビューへの変更はここまでです。この後にあるonEditButtonとonDeleteButtonは、編集ボタンと削除ボタンを押した際に起動されるコールバックメソッドとなります。それぞれ、モデルの属性の更新とモデルの削除を実装しています。

4. CustomersViewクラスの定義

コレクションCustomersに対応するビューCustomersViewクラスの内容は以下となります。

var CustomersView = Backbone.View.extend({
  el : $('#customer-app'),
  events : {
    "click #add-button" : "onAddButton"
  },
  initialize : function() {
    customers.on('add', this.onAddEvent, this);
    customers.on('reset', this.onResetEvent, this);
    customers.on('all', this.render, this);
    customers.fetch();
  },
  onAddEvent : function(customer) {
    var customerView = new CustomerView({
      model : customer,
      id : customer.get('id')
    });
    this.$('#list-tbody').append(customerView.render().el);
  },
  onResetEvent : function() {
    this.$('#list-tbody').html('');
    customers.each(this.onAddEvent);
  },
  onAddButton : function(e) {
    $('#dialog-name').val('');
    $('#dialog-address').val('');

    $('#dialog').bind('dialogclose', function(event) {
      customers.create({
        name : $('#dialog-name').val(),
        address : $('#dialog-address').val()
      });
    });
    $('#dialog').dialog('open');
  }
});

すでに説明したCustomerViewと同じくBackbone.Viewを継承していますが、いきなり最初から異なります。

  el : $('#customer-app'),

CustomerViewの場合は、tagNameを指定して、生成されるHTML要素の種類を指定していました。というのも、CustomerViewの場合、顧客の件数により動的にビューが生成されるためです。対して、CustomersViewはtable要素などをすべて含むdiv要素と対となります。そして、そのdiv要素はHTML上にある静的なもので、生成、削除が行われることはありません。Backbone.jsのViewは、elが存在しないとインスタンス化の際にtagNameからelを生成しますが、もしすでにelが存在する場合は、生成しないようになっています。ですので、$(‘#customer-app’)で、対となるdiv要素をelに指定しています。

次のinitializeでは、CustomerViewと同じくイベントとコールバックメソッドのバインドを行っています。そして最後に「customers.fetch()」でサーバから現在DBに登録されている顧客データの一覧を取得しています。なお、customersはCustomersコレクションのインスタンスです。

initialize : function() {
  customers.on('add', this.onAddEvent, this);
  customers.on('reset', this.onResetEvent, this);
  customers.on('all', this.render, this);
  customers.fetch();
},

コレクションのfetch()が実行されると、resetイベントが発生するので、自動的にバインドしていたthis.onResetEventメソッドが呼び出されます。そして、this.onResetEventメソッドではすべての顧客データをtable要素に追加しています。

一件追加処理は、まず、Addボタンが押された後に、コレクションのcreateメソッドで一件のCustomerを追加しています。すると、コレクションでaddイベントが発生するのでonAddEventメソッドが呼び出されます。

onAddEvent : function(customer) {
  var customerView = new CustomerView({
    model : customer,
    id : customer.get('id')
  });
  this.$('#list-tbody').append(customerView.render().el);
},

ここでは、CustomerViewを生成していますが、その際に対となるCustomerモデルを指定しています。同時に、idを指定することで、CustomerViewの対となるtr要素のid属性を指定しています。

実際のtable要素への行の追加は、CustomerViewのrenderメソッドを呼び出し、その結果を追加することで行っています。

なお、今回はrenderメソッドを特に定義していませんが、もし、モデルの情報が変わったら変更しなければならないUIがあれば定義します。ちなみに、Backbone.jsのサイトにあるTodo List Applicationでは、データが0件の場合に、一覧自体をまったく表示しないようにしています。

5. 最後に

ソースコードはgithubのこちらのリポジトリのbackbone_client2となりますので、細かい点はこちらを参照してください。