BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル Ember.js - Webアプリケーションを正しく行う

Ember.js - Webアプリケーションを正しく行う

ブックマーク

原文(投稿日:2013/04/17)へのリンク

はじめに

去年、私は "Ember.js – Rich Web Applications done right” という記事を書きました。その記事はEmber.js version 0.9.4をベースとしており、Ember.js自体も、まだかなり若いプロジェクトでした。

以来、このフレームワークは長い道のりを経てきました。そろそろ最新のEmber.js開発プラクティスとその技術、特にEmber Routerを使って記事を更新すべきときでしょう。 

Ember.js APIはEmber.js 1.0 Release Candidate 1のリリースでかなり安定しました。この記事はEmber.jsとEmber Dataの最新のマスターブランチ(2013年3月24日)ビルドに基づいています.

私の著書"Ember.js in Action"が、今年の下期にManning Publicationsから出る予定です。すでにアーリーアクセス版が出ていて、最初の4章分が読めるようになっています。リリースに向けて、順次新しい章を追加していく予定です。

さて、Ember.jsはどのように改良されてきたのでしょうか? Ember.jsは単一ページWebアプリを開発するためのフレームワークとしてまだふさわしいのでしょうか? 

この記事では、これら2つの質問に答えていきます。やらなきゃいけないことはたくさんあります。早速、はじめましょう。なお、これから構築するアプリケーションのソースコードは、 GitHub に置いてあります。

何を作るか?

シンプルなフォトアルバムアプリを構築していきましょう。ページの下部には、写真のサムネールを横に並べます。ユーザが写真を選択するとURLが更新され、選択された写真がサムネールの並びの上に表示されます。

シンプルなスライドショー機能も用意しましょう。4秒毎に次の写真に切り替えます。

最終的なアプリケーションは図1のようになります。

(クリックすると拡大)

GitHubに最終的なソースコードが置いてあります。アプリケーション本体はそのsiteディレクトリにあります。アプリケーションを開始する方法にはいろいろありますが、シンプルなのは以下のやり方です。

図1 – 最終的なアプリケーション

白紙の状態からはじめて、アプリケーションの機能を1つずつ構築していきましょう。まずは、アプリケーションのセットアップからです。

プロジェクト構造とセットアップ

 

  • asdf-gemを使ってカレントディレクトリをホストする。詳しくはGitHubを参照。gemをインストールして、siteディレクトリでコマンド“asdf”を実行する。
  • 別のWebサーバを使って、アプリケーションをhttp://localhost/としてホストする。

サンプルプロジェクトは図2のような構造になっています。

 

図2 – プロジェクト構造

外部ライブラリはすべて、site/scriptsディレクトリに置きます。必要なライブラリは以下の通りです。

  • Ember.js 2013年3月24日ビルド
  • Ember Data 2013年2月16日ビルド
  • Handlebars.js 1.0 RC 3
  • JQuery 1.9.1

imgディレクトリには10枚の画像ファイルがあり、これがフォトアルバムとして使われます。cssディレクトリにはmaster.cssというCSSファイルがあります。

アプリケーションロジックはappディレクトリのいくつかのファイルに分かれています。まずはapp.jsファイルからはじめましょう。以後、アプリケーションに追加のファイルが必要になるたび、説明を加えていきます。

バインディングの簡単な紹介

Ember.jsでは、オブジェクト間の変数の値を同期するのにバインディングが使われます。 リスト1のコードについて考えましょう。

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd">

<html lang="en">
<head>
     <title>Ember.js Example Bindings</title>
     <link rel="stylesheet" href="css/master.css" type="text/css" charset="utf-8">
     <script src="scripts/jquery-1.9.1.min.js" type="text/javascript" charset="utf-8"></script>
     <script src="scripts/handlebars-1.0.0.rc.3.js" type="text/javascript" charset="utf-8"></script>
     <script src="scripts/ember.prod.20130324.js" type="text/javascript" charset="utf-8"></script>
     <script type="text/javascript">
         BindingsExample = Ember.Application.create();

         BindingsExample.person = Ember.Object.create({
             name: 'Joachim Haagen Skeie'
         });

         BindingsExample.car = Ember.Object.create({
             ownerBinding: 'BindingsExample.person.name'
         });
     </script>

     <script type="text/x-handlebars">
         <div id="mainArea">{{BindingsExample.car.owner}}</div>
     </script>

</head>
<body bgcolor="#555154">

</body>
</html>

リスト1 - シンプルなバインディングの例

上のコードでは、jQuery、Handlebars、Ember.jsといったJavaScriptライブラリをインクルードする前に、まずCSSファイルをロードしています。アプリケーション本体は、10行目から25行目に定義されています。アプリケーションはその名前空間、BindingsExampleの定義からはじまっています。そして、BindingsExample.personBindingsExample.carという2つのオブジェクトを定義しています。 Ember.jsの命名規則に従って、インスタンス化したオブジェクトの名前は小文字ではじめます。

BindingsExample.personオブジェクトのnameプロパティに文字列“Joachim Haagen Skeie”をセットしています。carオブジェクトには、ownerBindingというプロパティがあります。プロパティ名が “Binding”で終わっているため、Ember.jsは自動的に“owner” プロパティを作ります。このプロパティはBindingsExample.person.nameプロパティの内容にバインドされます。

ブラウザでこのHTMLファイルをロードすると、BindingsExample.car.ownerプロパティの値、“Joachim Haagen Skeie”が出力されます。

このやり方が美しいのは、BindingsExample.person.nameプロパティが変わると、Webサイトの内容も更新されるところです。ブラウザのJavaScriptコンソールに以下を入力して、Webサイトの内容が変わることを確かめてみましょう。

BindingsExample.person.set('name', 'Some random dude')

Webページの内容は “Some random dude”になります。このことを頭に入れて、サンプルアプリケーションを立ち上げていきましょう。

Ember.jsアプリケーションの立ち上げ

どんなサンプルアプリケーションを構築するか感触を掴むため、最終的なアプリケーションを見てみましょう。

では、空のindex.htmlファイルを作って、app、css、img、scriptsというディレクトリを追加しましょう。そして以下のファイルを作るかコピーしましょう。

  • appディレクトリに、app.jsという名前のファイルを作る。
  • cssディレクトリに、このファイルをコピーする。
  • scriptsディレクトリに、Githubにあるファイルをコピーする。

index.htmlファイルからはじめましょう。リスト2にあるように、スクリプトとCSSファイルを参照するだけです。

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">

    <title>Ember.js Example Application</title>
    <link rel="stylesheet" href="css/master.css" type="text/css" charset="utf-8">
    <meta name="author" content="Joachim Haagen Skeie">
    <script src="scripts/jquery-1.9.1.min.js" type="text/javascript" charset="utf-8"></script>
    <script src="scripts/handlebars-1.0.0.rc.3.js" type="text/javascript" charset="utf-8"></script>
    <script src="scripts/ember.prod.20130324.js" type="text/javascript" charset="utf-8"></script>
    <script src="scripts/ember-data-20130216.js" type="text/javascript" charset="utf-8"></script>
    <script src="app/app.js" type="text/javascript" charset="utf-8"></script>

    <script type="text/x-handlebars">

    </script>
</head>
<body bgcolor="#555154">

</body>
</html>

リスト2 – 最初のindex.htmlファイル

最初にすべきことは、アプリケーションの名前空間をapp.jsに作ることです。私はEME (EMber-Example) という名前空間を作ることにしました。app.jsファイルに以下を加えます。

EME = Ember.Application.create({});

リスト3 – Ember-Example namespace (EME)

アプリケーションのオブジェクトモデルを定義する前に、CSSファイルにいくつか追加しておきましょう。背景を白色に、ボーダーを角丸にして、スクリーン全体をメインエリアで埋めることにします。css/master.cssファイルに以下を追加します。

#mainArea {
    border-radius: 25px;
    -moz-bottomleft: 25px;
    padding: 15px;
    margin-top: 5px;
    background: #fff;
    text-align: left;
    position: absolute;
    top: 5px;
    left: 5px;
    right: 5px;
    bottom: 5px;
    z-index:1;
}

リスト4 – アプリケーションのmainAreaのCSS

メインエリアを表示するために、index.htmlにリスト5を追加します。

    

リスト5 – アプリケーションを保持するdiv要素の追加

ページをリフレッシしましょう。画面全体に角丸の白いエリアが表示されるはずです。ここまで準備できたら、いよいよモデルを定義していきましょう。

Ember Dataによるオブジェクトモデルの定義

まず、index.htmlファイルでインクルードしているscriptsのリストに、ember-dataを追加する必要があります(まだやっていなければ)。

最初にデータストアを初期化する必要があります。app/app.jsを開いて、リスト6にある行を追加しましょう。

EME.store = DS.Store.create({
    adapter: "DS.RESTAdapter",
    revision: 11
});

リスト6 – データストアの作成

ここではEME.storeという新しいDS.Storeオブジェクトを作っています。データストアには2つの情報を教える必要があります。1つはEmber Dataリビジョン番号、もう1つは使用するアダプタです。リビジョン番号は、Ember Dataに破壊的な変更があるのを知るためにあります。Ember Dataはまだ未完成です。安定した1.0 APIに達しておらず、最終リリースに向けてAPIが進化するにつれ、破壊的な変更が時々発生するためです。また、バックエンドとのインターフェイスには、DS.RESTAdapterを使うことにします。

このアプリケーションに必要なのは、1種類のデータ、写真だけです。これには、id、title、URLという3つのプロパティがあります。DS.Modelを拡張することで、写真のデータモデルを定義します。models/photo_model.jsファイルを作成して、リスト7にあるコードを追加しましょう。

EME.Photo = DS.Model.extend({
    imageTitle: DS.attr('string'),
    imageUrl: DS.attr('string')
});

リスト7 – 写真のモデルオブジェクト

"id"プロパティを定義していないことに気づいたかもしれません。これはEmber Dataが作ってくれるためです。実のところ、自分で指定することは許されていません。ここでは、"imageTitle"と"imageUrl"という2つのプロパティを定義しています。いずれもDS.attr('string')によって、文字列として定義されています。

では少し離れて、アプリケーションの構造をどうするか、アプリケーションのURLをどう定義するかを考えましょう。

Ember Routerによるアプリケーションの構造化

以前の記事では、Ember StateManagerを使って、アプリケーションの状態を構造化しました。その後、StateManagerはEmber Routerという実装に置き換えられました。同じ目的で使われますが、Ember Routerはデフォルトで実行時に多数のボイラープレートコードを自動生成してくれます。これらは必要に応じて、簡単に上書きすることができます。

アプリケーションを定義するためにまず必要なことは、それを組み立てるルートです。ルートをユーザの状態だと考えても構いません。各ルートには明確にURLが定義されています。Ember.jsはコントローラ、ビュー、テンプレートを自動生成します。事前に定義された基本機能では足らない場合には、独自の実装を作ることができます。Ember.jsは自動生成したコードをあなたの書いたコードに置き換えます。

フォトアルバムの場合、全部で3つのルートが必要になります。

  • Index Route、“/” URLに対応
  • Photos Route、“/photos” URLに対応
  • Selected Photo Route、“/photos/:photo_id” ルートに対応

先ほど述べたように、これらはEmber.jsが生成してくれるのですが、そのためにはまず、アプリケーションが必要としているルートをEmber.jsに教える必要があります。リスト8のようにEME.Router.map()を実装しましょう。router.jsという新しいファイルを作って、次のコードを追加します。

EME.Router.map(function() {
    this.route("index", {path: "/"});
    this.resource("photos", {path: "/photos"}, function() {
        this.route("selectedPhoto", {path: ":photo_id"})
    });
});

リスト8 – アプリケーションルートの定義

map()関数内には、2種類の関数が使われています。route()は最終ルートを表現しており、resource()はサブルートを含むルートを表現しています。ここでは2つのトップレベルルート、1つは “/” URLに対応するindexというルート、もう1つは“/photos” URLに対応するphotosというリソースを定義しています。さらには、“/photos/:photo_id” URLに対応するselectedPhotoというサブルートを作っています。Ember Routerでは、コロンを前に付けたURL属性を使うことで動的部分を表します。アプリケーションURLの更新時に、Ember Routerはこの部分を動的に置き換えます。

Ember Routerの命名規則により、:photo_idはEME.Photoモデルオブジェクトの"id"プロパティを参照していることに注意しましょう。

indexルート内に表示するものを持ちたくないため、ユーザが直接“/” URLをリクエストした場合には、indexルートからphotosルートにリダイレクトします。これはリスト9にあるようにEME.IndexRouteクラスを作ることで実現できます。これをrouter.jsファイルに追加しましょう。

EME.IndexRoute = Ember.Route.extend({
    redirect: function() {
        this.transitionTo('photos');
    }
});

リスト9 – IndexからPhotosルートへのリダイレクト

ここではrouteのリダイレクト関数、transitionTo()関数を使ってphotosルートにリダイレクトしています。

Ember Dataを使ったバックエンドからの写真の取得

写真を見せるためにルートに必要な最後の部分は、どのデータを表示するかphotosルートに教えることです。以下のコードを使ってrouter.jsを拡張しましょう。

EME.PhotosRoute = Ember.Route.extend({
    model: function() {
        return EME.Photo.find();
    }
});

リスト9 – IndexからPhotosルートへのリダイレクト

上のコードでは、Ember Dataに任せて、サーバからEME.Photoオブジェクトを非同期に取得しています。サーバから応答が返ってくると、EME.PhotosRouteはPhotosControllerを自動生成し、データを入れてくれます。EME.Photo.find(1)にidを指定することで、サーバから特定の写真を問い合わせることもできますが、今回の場合はすべての写真を取得するので十分です。

モデルクラスをEME.Photosという名前にしたため、Ember DataはデフォルトでこのモデルのデータをURL “/photos”から探します。その内容はリスト10のようになります。

{ "photos": [
    { "id": 1, "image_title": "Bird", "image_url": "img/bird.jpg"},
    { "id": "2", "image_title": "Dragonfly", "image_url": "img/dragonfly.jpg"},
    { "id": "3", "image_title": "Fly", "image_url": "img/fly.jpg"},
    { "id": "4", "image_title": "Frog", "image_url": "img/frog.jpg"},
    { "id": "5", "image_title": "Lizard", "image_url": "img/lizard.jpg"},
    { "id": "6", "image_title": "Mountain 1", "image_url": "img/mountain.jpg"},
    { "id": "7", "image_title": "Mountain 2", "image_url": "img/mountain2.jpg"},
    { "id": "8", "image_title": "Panorama", "image_url": "img/panorama.jpg"},
    { "id": "9", "image_title": "Sheep", "image_url": "img/sheep.jpg"},
    { "id": "10", "image_title": "Waterfall", "image_url": "img/waterfall.jpg"}
]}

リスト10 – サンプルのフォトデータ

さて、写真をロードできたら、実際にそれを表示するコードが必要になります。アプリケーションにテンプレートを定義していきましょう。

アプリケーションテンプレートの作成

Ember.jsはデフォルトのテンプレートエンジンとしてHandlebars.jsを使っています。ここでは全部で4つのテンプレートを用意します。すでにリスト5でアプリケーションテンプレートを見ましたが、これには重要なところが欠けています。それは現在のルートのテンプレートを描画するアウトレットです。リスト11のように、アプリケーションテンプレートを拡張することからはじめましょう。

<script type="text/x-handlebars">
    <div id="mainArea">
        {{outlet}}
    </div>
</script>

リスト11 – アウトレットを使ったアプリケーションテンプレートの拡張

アウトレットは{{outlet}}という表現で定義され、Ember.jsにサブテンプレートを描画する場所を教えます。テンプレートの内容は現在閲覧されているルートによって変わります。indexルートの場合、このアウトレットにはindexテンプレートが含まれ、photosリソースの場合、photosテンプレートが含まれます。

今回は写真のサムネールリストを描画するところなので、リスト12のようなphotosテンプレートを定義しましょう。

   <script type="text/x-handlebars" id="photos">
       {{outlet}}

       <div class="thumbnailViewList">
           {{#each photo in controller}}
           <div class="thumbnailItem">
               {{#linkTo photos.selectedPhoto photo}}
                   {{view EME.PhotoThumbnailView
                        srcBinding="photo.imageUrl"
                        contentBinding="photo"}}
                   {{/linkTo}}
            </div>
            {{/each}}
       </div>
    </script>

リスト12 – photosテンプレートの定義

ここには注意すべきことがいくつかあります。1つはtext/x-handlebars scriptタグにある"id"属性です。このid属性によって、Ember.jsにテンプレート名を教えます。このテンプレート名、photosは、このテンプレートがPhotosRouteに属することをEmber.jsに教えます。標準の命名規則に従っている限り、あとはEmber.jsがうまく面倒をみてくれます。

このテンプレートで最初に定義しているのは、選択されている写真を描画する{{outlet}}です。それから、各サムネールを描画するdiv要素を定義しています。ここでは{{#each photo in controller}}を使って、PhotosControllerにロードされた写真をイテレートしています。ループ内では{{#linkTo ..}}を使って、各写真をselectedPhotoルートにリンクしています。リンクすべきルート(parent-route.sub.route経由)だけでなく、ルートに渡すコンテキストも指定していることに注意しましょう。この場合、コンテキストはユーザがクリックする写真になります。

各サムネールを表示するためのカスタムビューを作りましょう。リスト13のような内容で、views/photo_thumbnail_view.jsファイルを作ります。

EME.PhotoThumbnailView = Ember.View.extend({
    tagName: 'img',
    attributeBindings: ['src'],
    classNames: ['thumbnailItem'],
    classNameBindings: 'isSelected',

    isSelected: function() {
        return this.get('content.id') === 
this.get('controller.controllers.photosSelectedPhoto.content.id');
    }.property('controller.controllers.photosSelectedPhoto.content', 'content')
});

リスト13 – サムネールのためのカスタムビューの作成

PhotoThumbnailViewではまず、このビューがtagNameプロパティ経由でimgタグとして表示されることを指定しています。そしてsrc属性とバインドし、このタグのCSSクラスであるthumbnailItemItを指定しています。

どの写真が選択されているかがわかるよう、選択された写真を他の写真よりも大きくしましょう。classNameBindingsプロパティを使って、selectedプロパティにカスタムのis-selected CSSクラスを追加します。isSelected computed propertyが真を返す場合(ビューの写真idが選択された写真idと同じ場合)、CSSクラスis-selectedをビューに追加します。

あと残っているテンプレートは、選択された写真の表示だけです。このルートはphotosリソースのサブルートなので、photos/selectedPhotoというidになります。selectedPhotoテンプレートをリスト14に示します。

<script type="text/x-handlebars" id="photos/selectedPhoto">
    <div id="selectedPhoto">
        <h1>{{imageTitle}}</h1>

        <div class="selectedPhotoItem">
            <img id="selectedImage" {{bindAttr src="imageUrl"}}/>
        </div>
    </div>
</script>

リスト14 – 選択された写真のテンプレート

このテンプレートに新しいところはありません。選択された写真を置くためのdiv要素を表示するだけです。このテンプレートではタイトルと写真を表示します。

アプリケーションをリロードしましょう。結果は図3のようになります。

(クリックすると拡大)

図3 – ここまでのフォトアルバム

選択されている写真はハイライトされておらず、ほかのサムネールよりも大きくなっていないことに気づくかもしれません。それには、photosルートとselectedPhotosルートのためのコントローラを指定する必要があります。さらに、PhotoThumbnailViewがisSelected computed propertyを正しく解決できるよう、コントローラを関連付ける必要があります。それでは2つのコントローラを実装していきましょう。

コントローラの定義

photosリソースとselectedPhotoルートのために、カスタマイズしたコントローラを定義しましょう。さらに、photosコントローラをselectedPhotoコントローラと関連付ける必要があります。リスト15のようなcontrollers/photos_controller.jsファイルを作りましょう。

EME.PhotosController = Ember.ArrayController.extend({
    needs: ['photosSelectedPhoto'],
});

リスト15 – PhotosController

この時点では、コントローラ内部にはほとんどなにもありません。コントローラをEmber.ArrayControllerとして定義したので、写真リストのテンプレートへのプロキシやバインディング、その他コントローラ関係の処理は、Ember.jsが面倒を見てくれます。指定しているのは、このコントローラがphotosSelectedPhotoControllerを必要とすることだけです。ここでコントローラ間の明示的な関係を指定したので、それが指しているコントローラも作る必要があります。リスト16のようなcontrollers/selected_photo_controller.jsファイルを作りましょう。

EME.PhotosSelectedPhotoController = Ember.ObjectController.extend({});

リスト16 – PhotosSelectedPhotoController

このコントローラのカスタムロジックには何も実装していません。コントローラがEmber.ObjectControllerであることを指定して、宣言しているだけです。これは1つの写真のためのプロキシとして機能することを意味します。

アプリケーションをリフレッシュすると、図4にあるように、選択された写真が期待通りにハイライトされます。

(クリックすると拡大)

図4 – コントローラ追加後

あと残っているのは、ユーザによる写真の前後送り、スライドショーの開始停止のためのコントロールボタンです。

コントロールボタンとスライドショー機能の追加

選択された写真の下に、写真選択をコントロールするためのボタンを4つ追加します。“Play”ボタンはスライドショーを開始し、選択される写真を4秒毎に切り替えます。“Stop”ボタンは開始したスライドショーを停止します。“Next”と“Prev”ボタンはそれぞれ前後の写真を選択します。リスト17にあるように、まずはコントロールボタンを保持するphotoControlsというテンプレートを追加しましょう。

<script type="text/x-handlebars" id="photoControls">
    <div class="controlButtons">
        <button {{action playSlideshow}}>Play</button>
        <button {{action stopSlideshow}}>Stop</button>
        <button {{action prevPhoto}}>Prev</button>
        <button {{action nextPhoto}}>Next</button>
   </div>
</script> 

リスト17 – 選択された写真のテンプレート

このテンプレートは4つのボタンを生成しています。そして、各ボタンに適切なアクションをアタッチしています。これらのアクション関数は後で実装することにします。まずはこのphotoControlsテンプレートをどこに表示するか、アプリケーションに教えましょう。このテンプレートは選択した写真のすぐ下に表示したいため、リスト18にあるようにphotosテンプレートの{{outlet}}のすぐ後に指定します。

<script type="text/x-handlebars" id="photos">
        <div>Disclamer: The photographs used for this example application is under 
Copyright to Joachim Haagen Skeie.
            You are allowed to use the photographs while going through this example 
application. The source code itself
            is released under the MIT licence.
        </div>
        {{outlet}}
        {{render photoControls}} 
        <div class="thumbnailViewList">
            {{#each photo in controller}}
            <div class="thumbnailItem">
                {{#linkTo photos.selectedPhoto photo}}
                    {{view EME.PhotoThumbnailView
                        srcBinding="photo.imageUrl"
                        contentBinding="photo"}}
                    {{/linkTo}}
            </div>
            {{/each}}
        </div>
    </script> 

リスト18 – アプリケーションへのphotoControlsの追加

次に、4つのボタンのためのアクション関数を定義する必要があります。{{render}}表現を使っているため、私たちはPhotoControlsコントローラを生成して、photoControlsテンプレートのコントローラとして自動的に使わせることができます。リスト19のような controllers/photo_controls_controller.jsファイルを作りましょう。

EME.PhotoControlsController = Ember.Controller.extend({
    needs: ['photos', 'photosSelectedPhoto'],

    playSlideshow: function() {
        console.log('playSlideshow');
    },

    stopSlideshow: function() {
        console.log('stopSlideshow');
    },

    nextPhoto: function() {
        console.log('nextPhoto');
    },
    prevPhoto: function() {
        console.log('prevPhoto');
    }
}); 

リスト19 – イベントをハンドルするPhotoControlsControllerの追加

アプリケーションをリロードして、ブラウザコンソールを立ち上げると、ボタンをクリックするたびに、新たな行がコンソールに記録されるはずです。

このコントローラはphotosControllerとphotosSelectedPhotoを必要としている、と指定していたことに注意しましょう。こうしたのは、これら関数の内容をこのコントローラ内に実装するのではなく、ふさわしいコントローラに処理を委譲したかったためです。nextPhotoとprevPhotoはどちらもPhotosControllerにある方がふさわしいため、リスト20にあるように、このアクションをPhotosControllerに委譲するよう変更しましょう。

nextPhoto: function() {
    console.log('nextPhoto');
    this.get('controllers.photos').nextPhoto();
},

prevPhoto: function() {<
    console.log('prevPhoto');
    this.get('controllers.photos').prevPhoto();
}

リスト19 – PhotosControllerへのイベント委譲

これは処理をPhotosControllerの関数に委譲しているだけです。関数の実装を詳しく見る前に、このコントローラを仕上げましょう。リスト20にあるようにスライドショー機能を実装します。

    playSlideshow: function() {
        console.log('playSlideshow');
        var controller = this;
        controller.nextPhoto();
        this.set('slideshowTimerId', setInterval(function() {
            Ember.run(function() {
                controller.nextPhoto();
            });
        }, 4000));
    },

    stopSlideshow: function() {
        console.log('stopSlideshow');
        clearInterval(this.get('slideshowTimerId'));
        this.set('slideshowTimerId', null);
    }

リスト20 – スライドショー機能の追加

ここで注意すべきことがいくつかあります。ユーザがPlayボタンをクリックすると、playSlideshow()関数がすぐに、次の写真を選択するためにnextPhoto()関数を呼び出します。それからJavaScriptに組み込まれているsetInterval()関数を使ってタイマーを開始します。この関数が返すidは、後でタイマーをクリアするのに使います。そのため、このidをPhotoControlsControllersのslideshowTimerIdプロパティに保持しておきます。

私たちはタイマーがコールバック関数を実行するタイミングをコントロールしていないため、また、コールバックをEmber.jsの実行ループの一部として実行するため、コールバックの内容をEmber.run()にラップする必要があります。

この時点でアプリケーションに欠けているのは、nextPhotoとprevPhoto関数の実装だけです。ここではnextPhotoの実装だけを示すことにします。というのも実装はほぼ同じだからです。リスト21にnextPhotoの実装を示します。

    nextPhoto: function() {
        var selectedPhoto = null;
        if (!this.get('controllers.photosSelectedPhoto.content')) {
            this.transitionToRoute("photos.selectedPhoto", 
                this.get('content.firstObject'));
        } else {
            var selectedIndex = this.findSelectedItemIndex();

            if (selectedIndex >= (this.get('content.length') - 1)) {
                selectedIndex = 0;
            } else {
                selectedIndex++;
            }

            this.transitionToRoute("photos.selectedPhoto", 
               this.get('content').objectAt(selectedIndex))
        }
   }

リスト21 –The nextPhoto() function

選択されている写真がない場合、関数はリストの最初の写真を表示します。これはcontent.firstObjectプロパティを使ってphotos.selectedPhotoルートに遷移することで実現できます。

写真が選択されている場合には、選択されている写真がcontent配列のどのインデックスにあるかを見つける必要があります。これはfindSelectedItemIndex()関数を呼び出すことで実現できます。リスト22にそれを示します。

現在選択されている写真のインデックスがわかると、選択されているインデックスをインクリメントする必要があります。ただし、最後の写真が選択されている場合は例外です。このときにはcontent配列の最初の写真を選択する必要があります。

遷移したい写真のインデックスが決まれば、その写真をコンテキストとして使って、photos.selectedPhotoに遷移するだけです。

    findSelectedItemIndex: function() {
        var content = this.get('content');
        var selectedPhoto = this.get('controllers.photosSelectedPhoto.content');

        for (index = 0; index < content.get('length'); index++) {
            if (this.get('controllers.photosSelectedPhoto.content') === 
content.objectAt(index)) {
                return index;
            }
        }

        return 0;
    }

リスト22 – findSelectedItemIndex()関数

findSelectedItemIndex()のコードには驚くようなところはないはずです。選択した写真が見つかるまでcontent配列をイテレートしているだけです。写真が見つからない場合には、content配列にある最初の写真のインデックスを返します。

私は通常、自分のアダプタをapp.jsファイルには追加せずに、それ専用の.jsファイルに追加するようにしています。こうしておけば、後で実装を変えたいときに、アダプタのコードを見つけやすいためです。

まとめ

Ember.jsは、クリーンで一貫性のあるアプリケーション開発モデルを提供します。理解しやすく、作成と更新が簡単な独自のテンプレートビューを作るのはとても簡単です。一貫性のあるやり方でバインディングとcomputed propertyを結びつけることで、Ember.jsはWebフレームワークに必要なボイラープレートコードを提供してくれます。使われている技術、DOMツリーの更新方法が非常に明確なので、サードパーティ製アドオンやプラグインを追加するのも簡単です。

デフォルトで提供されるものでは不十分な場合も、Ember.jsの命名規則に従うことで、クラスを指定して実装するだけで済みます。この結果、テストしやすく、メンテナンスしやすいクリーンなコードになります。

この記事がEmber.jsの開発モデルに対する理解を深め、次のプロジェクトでEmber.jsをどう利用できるか明確なイメージを描く助けになれば幸いです。 

この記事で使われている概念や構成要素はすべて、"Ember.js in Action"で詳しく説明しています。この本は現在、Manning Publicationsのアーリーアクセスプログラムで入手できます。つまり、すぐにでもスタートできるということです。 

著者について

Joachim Haagen Skeie氏: ノルウェーのオスロにあるHaagen Software ASのオーナー。独立系コンサルタント、コースインストラクターに従事。アプリケーションプロファイルとオープンソースソフトウェアに強い関心がある。現在、彼の会社はEurekaJ Application Monitoring Toolの立ち上げに奔走している。コンタクトはTwitterアカウントやメールjoachim (at) haagen-software.noまで。ヨーロッパにおけるEmber.js開発者との集まりに関心があるなら、8月にドイツのミュンヘンで開かれるミニカンファレンス、Ember Festに参加するとよいだろう。Joachim氏がその運営チームを引っ張っている。

この記事に星をつける

おすすめ度
スタイル

BT