JavaScript
CakePHP
AWS
rest
勉強会

Marionette.jsでBackboneをもっと便利に! - AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~【第3回】マニュアル

More than 3 years have passed since last update.

:large_blue_circle: はじめに

本投稿は、2015/3/26に行われたAWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~【第3回】 - connpassの内容についてまとめた資料です。

:warning:ご参加頂いた方へ。
有難うございました!次回もぜひよろしくお願いします。
今後の予定⇢AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~ - connpass

今回は、Marionette.jsを使用します。
Backboneだけでは面倒な部分、煩雑になる部分などを解消していく過程を体験していきましょう!

:large_blue_circle: Marionetteの概要

Marionette.jsとは?

Backboneはその名の通り、クライアントアプリケーションでMVCモデルを構築する際の「背骨」を提供します。しかし、まさに背骨のみであり、実際のアプリケーション構築ではさまざまなコードをプログラマが書く必要があり、「毎回同じようなコードを書く」という状況が往々にして発生します。
前回のプログラムで言うと、

  • Viewの切り替えをしている部分
  • ネストしたViewの管理
  • サーバからのデータ取得からViewのRenderまでの一連のコード

などです。Marionetteを使用すると、かなりの部分を肩代わりしてくれます。

また、アプリケーションの規模が大きくなってくると、BackboneのみではViewの管理が大変になってきます。全体のレイアウトを定義するビュー、イベントを定義するビュー、コレクションを表示するビュー、モデルを表示するビュー、etc....。
これらも、Marionetteが提供する、役割に応じた複数のViewやRegionといった機能を利用することでViewの構造が把握しやすくなり、可読性、保守性の向上につながります。

他にも、uiなどの便利な機能、今回は触れませんが、triggersBehaviorsなど、有益な機能が盛り込まれています。

Backboneにはさまざまなプラグインがありますが、まず最初に導入を検討すべきなのがこのMarionetteといえるでしょう。

Marionetteオブジェクトをざっくり紹介

Marionetteは、MVCでいう、コントローラとビューに対しての機能を持ちます。
Backboneのオブジェクトが担っていた機能をMarionetteが提供するオブジェクトに置き換えていくことになります。今回使用するMarionetteのオブジェクトは下記のとおりです。各オブジェクトの機能詳細は後ほど述べます。
まずはこれだけある、ということで。

コントローラ系

Marionetteオブジェクト 概要
Marionette.Application Marionetteを使用したアプリケーションのスタートポイントとなります
Marionette.AppRouter ルーティングを行います。
Marionette.Controller コントローラです。

ビュー系

Marionetteオブジェクト 概要
Marionette.Application リージョン定義機能を持ちます。
Marionette.Region 画面に定義する領域「リージョン」を表すオブジェクトです。
Marionette.LayoutView 複数のビューのレイアウトを定義することに適したビューです。
Marionette.CollectionView Collectionの表示に適したビューです。
Marionette.CompositeView Collectionの表示に適したビューです。
Marionette.ItemView Modelの表示に適したビューです。

各ビューのリージョン機能、テンプレート機能の有無。

View リージョン機能 テンプレート機能
Marionette.Application 有り 無し
Marionette.LayoutView 有り 有り
Marionette.CollectionView 無し 無し
Marionette.CompositeView 無し 有り
Marionette.ItemView 無し 有り

Backboneオブジェクトとの関係

Backboneのオブジェクトと、Marionetteのオブジェクトとの対応関係は以下のようになります。

Backbone Marionette
- Marionette.Application
Backbone.Router Marionette.AppRouter
Marionette.Controller
Backbone.View Marionette.Region
Marionette.LayoutView
Marionette.CollectionView
Marionette.CompositeView
Marionette.ItemView

:large_blue_circle: 今回の内容

前回作成したプログラムを、Marionette.jsの各オブジェクトを使用して書き換えていきます。
:warning:画面は何も変わりません。内部の実装だけが変わります。

ではまず、準備から!

:large_blue_circle: 事前準備

SSHでログイン

sshログインから始めましょう!

  • :white_check_mark: ssh -i [秘密鍵のパス] study@[サーバのPublicIP]

ディレクトリを移動しておきます。

  • :white_check_mark: cd /var/www/study/rest-study

では、はじめましょう!

gitのブランチを整えておく

前回は、vol/02ブランチで作業をしました。前回のマニュアルでは、vol/02ブランチをpushしてGitHubで確認まで、としていましたので、まず前回までの内容をmasterブランチにマージします。
前回が作業途中の方や、今回から参加の方は、新たにディレクトリを作成します。

:warning:今回から参加される方、第1回の内容は終えていることが条件ですのでご了承ください。

今回分から始められる方、前回が途中の方、終わっているかたでやり方が違うのでご注意ください。
やりたいことは、

  • masterブランチを前回の終了状態にする
  • masterブランチを元に、今回の作業用である、「vol/03」ブランチを作成する

前回が作業途中の方

とにかくコミットしましょう!
途中でもとにかく!

次は全員同じ。 fork元をリモートリポジトリとして追加する

第一回勉強会でforkした元のリポジトリをリモートリポジトリとして追加します。

:white_check_mark: git remote
とコマンドを打って、

origin
upstream

と表示される場合はこの作業は終わっています。次へ行きましょう!
upstreamが表示されない人は次のコマンド。

:white_check_mark: git remote add upstream https://github.com/suzukishouten-study/rest-study.git

これでOKです!

次も全員同じ。 upstreamリポジトリから最新を取得する

:white_check_mark: git fetch upstream

これでOK!

次は、

  • 前回の内容を完了した方
  • 前回の内容を途中までやった方と今回から参加の方

で少し違うのでご注意!

前回の内容を完了した方

masterブランチを前回分を終了した状態にします。
前回を完了した方は、vol/02ブランチをマージです。

:white_check_mark:git checkout master

でmasterブランチをチェックアウト後、

:white_check_mark:git merge vol/02

でマージ。これでOK!

前回の内容を途中までやった方と今回から参加の方

masterブランチを前回分を終了した状態にします。
前回の内容を途中までやった方と今回から参加の方は、upstreamリポジトリのvol/02-finishブランチをマージです。

:white_check_mark: git checkout master

でmasterブランチをチェックアウト後、

:white_check_mark: git reset --hard upstream/vol/02-finish

で強制的にマージ。これでOK!

今回の作業用のbranchを作成

全員同じ手順です。

今回の作業用ブランチとして、vol/03ブランチを作り、チェックアウトします。
:warning:masterブランチから分岐してvol/03ブランチを作成するので、まずmasterブランチにいることを確認して下さい。
これまでの作業で、既にmasterブランチにいるはずですが。

  • :white_check_mark: git branchmasterにいることを確認
  • :white_check_mark: masterにいなかった場合は、git checkout mastermasterをチェックアウト

以下コマンドでvol/03ブランチを作成します。

  • :white_check_mark: git branch vol/03 でブランチを作って
  • :white_check_mark: git checkout vol/03 でそのブランチに変更

確認

念のため確認します。

  • :white_check_mark: git log でコミットログを確認してみましょう。

コミットログが出力されます。
前回の最後のコミットが表示されていればOKです。

これでブランチの準備は整いました。

:warning: git branch -a と、全ブランチを表示すると、vol/03-finishブランチが見えると思いますが、これは今回の内容が全て終えた状態のソースが入っています。このソースは、下記URLから各Lesson毎のコミットが確認できますので参考にしてください。

Commits · suzukishouten-study/rest-study

:large_blue_circle: Marionetteライブラリダウンロード

Marionetteの公式サイトMarionette.js – The Backbone Frameworkから取得します。

ダウンロードURLは下記です。サーバにSSHでログインし、wgetコマンドで取得します。
このあたりの手順は前回の資料にも記載がありますので参考にしてください。

  • :white_check_mark:/var/www/study/rest-study/app/webroot/js/libにダウンロードします。
cd /var/www/study/rest-study/app/webroot/js/lib
wget http://marionettejs.com/downloads/backbone.marionette.min.js

これで準備完了です!
では、Marionetteの各オブジェクトがどのように機能するのか、詳しく見て行きましょう。
まずはLesson1、「エントリポイント・ルーティング・コントローラ」と題し、前回作成したプログラムに、

  • Marionette.Application
  • Marionette.AppRouter
  • Marionette.Controller

の3つを適用していきます。

:large_blue_circle: Lesson 1 エントリポイント・ルーティング・コントローラ

Marionetteを使用して一番恩恵を受けるのはビュー部分なのですが、その前にまずエントリポイント・ルーティング・コントローラのあたりの変更点を抑えておくのがLesson1です。
まだMarionetteが本領を発揮するところではありません。
やっていることは下記のとおりです。

  • エントリポイントの変更
  • ルータとコントローラの分離

下記のMarionetteオブジェクトを使って、前回のプログラムを修正していきます。

  • Marionette.Application
  • Marionette.AppRouter
  • Marionette.Controller

編集するファイル一覧

編集 file 役割
修正 app/View/Layouts/default.ctp HTMLテンプレート
追加 app/webroot/js/main.js アプリケーションの開始ポイント
修正 app/webroot/js/app.js アプリケーション(Marionettte.Application)
修正 app/webroot/js/routers/router.js ルータ(Marionette.AppRouter)
追加 app/webroot/js/routers/controller.js コントローラ

default.ctp

まずはこのファイルです。

default.ctp
        <script src="js/lib/jquery-2.1.3.min.js" type="text/javascript"></script>
        <script src="js/lib/underscore-min.js" type="text/javascript"></script>
        <script src="js/lib/backbone-min.js" type="text/javascript"></script>
+       <script src="js/lib/backbone.marionette.min.js" type="text/javascript"></script>

        <!-- js(application) -->

〜中略〜

        <script src="js/views/todo-item-view.js" type="text/javascript"></script>
        <script src="js/views/todo-detail-view.js" type="text/javascript"></script>
        <script src="js/views/todo-collection-view.js" type="text/javascript"></script>
+       <!--   controller   -->
+       <script src="js/routers/controller.js" type="text/javascript"></script>
        <!--   router   -->
        <script src="js/routers/router.js" type="text/javascript"></script>
-       <!--   entry point   -->
+       <!--   application   -->
        <script src="js/app.js" type="text/javascript"></script>
+       <!--   entry point   -->
+       <script src="js/main.js" type="text/javascript"></script>

 </body>
 </html>
  • 追加したjsファイルの読み込みの追加です。
    • js/lib/backbone.marionette.min.js - Marionette本体。
    • js/routers/controller.js - Marionetteにより提供される、コントローラの実装。あとで作成します。
    • js/app.js - Marionetteにより提供される、Applicationオブジェクトの実装。
      • :warning:修正前は、これがエントリポイントでしたが、修正後は次のmain.jsがエントリポイントとなります。
    • js/main.js - エントリポイントです。あとで作成します。

main.js

前回は、app.jsがエントリポイントでした。今回の修正で、ファイルを2つにわけます。

  • main.js - エントリポイントとなります。
  • app.js - Marionette.Applicationを継承したオブジェクトの実装となります。
main.js
var app = app || {};

//開始
(function(app) {
    app.application = new app.Application();
    app.application.start();
})(app);

app.Application(app.jsで定義)をインスタンス化し、startメソッドを実行するのみです。

app.js

前回app.jsでやっていたことは下記の2つでした。

  • routerをインスタンス化
  • Backbone.history.start()を実行して「hashchangeイベント」の監視を開始
前回のapp.js再掲
var app = app || {};

//開始
(function(app) {
    var todoRouter = new app.TodoRouter();
    Backbone.history.start();
})(app);

Marionetteを使用する場合、これらの処理はMarionette.Applicationを継承したオブジェクト内で実行します。
ですので、このapp.js内ではオブジェクトの定義のみ行い、インスタンス化及び実行はmain.jsから、となるわけです。
前述のmain.js(今回のエントリポイントとなるファイル)内で当オブジェクトがインスタンス化されて実行されます。

app.js
 //開始
 (function(app) {
-       var todoRouter = new app.TodoRouter();
-       Backbone.history.start();
+       app.Application = Backbone.Marionette.Application.extend({
+               initialize : function(){
+                       new app.TodoRouter();
+               },
+
+               onStart : function(){
+                       Backbone.history.start();
+               },
+       });
 })(app);

やっていることは前回と変わりません。

  • インスタンス化 - initializeが実行される。
    • ここでrouterをインスタンス化。
  • start関数実行 - onStartが実行される。
    • ここでhashchangeイベントの監視開始。

router.js

routerは、継承元がMarionette.AppRouterに変わります。
router内ではルーティングの設定のみを書き、ルーティング後実行される関数の実装はcontroller内に書くようになります。

router.js
 //router
 (function(app) {
-       app.TodoRouter = Backbone.Router.extend({
-               routes : {
+       app.TodoRouter = Backbone.Marionette.AppRouter.extend({
+               //コントローラをインスタンス化
+               controller: new app.TodoController(),
+               //ルーティング設定
+               appRoutes : {
                        ''                      : 'todoLists',
                        'todo-lists'            : 'todoLists',
                        'todo-lists/:id'        : 'todoDetail'
                },
-
-               currentView : false,
-
-               todoLists : function() {
-                       //Todo一覧表示用ビューにルーティング
-                       this.removeCurrentView();
-                       this.nextView(app.TodoCollectionView);
-               },
-
-               todoDetail : function(id) {
-                       this.removeCurrentView();
-                       this.nextView(app.TodoDetailView, id);
-               },
-
-               nextView : function(View, option) {
-                       if (document.getElementById('#content') === null) {
-                               $('#main').append('<div id="content"/>');
-                       }
-                       this.currentView = new View(option);
-               },
-               removeCurrentView : function() {
-                       if (this.currentView) {
-                               this.currentView.remove();
-                       }
-               }
-
        });
 })(app);
  • ルーティング後に実行される関数の実装は全てコントローラに移動するため、routerからは削除。
  • controller: new app.TodoController()
    • コントローラをここでインスタンス化しています。

controller.js

ルーティング後の関数の実装をここに移動しています。
内容はそのままです。

controller.js
var app = app || {};

//controller
(function(app) {
    app.TodoController = Backbone.Marionette.Controller.extend({

        currentView : false,

        todoLists : function() {
            //Todo一覧表示用ビューにルーティング
            this.removeCurrentView();
            this.nextView(app.TodoCollectionView);
        },

        todoDetail : function(id) {
            this.removeCurrentView();
            this.nextView(app.TodoDetailView, id);
        },

        nextView : function(View, option) {
            if (document.getElementById('#content') === null) {
                $('#main').append('<div id="content"/>');
            }
            this.currentView = new View(option);
        },
        removeCurrentView : function() {
            if (this.currentView) {
                this.currentView.remove();
            }
        }
    });
})(app);

実装

では、実装しましょう!

  • :white_check_mark: app/View/Layouts/default.ctp を上記の通り修正。
  • :white_check_mark: app/webroot/js/main.js を上記の通り追加。
  • :white_check_mark: app/webroot/js/app.js を上記の通り修正。
  • :white_check_mark: app/webroot/js/routers/router.js を上記の通り修正。
  • :white_check_mark: app/webroot/js/routers/controller.jsを上記の通り作成。
  • :white_check_mark: 動作確認!
  • :white_check_mark: Gitにコミット

完成ソース(GitHub)へのリンク
- default.ctp
- main.js
- app.js
- router.js
- controller.js

Lesson2へ!
Marionetteが本領を発揮するViewの部分を扱います!

:large_blue_circle: Lesson 2 TODO一覧画面にMarionetteのビューを適用

Marionetteが本領発揮する部分、Viewの実装です。
まずはTODO一覧画面を修正します。
前述のとおりMarionetteが提供するViewは4種類ありますが、ここではそれらのうち下記を適用していきます。

  • LayoutView
  • CompositeView
  • ItemView

また、Marionette.ApplicationLayoutViewが持つリージョン機能も使用します。
さて、ここで各ビューの役割を詳しく見て行きましょう。

リージョンとビューの関係

ビュー

「ビュー」は、ある特定のデータを表示する最小単位、という認識でよいでしょう。
前回のプログラムだと、

  • TODO1件(Model1件)を表示するビュー
  • TODO一覧を表示するビュー

がそうです。それぞれ、Model1件、Collection一つを司るビューです。
図にするとこうです。

view.png

リージョン

「リージョン」とは、ビューを表示する領域です。
リージョンの機能により、表示するビューを差し替える(画面遷移)ことが可能になります。
前回はViewの破棄を自前でやっていましたが、リージョンの機能によって、これをMarionetteに任せることが出来ます。
今回は、TODO一覧ビューとTODO詳細ビューを切り替える際にこの機能が使われます。
切り替えについてはLesson3で見ていきます。

切り替えイメージ

region.png

今回のTODOリストアプリケーションではまだやりませんが、表示する対象が多くなってくると、複数のリージョンを用意して、各役割のビューを表示するようにします。

複数リージョンの利用イメージ

リージョン機能は、Marionette.ApplicationオブジェクトおよびMarionette.LayoutView内に生成が可能です。

典型的なパターンとしては、下記のようになります。

  • Marionette.Applicationが、アプリケーションの一番外側の土台となるリージョンを生成
    • そのリージョン内にLayoutViewを配置
      • そのLayoutView内にデータやボタン等を表示するビューを表示
      • またはLayoutView内にさらにリージョンを生成
        • そのLayoutView内にデータやボタン等を表示するビューを表示
        • またはLayoutView内にさらにリージョンを生成

という入れ子構造になります。

:warning:リージョンに配置するViewは常にLayoutViewを使うように書いてますが、リージョンを使用する必要のないシンプルな画面であれば、LayoutViewではなくCompositeViewやItemViewを直接リージョンに配置するほうがよいでしょう。この辺はケースバイケースです。

具体的には下記のようなイメージになります。

regions.png

  • Marionette.Applicationオブジェクトにより、下記リージョンを管理。
    • ヘッダ表示リージョン
      • ヘッダー表示ビューをはめ込んで表示する
    • コンテンツ表示リージョン
      • 内側に表示するビューを切り替える(Xコンテンツ表示LayoutView/Yコンテンツ表示LayoutView)
    • フッタ表示リージョン
      • フッタ表示ビューをはめ込んで表示する。
  • Xコンテンツ表示LayoutView
    • メニューボタン表示リージョン
      • メニューボタン表示LayoutViewをはめ込んで表示する
    • 一覧データ表示リージョン
      • 一覧データ表示LayoutViewをはめ込んで表示する
    • ページ切り替えボタン表示リージョン
      • ページ切り替えボタン表示LayoutViewをはめ込んで表示する

〜以下同様〜

ポイント

下記Marionetteの各Viewの役割を理解することがポイントです。

  1. Region
  2. LayoutView
  3. CompositeView
  4. ItemView

下図の通り、上記の順番でネストした構造になっています。

todo-list.png

以上踏まえて、下記解説を読みつつプログラムを修正していきましょう!

編集するファイル一覧

編集 file 役割
修正 app/View/Layouts/default.ctp HTMLテンプレート
修正 app/webroot/js/app.js アプリケーション(Marionettte.Application)
修正 app/webroot/js/routers/controller.js コントローラ
追加 app/webroot/js/views/todo-layout-view.js ビュー(LayoutView:Todoリストのレイアウト用)
追加 app/webroot/js/views/todo-composite-view.js ビュー(CompositeView:Todoリスト一覧)
修正 app/webroot/js/views/todo-item-view.js ビュー(ItemView:Todoリスト一覧内の1件表示用)
削除 app/webroot/js/views/todo-collection-view.js ビュー(Todoリスト一覧)

default.ctp

LayoutView用のテンプレートがひとつ増えているところがポイントです。

default.ctp
 <body>
    <!-- コンテンツ -->
    <div id="main"></div>
-   <!-- TODO一覧表示のテンプレート -->
-   <script type="text/template" id="list-template">
+
+   <!-- TODO一覧表示のレイアウトテンプレート -->
+   <script type="text/template" id="todo-layout-template">
    <h1>TODOリスト</h1>
+   <div id="todo-lists"></div>
+   </script>
+
+   <!-- TODO一覧表示のテンプレート -->
+   <script type="text/template" id="todo-composite-template">
    <textarea style="width:300px;height:50px"id="new-todo" placeholder="Todo?" autofocus></textarea>
    <input type="button" id="addTodo" value="追加">
    <hr>
    <div>
        <table border="1" width="350px">
-           <tbody id="todo-lists"></tbody>
+           <tbody></tbody>
        </table>
    </div>
    </script>

    <!-- TODO一行分のテンプレート(上のtbody部分に挿入される) -->
-   <script type="text/template" id="item-template">
+   <script type="text/template" id="todo-item-template">
    <td><input type="checkbox" class="toggle" <%- status === '1' ? 'checked' : '' %>></td>
    <td style="margin:0px">
        <span class="todo-edit" style="margin:0px"><%- todo %></span>

〜中略〜

        <a class="detail-link" href="#todo-lists/<%- id %>">詳細</a>
    </td>
    </script>

    <!-- 詳細画面 -->
    <script type="text/template" id="detail-template">
    <h2>Todo #<%- id %></h2>

〜中略〜

    <!--   view   -->
    <script src="js/views/todo-item-view.js" type="text/javascript"></script>
    <script src="js/views/todo-detail-view.js" type="text/javascript"></script>
-   <script src="js/views/todo-collection-view.js" type="text/javascript"></script>
+   <script src="js/views/todo-composite-view.js" type="text/javascript"></script>
+   <script src="js/views/todo-layout-view.js" type="text/javascript"></script>
    <!--   controller   -->
    <script src="js/routers/controller.js" type="text/javascript"></script>
    <!--   router   -->
  • TODO一覧表示のテンプレートを、LayoutViewで使用する部分とCompositeViewを使用する部分に分割しています。
    • TODO一覧表示のテンプレートは全部で、レイアウト用、一覧用、1件表示用になります。それぞれIDは下記のように変更しています。
      • レイアウト用 -> todo-layout-template
      • 一覧用 -> todo-composite-template
      • 1件用 -> todo-item-template
  • レイアウト用のビューに、リージョンとする部分のタグを追加し、idをtodo-listsとしています(ここに一覧表示用のビューが読み込まれます)。

  • 一覧表示用のビュー(todo-collection-view.js)はtodo-composite-view.jsに変更しています。

  • レイアウト用のビュー、js/views/todo-layout-view.jsを追加しています。

app.js

app.js
        onStart : function(){
            Backbone.history.start();
        },
+
+       regions : {
+           mainRegion : '#main'
+       }
+
    });
  • Marionette.Applicatonが持つ、リージョン機能の設定を追加しています。この指定で、default.ctp<div id="main"></div>のタグ内がリージョンとして扱えるようになります。

controller.js

Marionetteのリージョンの機能を使用してビューの描画や破棄を行っているので、そのあたりを自前でやっていた部分のコードが削減されています。

controller.js
 (function(app) {
    app.TodoController = Backbone.Marionette.Controller.extend({

-       currentView : false,
-
        todoLists : function() {
-           //Todo一覧表示用ビューにルーティング
-           this.removeCurrentView();
-           this.nextView(app.TodoCollectionView);
+           //Todoレイアウト用ビューにルーティング
+           this.nextView(app.TodoLayoutView);
        },

        todoDetail : function(id) {
-           this.removeCurrentView();
            this.nextView(app.TodoDetailView, id);
        },

        nextView : function(View, option) {
-           if (document.getElementById('#content') === null) {
-               $('#main').append('<div id="content"/>');
-           }
-           this.currentView = new View(option);
+           app.application.mainRegion.show(new View(option));
        },
-       removeCurrentView : function() {
-           if (this.currentView) {
-               this.currentView.remove();
-           }
-       }
    });
 })(app);

  • removeCurrentView関数(削除)
    • Marionetteのリージョンの機能により、Viewの生成、破棄が管理されるので、自前でやっていたremoveCurrentView関数の定義と呼び出し部分を削除しています。
  • nextView関数
    • 元々はViewの生成を行っているのみでしたが、リージョン機能を使用し、「Viewを生成し、そのViewを引数としてリージョンをshowする」という処理に変更しています。

todo-layout-view.js

レイアウト用ビューです。
ここでの役割は下記です。

  • リージョンの定義
  • コレクションの読み込み
    • コレクションの読み込みは、元々todo-collection-view.jsで行っていましたが、ここで行うようになります。
todo-layout-view.js
var app = app || {};

//Todo一覧表示用レイアウトビュー
(function(app) {
    app.TodoLayoutView = Backbone.Marionette.LayoutView.extend({
        //テンプレート
        template: '#todo-layout-template',

        regions : {
            listRegion : '#todo-lists',
        },

        onRender : function(){
            var todoCollection = new app.TodoCollection();
            this.listenTo(todoCollection , 'reset', this.showTodoList, this);
            todoCollection.fetch({reset : true});
        },

        showTodoList : function(todoCollection){
            this.listRegion.show( new app.TodoCompositeView({
                collection : todoCollection
            }));
        },

    });
})(app);
  • template変数
    • テンプレートは、template変数にidを設定しておくだけで、Marionetteが描画時にそのidで示されるテンプレートを自動で適用してくれます。
  • regions変数
    • ここでリージョンとして使用するタグのidを指定します。ここでは、一覧表示領域となる#todo-listsを指定しています。
  • onRender関数
    • Marionetteの全ての種類のビューで実装されます。ビュー生成時に自動で実行されます。
    • ここでは、コレクションのデータを取得し、取得完了時にshowTodoList関数を実行するようにしています。
  • showTodoList関数
    • onRender関数内で指定したthis.listenTo(todoCollection , 'reset', this.showTodoList, this);により、collectionのresetイベント(コレクションの中身がリフレッシュされた)の発火時に実行されるハンドラです。
    • regions変数で設定した、listRegionを、show関数によって表示しています。show関数の引数にapp.TodoCompositeViewの新しいオブジェクトを渡し、listRegion内のビューとして設定しています。

todo-composite-view.js

上記のtodo-layout-view.jsの中のlistRegionリージョンの中に表示されるビューです。
CompositeViewを使用しています。

ポイントは、childViewchildViewContaineruiです。

todo-collection-view.jsに記載していた追加ボタンやそのイベントハンドラはここに書きます。

var app = app || {};

//Todo一覧表示用ビュー
(function(app) {
    app.TodoCompositeView = Backbone.Marionette.CompositeView.extend({
        template: '#todo-composite-template',

        childView : app.TodoItemView,

        childViewContainer : 'tbody',

        ui : {
            addTodo : '#addTodo',
            newTodo : '#new-todo'
        },

        events : {
            'click @ui.addTodo' : 'onCreateTodo',
        },

        initialize: function(){
            _.bindAll( this, 'onCreatedSuccess' );
        },

        onCreateTodo : function(e) {
            this.collection.create(this.newAttributes(), {
                  silent:  true ,
                  success: this.onCreatedSuccess
            });
            this.ui.newTodo.val('');
        },

        newAttributes : function() {
            return {
                todo : this.ui.newTodo.val().trim(),
                status : 0
            };
        },

        onCreatedSuccess : function(){
            this.collection.fetch({ reset : true });
        },

    });
})(app);
  • childView変数
    • CompositeViewでは、子ビューとしてItemViewを指定します。素のBackboneでは、子ビューの描画を親ビューで行うコードを記載していましたが、CompositeViewを使用すると、子ビューとなるItemViewを指定するだけでその辺り書かなくてよくなります。
  • childViewContainer変数
    • 子ビューとなるItemViewを描画するコンテナとなる、テンプレート上のHTMLエレメントのIDを指定します。ここでは、tbodyタグを指定しています。
  • ui変数

    • ui変数は、Marionetteで用意されている変数です。ここに、"変数名 : セレクタ"の形式で指定すると、このビュー内では指定した変数名で参照出来るようになります。
      • events変数内でセレクタ指定する場合、@ui.変数名という形式で指定できるようになります。ここでは、'click @ui.addTodo' : 'onCreateTodo'と使用しています。
  • todo-collection-view.jsに記載していた、onCreateTodonewAttributesonCreatedSuccessはここに書きます。

todo-item-view.js

ItemViewを適用しています。
todo-composite-view.js同様、ui変数を使用できます。
テンプレートの描画をItemViewがやってくれますので、描画処理が削除されています。

todo-item-view.js
 //Todo一覧の1件表示用ビュー
 (function(app) {
-   app.TodoItemView = Backbone.View.extend({
+   app.TodoItemView = Backbone.Marionette.ItemView.extend({
        //DOMに要素追加のタグ名
        tagName : 'tr',

        //テンプレート
-       template : _.template($('#item-template').html()),
+       template : '#todo-item-template',
+
+       ui : {
+           checkBox : '.toggle',
+           removeLink : '.remove-link'
+       },

        //DOMイベントハンドラ設定
        events : {
            //チェックボックスクリック時
-           'click .toggle' : 'onStatusToggleClick',
+           'click @ui.checkBox' : 'onStatusToggleClick',
            //削除ボタンクリック時
-           'click .remove-link' : 'onRemoveClick',
+           'click @ui.removeLink' : 'onRemoveClick',
        },

-       initialize : function() {
-           this.listenTo(this.model, 'destroy', this.remove);
-       },
-       render : function() {
-           this.$el.html(this.template(this.model.toJSON()));
-           return this;
-       },
        onStatusToggleClick : function(e) {
            this.model.toggle();
        },

        onRemoveClick : function(e) {
            this.model.destroy({
                wait : true
            });
        },

    });
})(app);
  • ui変数、events変数
    • todo-composite-view.jsと同様に書き換えています。
  • initialize関数, render関数
    • ItemViewが描画処理をやってくれるので、自前の描画処理やViewの破棄処理等は削除しています。

todo-collection-view.js

今回の修正で、ビューのファイル名はMarionetteのビュー名に合わせるようにしました。CompositeViewを使用するようにしますので、
todo-collection-view.js は削除しています。

実装

では、実装しましょう!

  • :white_check_mark: app/View/Layouts/default.ctp を上記の通り修正。
  • :white_check_mark: app/webroot/js/app.js を上記の通り修正。
  • :white_check_mark: app/webroot/js/routers/controller.js を上記の通り修正。
  • :white_check_mark: app/webroot/js/views/todo-layout-view.js を上記の通り作成。
  • :white_check_mark: app/webroot/js/views/todo-composite-view.js上記の通り作成。
  • :white_check_mark: app/webroot/js/views/todo-item-view.jsを上記の通り修正。
  • :white_check_mark: app/webroot/js/views/todo-collection-view.jsを削除。
  • :white_check_mark: 動作確認!
  • :white_check_mark: Gitにコミット

完成ソース(GitHub)へのリンク
- default.ctp
- app.js
- controller.js
- todo-layout-view.js
- todo-composite-view.js
- todo-item-view.js

Lesson3へ!

:large_blue_circle: Lesson 3 TODO詳細画面にMarionetteのビューを適用

TODO詳細画面に、Marionetteのビューを適用して修正します。
Lesson2までの内容でほぼ修正できると思います。

編集するファイル一覧

編集 file 役割
修正 app/View/Layouts/default.ctp HTMLテンプレート
修正 app/webroot/js/routers/controller.js コントローラ
追加 app/webroot/js/views/todo-detail-layout-view.js ビュー(LayoutView:Todo1件の詳細画面のレイアウト用)
追加 app/webroot/js/views/todo-detail-item-view.js ビュー(ItemView:Todo1件表示用)
削除 app/webroot/js/views/todo-detail-view.js ビュー(Todo1件の詳細画面表示用)

default.ctp

詳細画面は元々View一つで実装していましたが、LayoutViewとItemViewに分けます。

default.ctp
    </td>
    </script>

-   <!-- 詳細画面 -->
-   <script type="text/template" id="detail-template">
+   <!-- 詳細画面のレイアウトテンプレート -->
+   <script type="text/template" id="todo-detail-layout-template">
+   <div id="todo-item"></div>
+   </script>
+
+   <!-- 詳細画面の表示内容テンプレート -->
+   <script type="text/template" id="todo-detail-item-template">
    <h2>Todo #<%- id %></h2>
    <div>
    <textarea style="width:300px;height:50px" id="edit-todo" autofocus placeholder="Todo?"><%- todo %></textarea>

〜中略〜

    <script src="js/collections/todo-collection.js" type="text/javascript"></script>
    <!--   view   -->
    <script src="js/views/todo-item-view.js" type="text/javascript"></script>
-   <script src="js/views/todo-detail-view.js" type="text/javascript"></script>
+   <script src="js/views/todo-detail-item-view.js" type="text/javascript"></script>
+   <script src="js/views/todo-detail-layout-view.js" type="text/javascript"></script>
    <script src="js/views/todo-composite-view.js" type="text/javascript"></script>
    <script src="js/views/todo-layout-view.js" type="text/javascript"></script>
    <!--   controller   -->
  • 詳細画面のテンプレートを、LayoutViewで使用する部分とItemViewを使用する部分に分割しています。
    • LayoutView用 -> todo-detail-layout-template
    • ItemView用 -> todo-detail-item-template
  • レイアウト用のビューに、リージョンとする部分のタグを追加し、idをtodo-itemとしています(ここに詳細表示(ItemView)のビューが読み込まれます)。
  • 詳細表示用のビュー(todo-detail-view.js)はtodo-detail-item-view.jsに変更しています。
  • レイアウト用のビュー、js/views/todo-detail-layout-view.jsを追加しています。

controller.js

ビューへのIDの渡し方を変更しています。

controller.js
        },

        todoDetail : function(id) {
-           this.nextView(app.TodoDetailView, id);
+           this.nextView(app.TodoDetailLayoutView, {modelId : id});
        },

        nextView : function(View, option) {
            app.application.mainRegion.show(new View(option));
        },

    });
 })(app);

  • todoDetail関数
    • nextView関数を実行していますが、元々はidをそのまま第二引数に渡していました。修正後は、{modelId : id}と、オブジェクトにして渡しています。これは、View側では元々Initialize関数の引数としてそのまま使用していましたが、他の箇所で使用するためにViewのoptions変数に渡すための修正です。

todo-detail-layout-view.js

レイアウト用ビューです。
ここでの役割は下記です。

  • リージョンの定義
  • モデルの読み込み
    • モデルの読み込みは、元々todo-detail-view.jsで行っていましたが、ここで行うようになります。
todo-detail-layout-view.js
var app = app || {};

//詳細画面用レイアウトビュー
(function(app) {
    app.TodoDetailLayoutView = Backbone.Marionette.LayoutView.extend({
        //テンプレート
        template : '#todo-detail-layout-template',

        regions : {
            itemRegion : '#todo-item',
        },

        onRender : function() {
            var todoModel = new app.TodoModel({
                id : this.options.modelId
            });
            //モデルのサーバからのデータ取得完了時、描画を行う
            this.listenTo(todoModel, 'sync', this.showItem, this);
            //サーバからデータ取得
            todoModel.fetch({
                wait : true
            });
        },

        showItem : function(todoModel) {
            this.itemRegion.show( new app.TodoDetailItemView({
                model : todoModel
            }));
        },

    });
})(app);
  • regions変数
    • リージョンとして#todo-itemを指定しています。
  • onRender関数
    • this.options.modelIdと指定し、controller.js内で指定したモデルのIDを取得しています。
      • Viewの生成時に指定したオブジェクトは、View内で、this.optionsとして参照することが出来ます。
  • データの取得から子ビューの呼び出しの流れは、collectionmodelの違いはありますが、同様に書きます。

todo-detail-item-view.js

ItemViewを適用しています。

todo-detail-item-view.js
var app = app || {};

//詳細ビュー
(function(app) {
    app.TodoDetailItemView = Backbone.Marionette.ItemView.extend({

        //テンプレート
        template: "#todo-detail-item-template",

        ui : {
            todoStatus   : '#edit-todo',
            updateButton : '#updateTodo',
            cancelButton : '#updateCancel'
        },

        //DOMイベントハンドラ設定
        events : {
            //更新ボタンクリック時
            'click @ui.updateButton' : 'onUpdateClick',
            //キャンセルボタンクリック時
            'click @ui.cancelButton' : 'onCancelClick',
        },

        //初期化
        initialize: function(){
            _.bindAll( this, 'onSaveSuccess' );
        },

        //更新ボタンクリックのイベントハンドラ
        onUpdateClick : function() {
            //テキストボックスから文字を取得
            var todoString = this.ui.todoStatus.val();
            this.model.save({
                todo : todoString
            }, {
                silent : true,
                success : this.onSaveSuccess,
            });
        },

        //キャンセルボタンクリックのイベントハンドラ
        onCancelClick : function() {
            this.backTodoLists();
        },

        //更新成功
        onSaveSuccess : function() {
            this.backTodoLists();
        },

        //TODOリスト画面に戻る
        backTodoLists : function() {
            Backbone.history.navigate('#todo-lists', true);
        }

    });
})(app);

修正内容はLesson2とほぼ同様の内容ですので解説は割愛します。

todo-collection-view.js

ビューのファイル名はMarionetteのビュー名に合わせますので、todo-detail-view.js は削除しています。

実装

では、実装しましょう!

  • :white_check_mark: app/View/Layouts/default.ctp を上記の通り修正。
  • :white_check_mark: app/webroot/js/routers/controller.js を上記の通り修正。
  • :white_check_mark: app/webroot/js/views/todo-detail-layout-view.js を上記の通り作成。
  • :white_check_mark: app/webroot/js/views/todo-detail-item-view.js上記の通り作成。
  • :white_check_mark: app/webroot/js/views/todo-detail-view.jsを削除。
  • :white_check_mark: 動作確認!
  • :white_check_mark: Gitにコミット

完成ソース(GitHub)へのリンク
- default.ctp
- controller.js
- todo-detail-layout-view.js
- todo-detail-item-view.js

これで完成です!

:large_blue_circle: お疲れ様でした

お疲れ様でした。うまくいきましたでしょうか。

最後に、今回使用したMarionetteの主な機能をまとめておきます。
理解できているか、チェックしてみましょう。

今回使用したMarionette機能まとめ

最後に、今回使用したMarionetteの主な機能をまとめておきます。
理解できているか、チェックしてみましょう。

  • オブジェクト
    • Marionette.Application
      • スタートポイント。土台となるリージョンを定義する。
    • Marionette.AppRouter
      • ルーティングを行う
    • Marionette.Controller
      • ルーティング後に実行する関数を定義する
    • Marionette.Region
      • ビューを一つ管理する
    • Marionette.LayoutView
      • リージョン内の一番外側のビューとして使用する。リージョンも定義できる
      • テンプレートを使用できる。
    • Marionette.CollectionView
      • ※今回使用していません。CompositeViewからテンプレート機能を除いたものです。
      • 内部にただItemViewを表示するだけ(リストボックス的な表示)の場合等に使用するとよいでしょう。
    • Marionette.CompositeView
      • コレクションの表示に使用する。コレクション内の1件表示用にItemViewを内包する。
      • テンプレートを使用できる。コレクションを操作するためのボタン等(追加ボタン等)を配置するのがパターン。
    • Marionette.ItemView
      • モデルの表示に使用する。
      • テンプレート機能を使用できる。モデル1件を操作するためのボタン(更新ボタン等)等を配置するのがパターン。
  • ビュー内個別機能
    • ui変数
      • ビュー内のHTMLエレメントのセレクタの記述を一箇所に集約する。
      • @ui.〜の形式でビュー内から参照可能となる。
    • regions変数
      • 管理するリージョンを定義する。
    • template変数
      • テンプレートのIDを指定しておくことで、自動的にレンダリングしてくれる。
    • options変数
      • ビューの初期化時に渡されたオブジェクトを参照することが出来る。

今回使用しなかったもの

triggersBehaviors、etc...

いろいろです。調べてみましょう!

:beers:飲みDev:pizza:

今回から、勉強会終了後にお酒を飲みながら居残り開発をする、名づけて「飲みDev」をやります!

テーマを挙げておきます。
下記、いけるところまで行きましょう!
もちろん、他の機能をつけたり、なんでもやってみましょう!

  1. リロードボタンをつける
  2. "完了分を表示しない"チェックボックスをつける
  3. チェックボックスを"Done"ボタンに変更、完了分は色を変える。完了分は"Todo"ボタン表示、押すと未完了に戻る。
  4. なにかおもしろ機能!

いい感じにできたら発表してください!

以上です!

コメント/フィードバックお待ちしております。

参加者の方も、そうでない方もお気づきの点があればお願い致します。