前回はURLと画面表示の基礎を解説しました。今回はEmber.
Ember.
サイドメニューをクリックしてメインコンテンツを切り替えるという挙動を考えた場合、
さて、
前準備
まずはEmber.
jsを使うために必要なライブラリをダウンロードします。 次の
index.
、html app.
を作成します。js index.
html <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>実践入門 Ember.
js </title> <link rel="stylesheet" href="style.css" > <script src="libs/jquery-2. ></script> <script src="libs/1.3. min. js" handlebars-v2. ></script> <script src="libs/0.0. js" ember. ></script> <script src="app.js" js" ></script> </head> <body> </body> </html>app.
js App = Ember.Application.create();
また、
style.
という名前で、css 中身は空のCSSファイルを作成してください。 これらを以下のディレクトリ構成で配置します。
. ├── app.
js ├── index. html ├── libs │ ├── ember. js │ ├── handlebars-v2. 0.0. js │ └── jquery-2. 1.3. min. js └── style. css
以降、app.
に、index.
のbodyタグの中に、style.
に記述することにします。
Routing
前回の解説では、Route
に対応していると解説しました。では、
実は、Route
を階層化することができ、
まずはルーティングを定義しておきましょう。
App.Router.map(function() {
this.resource('posts', function() {
this.route('show', {path: '/:post_id'});
});
});
あわせてテンプレートを定義しておきます。
<script type="text/x-handlebars">
<header>
<h1>Ember.js ブログ</h1>
</header>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="index">
<div id="hello-message">ようこそ</div>
{{link-to "記事を見る" "posts"}}
</script>
<script type="text/x-handlebars" data-template-name="posts">
<aside id="sidebar">
<ul>
{{#each post in model}}
<li>{{link-to post.title "posts.show" post}}</li>
{{/each}}
</ul>
</aside>
<main>
{{outlet}}
</main>
</script>
<script type="text/x-handlebars" data-template-name="posts/index">
記事を選択してください。
</script>
<script type="text/x-handlebars" data-template-name="posts/show">
<h2>{{model.title}}</h2>
<pre>
{{model.body}}
</pre>
</script>
また、
// データ
var posts = [{
id: 1,
title: 'はじめての Ember.js',
body: 'これから Ember.js を始めようという方向けの記事です。'
}, {
id: 2,
title: '公式サイトの歩き方',
body: 'http://emberjs.com/ の解説です。'
}, {
id: 3,
title: '2.0 のロードマップ',
body: 'Ember.js 2.0 のロードマップはこちらで公開されています。https://github.com/emberjs/rfcs/pull/15'
}];
// Route 定義
App.PostsRoute = Ember.Route.extend({
model: function() {
return posts;
}
});
App.PostsShowRoute = Ember.Route.extend({
model: function(params) {
var id = Number(params.post_id);
var posts = this.modelFor('posts');
return posts.filter(function(post) {
return post.id === id;
})[0];
}
});
見栄えを整えるために、
html, body {
margin: 0;
color: #444;
}
header {
padding: 0 10px;
background-color: #e15e45;
color: #fcfcfc;
text-shadow: rgba(0, 0, 0, 0.8) 0 1px 0;
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.2) inset;
}
header h1 {
margin: 0;
}
#sidebar {
float: left;
width: 200px;
height: 300px;
color: #ba6051;
text-shadow: 0 1px #faeeec;
border: 1px solid #ccc;
}
#sidebar ul {
padding: 0;
margin: 0;
list-style: none;
}
#sidebar li {
padding: 5px 10px;
}
#sidebar li a {
display: inline-block;
height: 25px;
width: 100%;
color: #ba6051;
text-shadow: rgba(0, 0, 0, 0.8) 0 1px 0;
padding: 10px 2px;
text-decoration: none;
}
#sidebar li .active {
background-color: #eae0e1;
}
main {
margin-left: 200px;
height: 300px;
padding-left: 10px;
background-color: #faf2f1;
border-top: 1px solid #ccc;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
}
main ul {
padding: 0;
margin: 0;
list-style: none;
}
では、
いくつか自動で生成されるRouteがありますが、

これらのRouteについて詳しく見ていきます。
PostsRoute
:/#/posts
で始まるURLにアクセスされた際にアクティブになります。記事一覧を表示します。以下2つのRouteの親です。PostsIndexRoute
:/#/posts
にアクセスされた際にアクティブになります。まだ記事が選択されていない状態なので、「記事を選択してください。」 というメッセージを表示します。 PostsShowRoute
:/#/posts/:post_
にアクセスされた際にアクティブになります。一件の記事を表示します。id
特定のRouteがアクティブになる際、
/#/posts
にアクセスされた場合対応するRouteは
PostsIndexRoute
なので、まずはその親の PostsRoute
がアクティブになりテンプレートを描画します。次に、
PostsIndexRoute
がアクティブになり、親である PostsRoute
のテンプレート中のoutlet
にPostsIndexRoute
自身のテンプレートを描画します。/#/posts/:post_
にアクセスされた場合id 対応するRouteは
PostsShowRoute
なので、まずはその親の PostsRoute
がアクティブになりテンプレートを描画します。次に、
PostsShowRoute
がアクティブになり、親である PostsRoute
のテンプレート中のoutlet
にPostsShowRoute
自身のテンプレートを描画します。
前回の記事で、application
テンプレートでoutlet
を使うと解説しましたが、ApplicationRoute
に行き着くのです。

ここで作成したアプリケーションを実際に動かしてみて、
さて、
Route / Resource
まずはRouterで記述可能なroute()
とresource()
について解説します。
さきほどのルーティングの例ではreoute()
とresource()
を使っていました。これらは似たような機能を持つメソッドなのですが、 実はURLに対応させるRoute名に違いがあります。
route()
だと階層化した際に親Routeの名前を引き継ぐのに対し、resource()
では親の有無に関係なくRoute名が決まります。
reoute()
とresource()
の違いApp.Router.map(function() {
this.resource('hi', function() {
this.resource('hoi'); // HoiRoute -> 親の Route 名 Hi を受け継ぎません
this.route('ho'); // HiHoRoute -> 親の Route 名 Hi を受け継ぎます
});
});
では、route()
とresource()
はどうやって使い分ければよいのでしょうか?
公式ガイドによると、resource()
を、route()
を使うべきであると記述されています。
これにしたがって今回のサンプルコードでは、
- 「
posts
(記事)」(名詞)には resource()
- 「
show
(表示する)」(動詞)には route()
Nested Route
Router.
でresource()
の入れ子としてroute()
を呼び出しています。こうして入れ子にすることで、
ポイントは次のとおりです。
Route はいくつでも階層化することができます。
また、
route()
/resource()
を任意の組み合わせで入れ子にすることができます。テンプレート名(
data-template-name
) は 親子関係にある Route 名を/
で区切った名前が対応します。this.
では、modelFor('Route 名') 親の Route の model()
を参照できます。同一の親 Route 以下での画面遷移であれば
model()
の呼び出し結果は再計算されないので、高速に画面を描画することができます。
実は今回扱うアプリケーションは、
- ブログ記事一覧を表示できる
- ブログ記事の詳細を表示できる
大きく違うのは、
それは、
Active
Routeがアクティブになっているとき、link-to
ヘルパが生成したactive
というCSSのクラスが自動で付与されます。このactive
クラスにスタイルをあてることで、
今回のサンプルでもそのようなCSSを利用しています。
#sidebar li .active {
background-color: #eae0e1;
}
また、active
というクラス名はカスタマイズ可能です。次のようにactiveClass
オプションを利用して、
{{link-to post.title "posts.show" post activeClass="current"}}
上記の例では、PostsShowRoute
がアクティブなときにはcurrent
というクラスが付与されます。
Loading Data
さて、
さきほど用意した手元のデータを、
[{
"id": 1,
"title": "はじめての Ember.js",
"body": "これから Ember.js を始めようという方向けの記事です。"
}, {
"id": 2,
"title": "公式サイトの歩き方",
"body": "http://emberjs.com/ の解説です。"
}, {
"id": 3,
"title": "2.0 のロードマップ",
"body": "Ember.js 2.0 のロードマップはこちらで公開されています。https://github.com/emberjs/rfcs/pull/15"
}]
これからこのJSONをXHRで取得するようコードを書き換えます。XHRではhttpもしくはhttpsプロトコルを利用する必要があるため、
このURLはみなさまのお手元からも利用できます
では、PostsRoute
を書き換えましょう。
App.PostsRoute = Ember.Route.extend({
model: function() {
return $.getJSON('http://emberjs.jsbin.com/goqene/2.json');
}
});
そしてブラウザをリロードしてみましょう。今までと変わらず記事一覧が表示されているでしょうか?
ブラウザの開発者ツールを利用して、
Google Chrome 39だと、

ところで、$.getJSON
の引数として渡したコールバックの中からデータにアクセスする必要があるからです。
$.getJSON('http://emberjs.jsbin.com/goqene/2.json', function(posts) {
// ここで posts にアクセスできる
});
では、
まず、$.getJSON()
の戻り値はjqXHR
オブジェクトです。このjqXHRオブジェクトはPromiseのインターフェースを実装しています。Promiseとは非同期を扱うためのパターンのひとつで、
コールバックを与えていた例をPromiseのインターフェースで書き直すと、
$.getJSON('http://emberjs.jsbin.com/goqene/2.json').then(function(posts) {
// ここで posts にアクセスできる
});
そしてEmber.model()
でPromiseが返されると、
さて、
Error handling
実は、
Substates
すべてのRouteにはloading
とerror
という子Routeが自動的に用意される仕組みがあります。これはEmber.
loading
:Routeのmodel()
が実行されている間アクティブになります。error
:Routeがテンプレートを描画するまでの間なんらかの例外が発生した場合にアクティブになります。
Ember.Loading
/ Error
という名前を追加したものがSubstatesのRoute名になります。例えば、PostsRoute
の場合はPostsLoadingRoute
/ PostsErrorRoute
です。また、ApplicationRoute
に対応するSubstatesはLoadingRoute
/ ErrorRoute
です。
この仕組みを利用すれば、
では、
Loading Substate
次のRouteを作成してください。
App.LoadingRoute = Ember.Route.extend({
activate: function() {
console.log('読み込み中です');
}
});
ここで利用しているactivate()
メソッドは、
ここまでのJavaScriptを記述したところでアプリケーションを動かしてみましょう。開発者コンソールのログに
コンソールに出力されるだけだと開発者にしかありがたみはないので、
<script type="text/x-handlebars" data-template-name="loading">
現在データを読込中です…
</script>
アプリケーションをリロードすると、
Error Substate
では、
App.ErrorRoute = Ember.Route.extend({
activate: function() {
console.log('エラーです');
}
});
次はこのErrorRoute
の挙動を確認するために、
App.PostsRoute = Ember.Route.extend({
model: function() {
return $.getJSON('/error.json');
}
});
/error.
に応答できるファイルは存在しないので、
この状態でアプリケーションを動かしてみると、
次はエラーが発生したことを画面に表示しましょう。次のテンプレートを作成してください。
<script type="text/x-handlebars" data-template-name="error">
おや、何か様子がおかしいです。
{{link-to "もう一度読み込む" "posts"}}
</script>
アプリケーションを動かしてみると、PostsRoute
がアクティブになり、
ErrorRoute
の動作が確認できたところで、PostsRoute
のmodel()
を元に戻しておきましょう。
App.PostsRoute = Ember.Route.extend({
model: function() {
return $.getJSON('http://emberjs.jsbin.com/goqene/2.json');
}
});
また、
今回の例ではPostsRoute
でJSONを取得しているため、loading
に対応しているLoadingRoute
を利用しました。もしPostsShowRoute
で一件の記事のJSONを取得するような場合であれば、PostsLoadingRoute
を使うこともできます。
Substateでは子のイベントはハンドルできますが、
今回の解説の最初でRoute定義を確認した際に見慣れないRouteがいくつかありましたが、

Events
Substatesの仕組みだと手軽にエラーをハンドリングできる一方で、
Routeは特定のタイミングでloading
/ error
イベントが発火します。このイベントをハンドルするためにはactions
というプロパティを設定し、error
/ loading
というイベントハンドラを定義します。
App.PostsRoute = Ember.Route.extend({
// ...
actions: {
loading: function() {
console.log('データを読込中です');
},
error: function() {
console.log('エラーが発生しました');
}
}
});
error
イベントハンドラの第一引数には、$.getJSON()
の場合、jqXHR
オブジェクトが渡されます。そこで次のようにして、
App.PostsRoute = Ember.Route.extend({
// ...
actions: {
error: function(jqXHR) {
if (jqXHR.status === 404) {
this.transitionTo('not_found');
} else {
this.transitionTo('something_went_wrong');
}
}
}
});
ここで利用しているRoute
のtransitionTo('route 名', [モデル...])
メソッドは画面遷移を行うためのメソッドです。テンプレートで利用しているlink-to
ヘルパと同じ使い方ができます。
ここでは次のようなテンプレートを定義することで、
<script type="text/x-handlebars" data-template-name="not_found">
お探しの記事は見つかりませんでした。
</script>
<script type="text/x-handlebars" data-template-name="something_went_wrong">
おや、何か様子がおかしいです。
</script>
またEventsはSubstatesと同じく、
Location
ここまで、#
から始まるフラグメントハッシュと呼ばれるものでした。ただ、#/posts
のようなURLではなく、/posts
のようなURLでアプリケーションを管理したいという場合があります。
そのようなときは、/
で始まるURLを利用できます。
App.Router.reopen({
location: 'history'
});
ただ、
また、
その場合には次の設定の行うことで、
App.Router.reopen({
location: 'none'
});
まとめ
今回はEmber.
次回はControllerを扱って一時データの保持とユーザの操作を受け取る方法を解説する予定です。