高レベルの話をするなら、あなたは Ember アプリケーションをネストした route の連続として設計し、それらはネストしたアプリケーションの状態(state)と一致します。このガイドではまず高レベルのコンセプトについて解説し、それから実例を見てみることにします。
ユーザはあなたのアプリケーションを、「何を見るか」の選択をすることで閲覧します。例えばあなたがブロクを持っているとして、あなたのユーザは最初に投稿もしくは "About" ページを選ぶでしょう。一般的に、あなたは最初の選択についてのデフォルトをもちたいはずです(このケースではおそらく投稿でしょう)。
ひとたびユーザが最初の選択をしたなら、通常彼らはそれで終わりません。コンテキストの中にある投稿群では、ユーザは最終的に個々の投稿やそれらのコメントを見るでしょう。個々の投稿の中では、彼らはコメントあるいはトラックバックのリストの閲覧を選択できます。
重要なことは、全てのケースにおいてユーザが何をページに映し出すかを選択するということです。あなたが自身のアプリケーションの state により深く入っていくに従い、それらの選択が影響をおよぼすページの範囲は小さくなります。
次のセクションでは、ページにおけるこれらの範囲をどのように制御するかを解説します。まずは、テンプレートがどのように構築されているかを見てみましょう。
ユーザが最初にアプリケーションに入るとき、アプリケーションは画面に表示されており、ルーターが制御する空の出力部分(outlet)を持っています。Ember では、outlet はテンプレートの一範囲であり、ユーザとの間の相互作用によって実行時に決定される子のテンプレートを持っています。
アプリケーション (application.handlebars) に対するテンプレートは、次のようなものを探します。
<h1>My Application</h1>
{{outlet}}
デフォルトではルーターは投稿リストの state に入り、 outlet を posts.handlebars で埋めます。これが正確にはどのように動作しているかは、後でみることにしましょう。
期待通り、 list of posts テンプレートは投稿リストを描画しました。個々の投稿へのリンクをクリックすると、アプリケーションの outlet の内容が、個々の投稿のテンプレートに置き換えられます。
テンプレートは次のようになるでしょう。
{{#each post in controller}}
<h1><a {{action showPost post href=true}}>{{post.title}}</a></h1>
<div>{{post.intro}}</div>
{{/each}}
個々の投稿へのリンクをクリックした際、アプリケーションは individual post state に移行し、アプリケーションの outlet の中にある posts.handlebars を、 post.handlebars で置き換えます。
このケースでは、個々の投稿もまた outlet を持っています。このケースでは、 outlet がユーザに対してコメントもしくはトラックバックを選ぶことを許可するでしょう。
個々の投稿のテンプレートは次のようになります。
<h1>{{title}}</h1>
<div class="body">
{{body}}
</div>
{{outlet}}
再び {{outlet}} が、ルーターがこのテンプレートの範囲内に何を映し出すかを決定するということを、シンプルに示しています。
{{outlet}} はすべてのテンプレートにおける特徴なので、あなたが route の階層構造により深く入り込んでいくとしても、そこではそれぞれの route がより小さなページの一部分を当然のように制御しています。
基本的な理論を理解したところで、ルーターが outlet をどのように制御しているかを見てみましょう。
まず、高レベルの handlebars テンプレートごとに、あなたは同じ名前のビューとコントローラを持っています。例えば、
- application.handlebars: 主となるアプリケーションビューに対するテンプレート。
- App.ApplicationController: テンプレートに対するコントローラ。 application.handlebars の初期値を持つコンテキストは、このコントローラのインスタンスである。
- App.ApplicationView: テンプレートに対するビューオブジェクト。
一般的にはビューオブジェクトをイベントの制御に使い、コントローラオブジェクトをテンプレートにデータを提供するために使います。
Ember は2つの主要なコントローラ、 ObjectController と ArrayController を提供します。これらのコントローラはモデルオブジェクトとモデルオブジェクトのリストのプロキシとして働きます。
我々はテンプレートに対し直接モデルオブジェクトをさらすのではなく、コントローラから物事を始めます。それは、view-related な 計算済みプロパティ (computed property) を出力する場所を持ち、また最終的にビュー関連からモデルを汚染させてしまうことがないようにするためです。
また、テンプレートに関連付けられたコントローラを使うことで {{outlet}} に接続することも可能です。
アプリケーションのルーターは、ユーザアクションに対するレスポンスの中で、アプリケーションの state を通じて、アプリケーションの動作に対して責任を持ちます。
シンプルなルーターから始めましょう。
App.Router = Ember.Router.extend({
root: Ember.Route.extend({
index: Ember.Route.extend({
route: '/',
redirectsTo: 'posts'
}),
posts: Ember.Route.extend({
route: '/posts'
}),
post: Ember.Route.extend({
route: '/posts/:post_id'
})
})
});
ルーターは3つのトップレベルの「状態」(state)を有しています。 index の state 、投稿リストの表示についての state 、個々の投稿の表示についての state です。
我々のケースでは、単純に index route を posts state にリダイレクトさせます。他のアプリケーションでは、専用のホームページを持ちたいと思うかもしれません。
我々は state のリストを持っており、アプリケーションは忠実に posts state に入りますが、そこで何もかもが行われるわけではありません。アプリケーションが posts state に入るとき、我々は アプリケーションテンプレート内の outlet にその state を接続したいと考えます。そこで、 connectOutlets コールバックを使ってそれを実現します。
App.Router = Ember.Router.extend({
root: Ember.Route.extend({
index: Ember.Route.extend({
route: '/',
redirectsTo: 'posts'
}),
posts: Ember.Route.extend({
route: '/posts',
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('posts', App.Post.find());
}
}),
post: Ember.Route.extend({
route: '/posts/:post_id'
})
})
});
connectOutlet は我々のために以下のことをやってくれます。
- posts.handlebars テンプレートを使って、App.PostsViewのインスタンスを作る。
- 全ての有効な posts (App.Post.find()) のリストに対して postsController の content プロパティを代入し、新しい App.PostsView のために postContorller を作る。
- 新しいビューを、application.handlebars の中にある outlet に対して接続する。
一般的に、これらのオブジェクトは直列に協調して動作するものと捉えるべきです。あなたがビューを作った時、常にあなたはビューのコントローラに対して内容を提供することになります。
次に、posts state にあるアプリケーションに対して、 post state に移行する手段を提供したいと思います。それは遷移を指示することで実現します。
posts: Ember.Route.extend({
route: '/posts',
showPost: Ember.Route.transitionTo('post'),
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('posts', App.Post.find());
}
})
あなたはこの遷移を、現在のテンプレートの中で {{action}} ヘルパーを使うことで発動させます。
{{#each post in controller}}
<h1><a {{action showPost post href=true}}>{{post.title}}</a></h1>
{{/each}}
ユーザが {{action}} ヘルパーを伴うリンクをクリックすると、 Ember は特定の名前を付けて、イベントを現在の state へと送ります。このケースでは、イベントは「遷移」です。
我々が遷移を行うことで、 ember もまたこのリンクに対するURLを生成できます。 Ember はコンテキスト中の id プロパティを使い、 post state の動的なセグメントである :post_id を埋めます。
次に、 post state の connectOutlets を実装する必要があります。今回は、 connectOutlets メソッドが post オブジェクトを受け取るようにします。post オブジェクトは、 {{action}} ヘルパー に対してコンテキストとして定義されます。
post: Ember.Route.extend({
route: '/posts/:post_id',
connectOutlets: function(router, post) {
router.get('applicationController').connectOutlet('post', post);
}
})
要約すると、 connectOutlet は以下の手順を踏みます。
- App.PostViewの新しいインスタンスを作る。それは post.handlebars テンプレートを使うことで行われる。
- ユーザがクリックした投稿に対して、postController の content プロパティを代入する。
- application.handlebars の中の outlet に対して新しいビューを接続する。
ユーザが /posts/1 へのリンクをブックマークとして保存し、また戻ってきた時に、/posts/1 へのリンクを取得するために、あなたは特に何もする必要がありません。
もしユーザが /posts/1 のURLが示すページに新しく入った場合、ルーターは以下の手順を実行します。
- URLと対応する state を探し出す(この場合は post)。
- URLから動的な部分を抽出し(この場合は :post_id)、 App.Post.find(post_id)を呼ぶ。これは命名規約を通じて行われる。 :post_id という動的部分は、 App.Post に対応する。
- connectOutletsを、 App.Post.find の返り値を使って呼び出す。
これは、ユーザが post state に対してページの中の別の部分から入ったか、あるいは URL を指定することで入ったかを関知しないという事を意味し、またルーターは同じオブジェクトに対して connectOutlets メソッドを呼び出す事を意味します。
最後に、コメントとトラックバック機能を実装しましょう。
post state は root state と同じパターンを用いているため、非常に似通っています。
post: Ember.Route.extend({
route: '/posts/:post_id',
connectOutlets: function(router, post) {
router.get('applicationController').connectOutlet('post', post);
},
index: Ember.Route.extend({
route: '/',
redirectsTo: 'comments'
}),
comments: Ember.Route.extend({
route: '/comments',
showTrackbacks: Ember.Route.transitionTo('trackbacks'),
connectOutlets: function(router) {
var postController = router.get('postController');
postController.connectOutlet('comments', postController.get('comments'));
}
}),
trackbacks: Ember.Route.extend({
route: '/trackbacks',
showComments: Ember.Route.transitionTo('comments'),
connectOutlets: function(router) {
var postController = router.get('postController');
postController.connectOutlet('trackbacks', postController.get('trackbacks'));
}
})
})
ここには次のような変更点があるだけです。
- showTrackback と showCommencts が、遷移する意味のある状態にのみ遷移するようにした。
- 我々が post.handlebars の中の outlet にビューをセットした後、postController にある connectOutlet を呼び出す。
- この場合は、現在の投稿から commentsController と trackbacksController への内容を取得する。 postController は基礎となる投稿のプロキシであり、 postController から直接に関連を取り出す
ここに、個々の投稿のテンプレートを示します。
<h1>{{title}}</h1>
<div class="body">
{{body}}
</div>
<p>
<a {{action showComments href=true}}>Comments</a> |
<a {{action showTrackbacks href=true}}>Trackbacks</a>
</p>
{{outlet}}
最後に、ブックマークされたリンクによって戻ってくることは、ネストした設定によって上手く動作します。 /posts/1/trackbacks にあるサイトにユーザが入った時に何が起こるかを見てみましょう。
- ルーターが、どの state が URL に対応しているのかを解決する(post.trackbacks)。そしてその state に入る。
- ルーターは(URLの)動的なセグメントを展開し、 connectOutlets を呼ぶ。これはパスを返し、ユーザはそのパスに従ってアプリケーション内を移動することになる。これまでと同じように、ルーターは App.Post.find(1) によって connectOutlet メソッドを呼ぶ。
- ルーターが trackbacks state を得ると、ルーターは connectOutlets を作動させます。 connectOutlets メソッドは postController の content を有しているため、 trackbacks state はその関連を検索するでしょう。
再度、 connectOutlets コールバックが動的なURLセグメンツとともに動作することにより、 {{action}} ヘルパーによって生成された URL は後に動作することが保証されます。
最後の1点: あなたは App.Post.find(1) が呼ばれるまでに、アプリケーションがまだ Post 1 をロードしていない場合も、このシステムは動作できるのかどうか自問するのではありませんか?
これが動作する理由は、クエリをこれから開始する必要があったとしても、 ember-data がオブジェクトを常に即座に返すからです。このオブジェクトは 空の data ハッシュから始まります。サーバがデータを返したとき、 ember-data はオブジェクトの data を更新し、定義済みの全ての属性( DS.attr を使って定義されたプロパティ)にバインドされたものを発火させます。
あなたが trackbaks に対するオブジェクトを要求した場合、その ManyArray が同様に返ってきます。サーバが投稿に関連する内容を返すと、 ember-data もまた自動的に trackbaks 配列を更新します。
trackbacks.handlebars テンプレートで、次のようなことを行ったとしましょう。
<ul>
{{#each trackback in controller}}
<li><a {{bindAttr href="trackback.url"}}>{{trackback.title}}</a></li>
{{/each}}
</ul>
ember-data が trackbacks 配列を更新するとき、その変更は trackbacksController を通じて、DOMに対して伝えられます。
あなたはまた、まだロードされてない断片的なデータを表示することを避けたいと思うでしょう。その場合には、次のようにすることができます。
<ul>
{{#if controller.isLoaded}}
{{#each trackback in controller}}
<li><a {{bindAttr href="trackback.url"}}>{{trackback.title}}</a></li>
{{/each}}
{{else}}
<li><img src="/spinner.gif"> Loading trackbacks...</li>
{{/if}}
</ul>
ember-data がサーバにより提供されたデータからトラックバックに対する ManyArray を作るとき、 isLoaded プロパティもまたセットされます。なぜなら全てのテンプレート構造、#if を含むそれは、基礎的なプロパティが変更されると自動的に DOM を更新するので、「ぴったりと動作」するのです。