前回は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を扱って一時データの保持とユーザの操作を受け取る方法を解説する予定です。
