In Ember, the application's state manager handles routing. Let's take a look at a simple example:
App.stateManager = Ember.StateManager.create({
start: Ember.State.extend({
index: Ember.State.extend({
route "/",
setupContext: function(manager) {
manager.transitionTo('posts.index')
}
}),
posts: Ember.State.extend({
route: "/posts",
setupContext: function(manager) {
// the postsController is shared between the index and
// show views, so set it up here
var postsController = manager.get('postsController');
postsController.set('content', Post.findAll());
},
index: Ember.State.extend({
route: "/",
setupContext: function(manager) {
// the application's main view is a ContainerView whose
// view is bound to the manager's `currentView`
manager.set('currentView', App.PostsView.create({
controller: postsController
}));
},
showPost: function(manager, event) {
// this event was triggered from an {{action}} inside of
// an {{#each}} loop in a template
manager.transitionTo('show', event.context);
}
}),
show: Ember.State.extend({
route: "/:post_id",
setupContext: function(manager, post) {
var postsController = manager.get('postsController');
postsController.set('selected', post);
manager.set('currentView', App.PostView.create({
controller: postController
}))
},
serialize: function(manager, post) {
return { "post_id": post.get('id') }
},
deserialize: function(manager, params) {
return Post.find(params["post_id"]);
}
})
})
})
})
The primary goal of Ember routing is ensuring that the code that runs when you enter a state programmatically is the same as the code that runs when you enter a state through the URL.
In general, once you have set up your router, navigating through your application should automatically update the URL. Sharing that URL across sessions or through social media should result in exactly the same path through the application hierarchy.
Let's take a look at what happens in both situations with this simple example:
When the user enters at /
, the state manager will start out in the start
state. It
will immediately move into the start.index
state. Because the route for the index
state has no dynamic segments, the state manager passes no context to the setupContext
event.
In this case, the index state immediately moves the state manager into the default view
for this application, the posts.index
state.
When the state manager transitions into the posts.index
state, it generates a URL for
the current state, /posts
, and sets the current URL to /posts
(either using hash
changes or pushState as appropriate).
Entering the posts.index
state will also render a list of all of the posts as the
current view. Clicking on a post will trigger the state's showPost
event with that
post object as its context.
The showPost
event will transition the state manager into the posts.show
state,
passing the post as its context.
In this case, the posts.show
state has a dynamic segment (:post_id
), so the
state manager invokes the serialize
method with the context. Assuming the post
has an id of 45, the serialize
method returns { "post_id": 45 }
, which the
state manager uses to generate the state's portion of the URL.
Now that the user has navigated to a state corresponding to a URL of /posts/45
,
he may want to bookmark it or share it with a friend. Let's see what happens when
a user enters the app at /posts/45
.
First, the app will descend into the posts
state, whose route matches the first
part of the URL, /posts
. Since the state has no dynamic segment, it invokes the
setupContext
method with no context, and the state sets up the postsController
,
exactly as before when the app programmatically entered the state.
Next, it descends into posts.show
. Since posts.show
has a dynamic segment,
the state manager will invoke the state's deserialize
method to retrieve its
context.
The deserialize
method returns a Post
object for id 45
. To finish up, the
state manager invokes setupContext
with the deserialized Post
object.
NOTE: Implementation of this section is still TODO
While the above system is cool, writing a serialize and deserialize method for every state containing a dynamic segment can get somewhat repetitive.
To avoid this, you can specify a modelType
on a state, and you will get
default methods. For example, if you specify a modelType
of App.Post
,
like this:
show: Ember.State.extend({
route: "/:post_id",
modelType: 'App.Post',
setupContext: function(manager, context) {
var postsController = manager.get('postsController');
postsController.set('selected', context.post);
manager.set('currentView', App.PostView.create({
controller: postController
}))
}
})
The state manager will create the methods for you behind the scenes. You will get a state that looks like this:
show: Ember.State.extend({
route: "/:post_id",
modelType: 'App.Post',
setupContext: function(manager, context) {
var postsController = manager.get('postsController');
postsController.set('selected', context.post);
manager.set('currentView', App.PostView.create({
controller: postController
}))
},
serialize: function(manager, context) {
return { "post_id": context.post.get('id') };
},
deserialize: function(manager, params) {
return { "post": App.Post.find(params['post_id']) }
}
})
That makes the full example from above look like:
App.stateManager = Ember.StateManager.create({
start: Ember.State.extend({
index: Ember.State.extend({
route "/",
setupContext: function(manager) {
manager.transitionTo('posts.index')
}
}),
posts: Ember.State.extend({
route: "/posts",
setupContext: function(manager) {
// the postsController is shared between the index and
// show views, so set it up here
var postsController = manager.get('postsController');
postsController.set('content', Post.findAll());
},
index: Ember.State.extend({
route: "/",
setupContext: function(manager) {
// the application's main view is a ContainerView whose
// view is bound to the manager's `currentView`
manager.set('currentView', App.PostsView.create({
controller: postsController
}));
},
showPost: function(manager, event) {
// this event was triggered from an {{action}} inside of
// an {{#each}} loop in a template
manager.transitionTo('show', event.context);
}
}),
show: Ember.State.extend({
route: "/:post_id",
modelType: 'App.Post',
setupContext: function(manager, context) {
var postsController = manager.get('postsController');
postsController.set('selected', context.post);
manager.set('currentView', App.PostView.create({
controller: postController
}))
}
})
})
})
})