Backbone.js入門 その3

こんばんは、south37です。
今日も前回に引き続きBackbone.jsです。

View編

今日は、BackboneのViewに触れてみたいと思います。例によってこのページを参考にしていきます。

そもそもViewとは何でしょうか。ざっくり言うと表示に関わる処理を行う訳ですが、Backbone.jsにおいては大きく分けて2つの役割があり、
1. DOMのイベントハンドリング
2. Model/Collectionのイベントハンドリング
をします。Viewはユーザとのインターフェースである為、ユーザによる操作(ボタンクリック等、1に該当)をイベントとして受け取ってバックエンドに通知したり、バックエンドからの処理完了の通知(2に該当)を受け取ってviewを書き換えたりする必要がある訳です。
BackboneのViewはかなり機能をしぼってある為、以前紹介したHandlebarsのようなテンプレートエンジンも適宜組み合わせながら実装すると良いと思います。

それでは、実際にコードを見てみましょう。まずは、アプリ全体のViewを扱うappViewを定義します。

var AppView = Backbone.View.extend({
  events: {
    "click #addBtn": "onAdd"
  },
  initialize: function () {
    this.$title = $("#addForm [name='title']");
    this.$content = $("#addForm [name='content']");

    this.collection = new MemoList();
    this.listView = new ListView({el:$("#memoList"), collection:this.collection});

    this.render();
  },
  render: function () {
    this.$title.val('');
    this.$content.val('');
  },
  onAdd: function () {
    this.collection.add({
      title:this.$title.val(),
      content:this.$content.val()
    });
    this.render();
  }
});

var appView = new AppView({el:$("#main")});

いつもの様にextendでプロパティをいろいろ定義しています。eventsでは、DOM操作に対して呼ばれるメソッドを定義します。上記の例では、idがaddBtnであるDOM要素をクリックした時に、onAddメソッドが呼ばれるようにしています。

initializeの中では、まずformに入力した値をViewが取得出来るようにする為に、jqueryでつかまえたDOMオブジェクトをjQueryオブジェクトの形で$titleと$contentに突っ込んでいます。jqueryオブジェクトは変数名の頭に$をつけるのが慣例みたいです。ちなみに、HTMLは

<form id="addForm">
  <input name="title"/>
  <textarea rows="5" name="content"></textarea>
  <a href="#" id="addBtn" class="btn btn-primary">Add</a>
</form>

のようになっています。
さらに、前回定義したMemoListやこの後定義するListViewもプロパティとして持つようにします。最後に、viewをレンダリングする用のメソッドであるrenderを呼びます。

renderは、もともとは中身が空のメソッドなので、自分でviewを描き変える処理を書いてやる必要があります。今回の例では、入力フォームをリフレッシュしています。画面はrenderからしか描きかえないという規約を作る事で、スパゲッティコードを避ける事が出来ます*1

onAddでは、addボタンが押された時の処理を書いています。上記の例では、collection(MemoList)にformに入力された値をプロパティとして持つMemoオブジェクトを追加しています。

次に、上でも出てきたListViewクラスを定義します。これは、MemoListに加えられたMemoオブジェクト全体の描画を扱うクラスです。コードは次のようになります。

var ListView = Backbone.View.extend({
  initialize: function () {
    this.listenTo(this.collection, "add", this.addItemView);
  },
  addItemView: function (item) {
  this.$el.append((new ItemView({model: item})).render().el);  }
});

まずinitializeは、collectionのaddメソッドが呼ばれた時にaddItemViewが呼ばれるように設定しています(前回紹介したlistenToを使ったobserverパターンですね)。appViewの中でListViewをnewする時にcollectionとしてmemolistを渡してるので、ここでのcollectionはmemolistを指しています。

addItemViewの中では、new演算子でItemViewオブジェクトを生成してから、renderメソッドを呼び出してhtml文字列の生成を行った後、elプロパティに入ったjQueryオブジェクトをthis.$elに追加しています。ItemViewのrenderメソッドはこの後で定義をお見せしますが、テンプレートエンジンを使ってhtml文字列を生成します。
appendはjqueryオブジェクトの持つメソッドで、このページで説明されている通り親jqueryオブジェクトに子jqueryオブジェクトを追加します。この時点で、描画も行われる事になります。

次に、ItemViewを定義します。

var ItemView = Backbone.View.extend({
  tmpl: _.template($("#tmpl-itemview").html()),
  events: {
    "click .delete": "onDelete"
  },
  onDelete: function () {
    this.model.destroy();
    this.remove();
  },
  render: function () {
    this.$el.html(this.tmpl(this.model.toJSON()));
    return this;
  }
});

まず、テンプレートとしてunderscore.jsのtemplateを使います。これは、以前紹介したHandlebars.js等と同じで、HTML文字列を渡してやる事で、それをレンダリングするメソッドを返します。HTMLには、

<script type="text/template" id="tmpl-itemview">
  <h3><%= title %></h3>
  <p><%= content %></p>
  <p><a href="#" class="delete">delete</a></p>
</script>

のように記述します。<%= %>で囲まれている変数部分が書き換えられる事になります。

eventsでは、deleteボタンのclickにonDeleteをバインドします。onDeleteでは、modelと自身(itemViewオブジェクト)の破棄を行います。
renderでは、テンプレートメソッドを使ってhtml文字列を作った後、それをjQueryオブジェクトである$elのhtm要素として設定して、さらに自身をreturnしています。これによって、先程のlistViewの中の処理でrenderからelへとメソッドチェーンを繋ぐ事が出来た訳です。

以上のコードで、簡単なサーバーを使わないメモアプリが作れます。ちなみに、元のページではサーバーを利用する版のコードを書いているので、興味があれば読んでみるといいと思います。

今日はここまでです。

追記

実際に書いたHTMLとjsコードをのせておきます。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Backbone View Example</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="lib/bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
  <style>
    body {
      padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */
    }
  </style>
</head>   

<body>
  <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <ul class="nav">
        <li class="navbar-text"><a class="brand" href="./index.html">Backbone Memo Application</a></li>
      </ul>  
    </div>
  </div>

  <div id="main" class="container"> 
    <h1>Memo</h1>
 
    <form id="addForm" role="form" class="form">
      <div class="form-group">
        <input type="title" name="title" class="form-control" placeholder="Title"/>
      </div>
      <div class="form-group">
        <textarea rows="5" name="content" class="form-control" placeholder="Content"></textarea>
      </div>
      <a href="#" id="addBtn" class="btn btn-primary">Add</a>
    </form>

    <div id="memoList" class="row-fluid"></div>        
  </div>

  <script type="text/template" id="tmpl-itemview">
    <h3><%= title %></h3>
    <p><%= content %></p>
    <p><a href="#" class="delete">delete</a></p>
  </script>
  <script src="lib/bower_components/jquery/jquery.min.js"></script>
  <script src="lib/bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
  <script src="lib/bower_components/json2/json2.js"></script>
  <script src="lib/bower_components/underscore/underscore-min.js"></script>
  <script src="lib/bower_components/backbone/backbone-min.js"></script>
  <script src="lib/js/model.js"></script>
  <script src="lib/js/view.js"></script>
</body>
</html>

最後のjsライブラリ読み込みのとこでは、bower_components配下のライブラリを読み込んでいます。これは、bowerと呼ばれるクライアントサイドjs用のパッケージマネージャを使ってダウンロードしたからです。このページに使い方がのってますが、

bower install ライブラリ名

でライブラリのインストールが出来ます。この際に

bower install --save ライブラリ名

という風に--saveオプションをつけてインストールを行うと、bower.jsonというファイルにその情報が書き込まれていきます。bower.jsonを共有する事で簡単に同じ構成を作る事が出来る為、とても便利です。
今回作ったbower.jsonの中身はこんな感じです。

{
  "name": "view",
  "version": "0.0.0",
  "authors": [
    "Tarou Yamada <tarou@gmail.com>"
  ],
  "license": "MIT",
  "dependencies": {
    "jquery": "~2.0.3",
    "json2": "*",
    "backbone": "~1.1.0",
    "underscore": "~1.5.2",
    "bootstrap": "~3.0.3"
  }
}

これをlibディレクトリ配下にbower.jsonという名前で置いておき、

bower install

すれば、全く同じ環境が出来上がります。

今回書いたjsファイルは、model.js

var Memo = Backbone.Model.extend({
  defaults: {
    "content":""
  },
  validate:function (attributes) {
    if (attributes.content === "") {
      return "content must be not empty.";
    }
  }
});
                                         
var MemoList = Backbone.Collection.extend({
  model:Memo,
  url:"/memos"
});

とview.js

var ItemView = Backbone.View.extend({
  tmpl: _.template($("#tmpl-itemview").html()),
  events: {
    "click .delete": "onDelete"
  },
  onDelete: function () {
    this.model.destroy();
    this.remove();
  },
  render: function () {
    this.$el.html(this.tmpl(this.model.toJSON()));
    return this;
  }
});

var ListView = Backbone.View.extend({
  initialize: function () {
    this.listenTo(this.collection, "add", this.addItemView);
  },
  addItemView: function (item) {
    this.$el.append((new ItemView({model: item})).render().el);
  }
});


var AppView = Backbone.View.extend({
  events: {
    "click #addBtn": "onAdd"
  },
  initialize: function () {
    this.$title = $("#addForm [name='title']");
    this.$content = $("#addForm [name='content']");

    this.collection = new MemoList();
    this.listView = new ListView({el:$("#memoList"), collection:this.collection});
                                                      
    this.render();
  },
  render: function () {
    this.$title.val('');
    this.$content.val('');
  },
  onAdd: function () {
    this.collection.add({
      title:this.$title.val(), 
      content:this.$content.val()
    });
    this.render();
  }
});

var appView = new AppView({el:$("#main")});

の二つです。こいつらもlib/jsはいかに置いてやり、index.htmlを

open index.html

の様にブラウザで開いてやれば、メモアプリが出来上がっているはずです。

*1:jsがスパゲッティになりやすい理由として、ロジックと描画がグチャグチャに混じりやすい点があげられます。Backbone.jsでは、きちんと役割を持ったメソッドを用意する事でこれを避けています