Routing
Introduction
Routing
As users interact with your application, it moves through many different states. Ember.js gives you helpful tools for managing that state in a way that scales with your application.
To understand why this is important, imagine we are writing a web app for managing a blog. At any given time, we should be able to answer questions like: Is the user currently logged in? Are they an admin user? What post are they looking at? Is the settings screen open? Are they editing the current post?
In Ember.js, each of the possible states in your application is represented by a URL. Because all of the questions we asked above— Are we logged in? What post are we looking at? —are encapsulated by route handlers for the URLs, answering them is both simple and accurate.
At any given time, your application has one or more active route handlers. The active handlers can change for one of two reasons:
- The user interacted with a view, which generated an event that caused the URL to change.
- The user changed the URL manually (e.g., via the back button), or the page was loaded for the first time.
When the current URL changes, the newly active route handlers may do one or more of the following:
- Conditionally redirect to a new URL.
- Update a controller so that it represents a particular model.
- Change the template on screen, or place a new template into an existing outlet.
Logging Route Changes
As your application increases in complexity, it can be helpful to see exactly what is going on with the router. To have Ember write out transition events to the log, simply modify your Ember.Application:
1 App = Ember.Application.create({
2 LOG_TRANSITIONS: true
3 });
Specifying a Root URL
If your Ember application is one of multiple web applications served from the same domain, it may be necessary to indicate to the router what the root URL for your Ember application is. By default, Ember will assume it is served from the root of your domain.
If for example, you wanted to serve your blogging application from www.emberjs.com/blog/, it would be necessary to specify a root URL of /blog/.
This can be achieved by setting the rootURL on the router:
1 App.Router.reopen({
2 rootURL: '/blog/'
3 });
Defining Your Routes
When your application starts, the router is responsible for displaying templates, loading data, and otherwise setting up application state. It does so by matching the current URL to the routes that you’ve defined.
The map method
of your Ember application’s router can be invoked to define URL mappings. When
calling map, you should pass a function that will be invoked with the value
this set to an object which you can use to create
routes and
resources.
1 App.Router.map(function() {
2 this.route("about", { path: "/about" });
3 this.route("favorites", { path: "/favs" });
4 });
Now, when the user visits /about, Ember.js will render the about
template. Visiting /favs will render the favorites template.
Note that you can leave off the path if it is the same as the route name. In this case, the following is equivalent to the above example:
1 App.Router.map(function() {
2 this.route("about");
3 this.route("favorites", { path: "/favs" });
4 });
Inside your templates, you can use {{link-to}} to navigate between
routes, using the name that you provided to the route method (or, in
the case of /, the name index).
1 {{#link-to 'index'}}<img class="logo">{{/link-to}}
2
3 <nav>
4 {{#link-to 'about'}}About{{/link-to}}
5 {{#link-to 'favorites'}}Favorites{{/link-to}}
6 </nav>
The {{link-to}} helper will also add an active class to the link that
points to the currently active route.
You can customize the behavior of a route by creating an Ember.Route
subclass. For example, to customize what happens when your user visits
/, create an App.IndexRoute:
1 App.IndexRoute = Ember.Route.extend({
2 setupController: function(controller) {
3 // Set the IndexController's `title`
4 controller.set('title', "My App");
5 }
6 });
The IndexController is the starting context for the index template.
Now that you’ve set title, you can use it in the template:
1 <!-- get the title from the IndexController -->
2 <h1>{{title}}</h1>
(If you don’t explicitly define an App.IndexController, Ember.js will
automatically generate one for you.)
Ember.js automatically figures out the names of the routes and controllers based on
the name you pass to this.route.
Resources
You can define groups of routes that work with a resource:
1 App.Router.map(function() {
2 this.resource('posts', { path: '/posts' }, function() {
3 this.route('new');
4 });
5 });
As with this.route, you can leave off the path if it’s the same as the
name of the route, so the following router is equivalent:
1 App.Router.map(function() {
2 this.resource('posts', function() {
3 this.route('new');
4 });
5 });
This router creates three routes:
<small><sup>1</sup> Transitioning to posts or creating a link to
posts is equivalent to transitioning to posts.index or linking to
posts.index</small>
NOTE: If you define a resource using this.resource and do not supply
a function, then the implicit resource.index route is not created. In
that case, /resource will only use the ResourceRoute, ResourceController,
and resource template.
Routes nested under a resource take the name of the resource plus their
name as their route name. If you want to transition to a route (either
via transitionTo or {{#link-to}}), make sure to use the full route
name (posts.new, not new).
Visiting / renders the index template, as you would expect.
Visiting /posts is slightly different. It will first render the
posts template. Then, it will render the posts/index template into the
posts template’s outlet.
Finally, visiting /posts/new will first render the posts template,
then render the posts/new template into its outlet.
NOTE: You should use this.resource for URLs that represent a noun,
and this.route for URLs that represent adjectives or verbs
modifying those nouns. For example, in the code sample above, when
specifying URLs for posts (a noun), the route was defined with
this.resource('posts'). However, when defining the new action
(a verb), the route was defined with this.route('new').
Dynamic Segments
One of the responsibilities of a resource’s route handler is to convert a URL into a model.
For example, if we have the resource this.resource('posts');, our
route handler might look like this:
1 App.PostsRoute = Ember.Route.extend({
2 model: function() {
3 return this.store.find('posts');
4 }
5 });
The posts template will then receive a list of all available posts as
its context.
Because /posts represents a fixed model, we don’t need any
additional information to know what to retrieve. However, if we want a route
to represent a single post, we would not want to have to hardcode every
possible post into the router.
Enter dynamic segments.
A dynamic segment is a portion of a URL that starts with a : and is
followed by an identifier.
1 App.Router.map(function() {
2 this.resource('posts');
3 this.resource('post', { path: '/post/:post_id' });
4 });
5
6 App.PostRoute = Ember.Route.extend({
7 model: function(params) {
8 return this.store.find('post', params.post_id);
9 }
10 });
Because this pattern is so common, the above model hook is the
default behavior.
For example, if the dynamic segment is :post_id, Ember.js is smart
enough to know that it should use the model App.Post (with the ID
provided in the URL). Specifically, unless you override model, the route will
return this.store.find('post', params.post_id) automatically.
Not coincidentally, this is exactly what Ember Data expects. So if you use the Ember router with Ember Data, your dynamic segments will work as expected out of the box.
If your model does not use the id property in the URL, you should
define a serialize method on your route:
1 App.Router.map(function() {
2 this.resource('post', {path: '/posts/:post_slug'});
3 });
4
5 App.PostRoute = Ember.Route.extend({
6 model: function(params) {
7 // the server returns `{ slug: 'foo-post' }`
8 return jQuery.getJSON("/posts/" + params.post_slug);
9 },
10
11 serialize: function(model) {
12 // this will make the URL `/posts/foo-post`
13 return { post_slug: model.get('slug') };
14 }
15 });
The default serialize method inserts the model’s id into the route’s
dynamic segment (in this case, :post_id).
Nested Resources
You can nest both routes and resources:
1 App.Router.map(function() {
2 this.resource('post', { path: '/post/:post_id' }, function() {
3 this.route('edit');
4 this.resource('comments', function() {
5 this.route('new');
6 });
7 });
8 });
This router creates five routes:
<small><sup>2</sup> :post_id is the post’s id. For a post with id = 1, the route will be:
/post/1</small>
The comments template will be rendered in the post outlet.
All templates under comments (comments/index and comments/new) will be rendered in the comments outlet.
The route, controller, and view class names for the comments resource are not prefixed with Post. Resources
always reset the namespace, ensuring that the classes can be re-used between multiple parent resources and that
class names don’t get longer the deeper nested the resources are.
You are also able to create deeply nested resources in order to preserve the namespace on your routes:
1 App.Router.map(function() {
2 this.resource('foo', function() {
3 this.resource('foo.bar', { path: '/bar' }, function() {
4 this.route('baz'); // This will be foo.bar.baz
5 });
6 });
7 });
This router creates the following routes:
Initial routes
A few routes are immediately available within your application:
-
App.ApplicationRouteis entered when your app first boots up. It renders theapplicationtemplate. -
App.IndexRouteis the default route, and will render theindextemplate when the user visits/(unless/has been overridden by your own custom route).
Remember, these routes are part of every application, so you don’t need to
specify them in App.Router.map.
Wildcard / globbing routes
You can define wildcard routes that will match multiple routes. This could be used, for example, if you’d like a catchall route which is useful when the user enters an incorrect URL not managed by your app.
1 App.Router.map(function() {
2 this.route('catchall', {path: '/*wildcard'});
3 });
Like all routes with a dynamic segment, you must provide a context when using a {{link-to}}
or transitionTo to programatically enter this route.
1 App.ApplicationRoute = Ember.Route.extend({
2 actions: {
3 error: function () {
4 this.transitionTo('catchall', "application-error");
5 }
6 }
7 });
With this code, if an error bubbles up to the Application route, your application will enter
the catchall route and display /application-error in the URL.
Generated Objects
As explained in the routing guide, whenever you define a new route, Ember.js attempts to find corresponding Route, Controller, View, and Template classes named according to naming conventions. If an implementation of any of these objects is not found, appropriate objects will be generated in memory for you.
Generated routes
Given you have the following route:
1 App.Router.map(function() {
2 this.resource('posts');
3 });
When you navigate to /posts, Ember.js looks for App.PostsRoute.
If it doesn’t find it, it will automatically generate an App.PostsRoute for you.
Custom Generated Routes
You can have all your generated routes extend a custom route. If you define App.Route,
all generated routes will be instances of that route.
Generated Controllers
If you navigate to route posts, Ember.js looks for a controller called App.PostsController.
If you did not define it, one will be generated for you.
Ember.js can generate three types of controllers:
Ember.ObjectController, Ember.ArrayController, and Ember.Controller.
The type of controller Ember.js chooses to generate for you depends on your route’s
model hook:
- If it returns an object (such as a single record), an ObjectController will be generated.
- If it returns an array, an ArrayController will be generated.
- If it does not return anything, an instance of
Ember.Controllerwill be generated.
Custom Generated Controllers
If you want to customize generated controllers, you can define your own App.Controller, App.ObjectController
and App.ArrayController. Generated controllers will extend one of these three (depending on the conditions above).
Generated Views and Templates
A route also expects a view and a template. If you don’t define a view, a view will be generated for you.
A generated template is empty.
If it’s a resource template, the template will simply act
as an outlet so that nested routes can be seamlessly inserted. It is equivalent to:
1 {{outlet}}
Specifying A Routes Model
Templates in your application are backed by models. But how do templates know which model they should display?
For example, if you have a photos template, how does it know which
model to render?
This is one of the jobs of an Ember.Route. You can tell a template
which model it should render by defining a route with the same name as
the template, and implementing its model hook.
For example, to provide some model data to the photos template, we
would define an App.PhotosRoute object:
1 App.PhotosRoute = Ember.Route.extend({
2 model: function() {
3 return [{
4 title: "Tomster",
5 url: "http://emberjs.com/images/about/ember-productivity-sm.png"
6 }, {
7 title: "Eiffel Tower",
8 url: "http://emberjs.com/images/about/ember-structure-sm.png"
9 }];
10 }
11 });
Asynchronously Loading Models
In the above example, the model data was returned synchronously from the
model hook. This means that the data was available immediately and
your application did not need to wait for it to load, in this case
because we immediately returned an array of hardcoded data.
Of course, this is not always realistic. Usually, the data will not be available synchronously, but instead must be loaded asynchronously over the network. For example, we may want to retrieve the list of photos from a JSON API available on our server.
In cases where data is available asynchronously, you can just return a
promise from the model hook, and Ember will wait until that promise is
resolved before rendering the template.
If you’re unfamiliar with promises, the basic idea is that they are
objects that represent eventual values. For example, if you use jQuery’s
getJSON() method, it will return a promise for the JSON that is
eventually returned over the network. Ember uses this promise object to
know when it has enough data to continue rendering.
For more about promises, see A Word on Promises in the Asynchronous Routing guide.
Let’s look at an example in action. Here’s a route that loads the most recent pull requests sent to Ember.js on GitHub:
1 App.PullRequestsRoute = Ember.Route.extend({
2 model: function() {
3 return Ember.$.getJSON('https://api.github.com/repos/emberjs/ember.js/pulls'\
4 );
5 }
6 });
While this example looks like it’s synchronous, making it easy to read
and reason about, it’s actually completely asynchronous. That’s because
jQuery’s getJSON() method returns a promise. Ember will detect the
fact that you’ve returned a promise from the model hook, and wait
until that promise resolves to render the pullRequests template.
(For more information on jQuery’s XHR functionality, see jQuery.ajax in the jQuery documentation.)
Because Ember supports promises, it can work with any persistence library that uses them as part of its public API. You can also use many of the conveniences built in to promises to make your code even nicer.
For example, imagine if we wanted to modify the above example so that the template only displayed the three most recent pull requests. We can rely on promise chaining to modify the data returned from the JSON request before it gets passed to the template:
1 App.PullRequestsRoute = Ember.Route.extend({
2 model: function() {
3 var url = 'https://api.github.com/repos/emberjs/ember.js/pulls';
4 return Ember.$.getJSON(url).then(function(data) {
5 return data.splice(0, 3);
6 });
7 }
8 });
Setting Up Controllers with the Model
So what actually happens with the value you return from the model
hook?
By default, the value returned from your model hook will be assigned
to the model property of the associated controller. For example, if your
App.PostsRoute returns an object from its model hook, that object
will be set as the model property of the App.PostsController.
(This, under the hood, is how templates know which model to render: they
look at their associated controller’s model property. For example, the
photos template will render whatever the App.PhotosController’s
model property is set to.)
See the Setting Up a Controller guide to learn how to change this
default behavior. Note that if you override the default behavior and do
not set the model property on a controller, your template will not
have any data to render!
Dynamic Models
Some routes always display the same model. For example, the /photos
route will always display the same list of photos available in the
application. If your user leaves this route and comes back later, the
model does not change.
However, you will often have a route whose model will change depending
on user interaction. For example, imagine a photo viewer app. The
/photos route will render the photos template with the list of
photos as the model, which never changes. But when the user clicks on a
particular photo, we want to display that model with the photo
template. If the user goes back and clicks on a different photo, we want
to display the photo template again, this time with a different model.
In cases like this, it’s important that we include some information in the URL about not only which template to display, but also which model.
In Ember, this is accomplished by defining routes with dynamic segments.
A dynamic segment is a part of the URL that is filled in by the current
model’s ID. Dynamic segments always start with a colon (:). Our photo
example might have its photo route defined like this:
1 App.Router.map(function() {
2 this.resource('photo', { path: '/photos/:photo_id' });
3 });
In this example, the photo route has a dynamic segment :photo_id.
When the user goes to the photo route to display a particular photo
model (usually via the {{link-to}} helper), that model’s ID will be
placed into the URL automatically.
See Links for more information about linking
to a route with a model using the {{link-to}} helper.
For example, if you transitioned to the photo route with a model whose
id property was 47, the URL in the user’s browser would be updated
to:
1 /photos/47
What happens if the user visits your application directly with a URL that contains a dynamic segment? For example, they might reload the page, or send the link to a friend, who clicks on it. At that point, because we are starting the application up from scratch, the actual JavaScript model object to display has been lost; all we have is the ID from the URL.
Luckily, Ember will extract any dynamic segments from the URL for
you and pass them as a hash to the model hook as the first argument:
1 App.Router.map(function() {
2 this.resource('photo', { path: '/photos/:photo_id' });
3 });
4
5 App.PhotoRoute = Ember.Route.extend({
6 model: function(params) {
7 return Ember.$.getJSON('/photos/'+params.photo_id);
8 }
9 });
In the model hook for routes with dynamic segments, it’s your job to
turn the ID (something like 47 or post-slug) into a model that can
be rendered by the route’s template. In the above example, we use the
photo’s ID (params.photo_id) to construct a URL for the JSON
representation of that photo. Once we have the URL, we use jQuery to
return a promise for the JSON model data.
Note: A route with a dynamic segment will only have its model hook called
when it is entered via the URL. If the route is entered through a transition
(e.g. when using the link-to Handlebars helper), then a model context is
already provided and the hook is not executed. Routes without dynamic segments
will always execute the model hook.
Refreshing your model
If your data represented by your model is being updated frequently, you may want to refresh it periodically:
The controller can send an action to the Route; in this example above, the
IndexController exposes an action getLatest which sends the route an
action called invalidateModel. Calling the route’s refresh method will force
Ember to execute the model hook again.
Ember Data
Many Ember developers use a model library to make finding and saving records easier than manually managing Ajax calls. In particular, using a model library allows you to cache records that have been loaded, significantly improving the performance of your application.
One popular model library built for Ember is Ember Data. To learn more about using Ember Data to manage your models, see the Models guide.
Setting Up A Controller
Changing the URL may also change which template is displayed on screen. Templates, however, are usually only useful if they have some source of information to display.
In Ember.js, a template retrieves information to display from a controller.
Two built-in controllers—Ember.ObjectController and
Ember.ArrayController—make it easy for a controller to present a
model’s properties to a template, along with any additional
display-specific properties.
To tell one of these controllers which model to present, set its
model property in the route handler’s setupController hook.
1 App.Router.map(function() {
2 this.resource('post', { path: '/posts/:post_id' });
3 });
4
5 App.PostRoute = Ember.Route.extend({
6 // The code below is the default behavior, so if this is all you
7 // need, you do not need to provide a setupController implementation
8 // at all.
9 setupController: function(controller, model) {
10 controller.set('model', model);
11 }
12 });
The setupController hook receives the route handler’s associated
controller as its first argument. In this case, the PostRoute’s
setupController receives the application’s instance of
App.PostController.
To specify a controller other than the default, set the route’s
controllerName property:
1 App.SpecialPostRoute = Ember.Route.extend({
2 controllerName: 'post'
3 });
As a second argument, it receives the route handler’s model. For more information, see Specifying a Route’s Model.
The default setupController hook sets the model property of the
associated controller to the route handler’s model.
If you want to configure a controller other than the controller
associated with the route handler, use the controllerFor method:
1 App.PostRoute = Ember.Route.extend({
2 setupController: function(controller, model) {
3 this.controllerFor('topPost').set('model', model);
4 }
5 });
Rendering A Template
One of the most important jobs of a route handler is rendering the appropriate template to the screen.
By default, a route handler will render the template into the closest parent with a template.
1 App.Router.map(function() {
2 this.resource('posts');
3 });
4
5 App.PostsRoute = Ember.Route.extend();
If you want to render a template other than the one associated with the
route handler, implement the renderTemplate hook:
1 App.PostsRoute = Ember.Route.extend({
2 renderTemplate: function() {
3 this.render('favoritePost');
4 }
5 });
If you want to use a different controller than the route handler’s
controller, pass the controller’s name in the controller option:
1 App.PostsRoute = Ember.Route.extend({
2 renderTemplate: function() {
3 this.render({ controller: 'favoritePost' });
4 }
5 });
Ember allows you to name your outlets. For instance, this code allows you to specify two outlets with distinct names:
1 <div class="toolbar">{{outlet toolbar}}</div>
2 <div class="sidebar">{{outlet sidebar}}</div>
So, if you want to render your posts into the sidebar outlet, use code
like this:
1 App.PostsRoute = Ember.Route.extend({
2 renderTemplate: function() {
3 this.render({ outlet: 'sidebar' });
4 }
5 });
All of the options described above can be used together in whatever combination you’d like:
1 App.PostsRoute = Ember.Route.extend({
2 renderTemplate: function() {
3 var controller = this.controllerFor('favoritePost');
4
5 // Render the `favoritePost` template into
6 // the outlet `posts`, and use the `favoritePost`
7 // controller.
8 this.render('favoritePost', {
9 outlet: 'posts',
10 controller: controller
11 });
12 }
13 });
If you want to render two different templates into outlets of two different rendered templates of a route:
1 App.PostRoute = App.Route.extend({
2 renderTemplate: function() {
3 this.render('favoritePost', { // the template to render
4 into: 'posts', // the template to render into
5 outlet: 'posts', // the name of the outlet in that template
6 controller: 'blogPost' // the controller to use for the template
7 });
8 this.render('comments', {
9 into: 'favoritePost',
10 outlet: 'comment',
11 controller: 'blogPost'
12 });
13 }
14 });
Redirecting
Transitioning and Redirecting
Calling transitionTo from a route or transitionToRoute from a controller
will stop any transition currently in progress and start a new one, functioning
as a redirect. transitionTo takes parameters and behaves exactly like the link-to helper:
- If you transition into a route without dynamic segments that route’s
modelhook will always run. - If the new route has dynamic segments, you need to pass either a model or an identifier for each segment.
Passing a model will skip that segment’s
modelhook. Passing an identifier will run themodelhook and you’ll be able to access the identifier in the params. See Links for more detail.
Before the model is known
If you want to redirect from one route to another, you can do the transition in
the beforeModel hook of your route handler.
1 App.Router.map(function() {
2 this.resource('posts');
3 });
4
5 App.IndexRoute = Ember.Route.extend({
6 beforeModel: function() {
7 this.transitionTo('posts');
8 }
9 });
After the model is known
If you need some information about the current model in order to decide about
the redirection, you should either use the afterModel or the redirect hook. They
receive the resolved model as the first parameter and the transition as the second one,
and thus function as aliases. (In fact, the default implementation of afterModel just calls redirect.)
1 App.Router.map(function() {
2 this.resource('posts');
3 this.resource('post', { path: '/post/:post_id' });
4 });
5
6 App.PostsRoute = Ember.Route.extend({
7 afterModel: function(posts, transition) {
8 if (posts.get('length') === 1) {
9 this.transitionTo('post', posts.get('firstObject'));
10 }
11 }
12 });
When transitioning to the PostsRoute if it turns out that there is only one post,
the current transition will be aborted in favor of redirecting to the PostRoute
with the single post object being its model.
Based on other application state
You can conditionally transition based on some other application state.
1 App.Router.map(function() {
2 this.resource('topCharts', function() {
3 this.route('choose', { path: '/' });
4 this.route('albums');
5 this.route('songs');
6 this.route('artists');
7 this.route('playlists');
8 });
9 });
10
11 App.TopChartsChooseRoute = Ember.Route.extend({
12 beforeModel: function() {
13 var lastFilter = this.controllerFor('application').get('lastFilter');
14 this.transitionTo('topCharts.' + (lastFilter || 'songs'));
15 }
16 });
17
18 // Superclass to be used by all of the filter routes below
19 App.FilterRoute = Ember.Route.extend({
20 activate: function() {
21 var controller = this.controllerFor('application');
22 controller.set('lastFilter', this.templateName);
23 }
24 });
25
26 App.TopChartsSongsRoute = App.FilterRoute.extend();
27 App.TopChartsAlbumsRoute = App.FilterRoute.extend();
28 App.TopChartsArtistsRoute = App.FilterRoute.extend();
29 App.TopChartsPlaylistsRoute = App.FilterRoute.extend();
In this example, navigating to the / URL immediately transitions into
the last filter URL that the user was at. The first time, it transitions
to the /songs URL.
Your route can also choose to transition only in some cases. If the
beforeModel hook does not abort or transition to a new route, the remaining
hooks (model, afterModel, setupController, renderTemplate) will execute
as usual.
Specifying The URL Type
By default the Router uses the browser’s hash to load the starting state of your application and will keep it in sync as you move around. At present, this relies on a hashchange event existing in the browser.
Given the following router, entering /#/posts/new will take you to the posts.new
route.
1 App.Router.map(function() {
2 this.resource('posts', function() {
3 this.route('new');
4 });
5 });
If you want /posts/new to work instead, you can tell the Router to use the browser’s
history API.
Keep in mind that your server must serve the Ember app at all the routes defined here.
1 App.Router.reopen({
2 location: 'history'
3 });
Finally, if you don’t want the browser’s URL to interact with your application at all, you can disable the location API entirely. This is useful for testing, or when you need to manage state with your Router, but temporarily don’t want it to muck with the URL (for example when you embed your application in a larger page).
1 App.Router.reopen({
2 location: 'none'
3 });
Query Parameters
Query parameters are optional key-value pairs that appear to the right of
the ? in a URL. For example, the following URL has two query params,
sort and page, with respective values ASC and 2:
1 http://example.com/articles?sort=ASC&page=2
Query params allow for additional application state to be serialized
into the URL that can’t otherwise fit into the path of the URL (i.e.
everything to the left of the ?). Common use cases for query params include
representing the current page, filter criteria, or sorting criteria.
Specifying Query Parameters
Query params can be declared on route-driven controllers, e.g. to
configure query params that are active within the articles route,
they must be declared on ArticlesController.
Let’s say we’d like to add a category
query parameter that will filter out all the articles that haven’t
been categorized as popular. To do this, we specify 'category'
as one of ArticlesController’s queryParams:
1 App.ArticlesController = Ember.ArrayController.extend({
2 queryParams: ['category'],
3 category: null
4 });
This sets up a binding between the category query param in the URL,
and the category property on ArticlesController. In other words,
once the articles route has been entered, any changes to the
category query param in the URL will update the category property
on ArticlesController, and vice versa.
Now we just need to define a computed property of our category-filtered
array that the articles template will render:
1 App.ArticlesController = Ember.ArrayController.extend({
2 queryParams: ['category'],
3 category: null,
4
5 filteredArticles: function() {
6 var category = this.get('category');
7 var articles = this.get('model');
8
9 if (category) {
10 return articles.filterBy('category', category);
11 } else {
12 return articles;
13 }
14 }.property('category', 'model')
15 });
With this code, we have established the following behaviors:
- If the user navigates to
/articles,categorywill benull, so the articles won’t be filtered. - If the user navigates to
/articles?category=recent,categorywill be set to"recent", so articles will be filtered. - Once inside the
articlesroute, any changes to thecategoryproperty onArticlesControllerwill cause the URL to update the query param. By default, a query param property change won’t cause a full router transition (i.e. it won’t callmodelhooks andsetupController, etc.); it will only update the URL.
link-to Helper
The link-to helper supports specifying query params by way of the
query-params subexpression helper.
1 // Explicitly set target query params
2 {{#link-to 'posts' (query-params direction="asc")}}Sort{{/link-to}}
3
4 // Binding is also supported
5 {{#link-to 'posts' (query-params direction=otherDirection)}}Sort{{/link-to}}
In the above examples, direction is presumably a query param property
on the PostsController, but it could also refer to a direction property
on any of the controllers associated with the posts route hierarchy,
matching the leaf-most controller with the supplied property name.
The link-to helper takes into account query parameters when determining its “active” state, and will set the class appropriately. The active state is determined by calculating whether the query params end up the same after clicking a link. You don’t have to supply all of the current, active query params for this to be true.
transitionTo
Route#transitionTo (and Controller#transitionToRoute) now
accepts a final argument, which is an object with
the key queryParams.
1 this.transitionTo('post', object, {queryParams: {showDetails: true}});
2 this.transitionTo('posts', {queryParams: {sort: 'title'}});
3
4 // if you just want to transition the query parameters without changing the route
5 this.transitionTo({queryParams: {direction: 'asc'}});
You can also add query params to URL transitions:
1 this.transitionTo("/posts/1?sort=date&showDetails=true");
Opting into a full transition
Keep in mind that if the arguments provided to transitionTo
or link-to only correspond to a change in query param values,
and not a change in the route hierarchy, it is not considered a
full transition, which means that hooks like model and
setupController won’t fire by default, but rather only
controller properties will be updated with new query param values, as
will the URL.
But some query param changes necessitate loading data from the server,
in which case it is desirable to opt into a full-on transition. To opt
into a full transition when a controller query param property changes,
you can use the optional queryParams configuration hash on the Route
associated with that controller, and set that query param’s
refreshModel config property to true:
1 App.ArticlesRoute = Ember.Route.extend({
2 queryParams: {
3 category: {
4 refreshModel: true
5 }
6 },
7 model: function(params) {
8 // This gets called upon entering 'articles' route
9 // for the first time, and we opt into refiring it upon
10 // query param changes by setting `refreshModel:true` above.
11
12 // params has format of { category: "someValueOrJustNull" },
13 // which we can just forward to the server.
14 return this.store.findQuery('articles', params);
15 }
16 });
17
18 App.ArticlesController = Ember.ArrayController.extend({
19 queryParams: ['category'],
20 category: null
21 });
Update URL with replaceState instead
By default, Ember will use pushState to update the URL in the
address bar in response to a controller query param property change, but
if you would like to use replaceState instead (which prevents an
additional item from being added to your browser’s history), you can
specify this on the Route’s queryParams config hash, e.g. (continued
from the example above):
1 App.ArticlesRoute = Ember.Route.extend({
2 queryParams: {
3 category: {
4 replace: true
5 }
6 }
7 });
Note that the name of this config property and its default value of
false is similar to the link-to helper’s, which also lets
you opt into a replaceState transition via replace=true.
Map a controller’s property to a different query param key
By default, specifying foo as a controller query param property will
bind to a query param whose key is foo, e.g. ?foo=123. You can also map
a controller property to a different query param key using the
following configuration syntax:
1 App.ArticlesController = Ember.ArrayController.extend({
2 queryParams: {
3 category: "articles_category"
4 },
5 category: null
6 });
This will cause changes to the ArticlesController’s category
property to update the articles_category query param, and vice versa.
Note that query params that require additional customization can
be provided along with strings in the queryParams array.
1 App.ArticlesController = Ember.ArrayController.extend({
2 queryParams: [ "page", "filter", {
3 category: "articles_category"
4 }],
5 category: null,
6 page: 1,
7 filter: "recent"
8 });
Default values and deserialization
In the following example, the controller query param property page is
considered to have a default value of 1.
1 App.ArticlesController = Ember.ArrayController.extend({
2 queryParams: 'page',
3 page: 1
4 });
This affects query param behavior in two ways:
- Query param values are cast to the same datatype as the default
value, e.g. a URL change from
/?page=3to/?page=2will setArticlesController’spageproperty to the number2, rather than the string"2". The same also applies to boolean default values. - When a controller’s query param property is currently set to its
default value, this value won’t be serialized into the URL. So in the
above example, if
pageis1, the URL might look like/articles, but once someone sets the controller’spagevalue to2, the URL will become/articles?page=2.
Sticky Query Param Values
By default, query param values in Ember are “sticky”, in that if you make changes to a query param and then leave and re-enter the route, the new value of that query param will be preserved (rather than reset to its default). This is a particularly handy default for preserving sort/filter parameters as you navigate back and forth between routes.
Furthermore, these sticky query param values are remembered/restored
according to the model loaded into the route. So, given a team route
with dynamic segment /:team_name and controller query param “filter”,
if you navigate to /badgers and filter by "rookies", then navigate
to /bears and filter by "best", and then navigate to /potatoes and
filter by "lamest", then given the following nav bar links,
1 {{#link-to 'team' 'badgers '}}Badgers{{/link-to}}
2 {{#link-to 'team' 'bears' }}Bears{{/link-to}}
3 {{#link-to 'team' 'potatoes'}}Potatoes{{/link-to}}
the generated links would be
1 <a href="/badgers?filter=rookies">Badgers</a>
2 <a href="/bears?filter=best">Bears</a>
3 <a href="/potatoes?filter=lamest">Potatoes</a>
This illustrates that once you change a query param, it is stored and tied to the model loaded into the route.
If you wish to reset a query param, you have two options:
- explicitly pass in the default value for that query param into
link-toortransitionTo - use the
Route.resetControllerhook to set query param values back to their defaults before exiting the route or changing the route’s model
In the following example, the controller’s page query param is reset
to 1, while still scoped to the pre-transition ArticlesRoute model.
The result of this is that all links pointing back into the exited route
will use the newly reset value 1 as the value for the page query
param.
1 App.ArticlesRoute = Ember.Route.extend({
2 resetController: function (controller, isExiting, transition) {
3 if (isExiting) {
4 // isExiting would be false if only the route's model was changing
5 controller.set('page', 1);
6 }
7 }
8 });
In some cases, you might not want the sticky query param value to be
scoped to the route’s model but would rather reuse a query param’s value
even as a route’s model changes. This can be accomplished by setting the
scope option to "controller" within the controller’s queryParams
config hash:
1 App.ArticlesRoute = Ember.Route.extend({
2 queryParams: {
3 showMagnifyingGlass: {
4 scope: "controller"
5 }
6 }
7 });
The following demonstrates how you can override both the scope and the query param URL key of a single controller query param property:
1 App.ArticlesController = Ember.Controller.extend({
2 queryParams: [ "page", "filter",
3 {
4 showMagnifyingGlass: {
5 scope: "controller",
6 as: "glass",
7 }
8 }
9 ]
10 });
Examples
- Search queries
- Sort: client-side, no refiring of model hook
- Sort: server-side, refire model hook
- Pagination + Sorting
- Boolean values. False value removes QP from URL
- Global query params on app route
- Opt-in to full transition via refresh()
- update query params by changing controller QP property
- update query params with replaceState by changing controller QP property
- w/ {{partial}} helper for easy tabbing
- link-to with no route name, only QP change
- Complex: serializing textarea content into URL (and subexpressions))
- Arrays
Asynchronous Routing
This section covers some more advanced features of the router and its capability for handling complex async logic within your app.
A Word on Promises…
Ember’s approach to handling asynchronous logic in the router makes
heavy use of the concept of Promises. In short, promises are objects that
represent an eventual value. A promise can either fulfill
(successfully resolve the value) or reject (fail to resolve the
value). The way to retrieve this eventual value, or handle the cases
when the promise rejects, is via the promise’s then method, which
accepts two optional callbacks, one for fulfillment and one for
rejection. If the promise fulfills, the fulfillment handler gets called
with the fulfilled value as its sole argument, and if the promise rejects,
the rejection handler gets called with a reason for the rejection as its
sole argument. For example:
1 var promise = fetchTheAnswer();
2
3 promise.then(fulfill, reject);
4
5 function fulfill(answer) {
6 console.log("The answer is " + answer);
7 }
8
9 function reject(reason) {
10 console.log("Couldn't get the answer! Reason: " + reason);
11 }
Much of the power of promises comes from the fact that they can be chained together to perform sequential asynchronous operations:
1 // Note: jQuery AJAX methods return promises
2 var usernamesPromise = Ember.$.getJSON('/usernames.json');
3
4 usernamesPromise.then(fetchPhotosOfUsers)
5 .then(applyInstagramFilters)
6 .then(uploadTrendyPhotoAlbum)
7 .then(displaySuccessMessage, handleErrors);
In the above example, if any of the methods
fetchPhotosOfUsers, applyInstagramFilters, or
uploadTrendyPhotoAlbum returns a promise that rejects,
handleErrors will be called with
the reason for the failure. In this manner, promises approximate an
asynchronous form of try-catch statements that prevent the rightward
flow of nested callback after nested callback and facilitate a saner
approach to managing complex asynchronous logic in your applications.
This guide doesn’t intend to fully delve into all the different ways promises can be used, but if you’d like a more thorough introduction, take a look at the readme for RSVP, the promise library that Ember uses.
The Router Pauses for Promises
When transitioning between routes, the Ember router collects all of the
models (via the model hook) that will be passed to the route’s
controllers at the end of the transition. If the model hook (or the related
beforeModel or afterModel hooks) return normal (non-promise) objects or
arrays, the transition will complete immediately. But if the model hook
(or the related beforeModel or afterModel hooks) returns a promise (or
if a promise was provided as an argument to transitionTo), the transition
will pause until that promise fulfills or rejects.
If the promise fulfills, the transition will pick up where it left off and
begin resolving the next (child) route’s model, pausing if it too is a
promise, and so on, until all destination route models have been
resolved. The values passed to the setupController hook for each route
will be the fulfilled values from the promises.
A basic example:
1 App.TardyRoute = Ember.Route.extend({
2 model: function() {
3 return new Ember.RSVP.Promise(function(resolve) {
4 Ember.run.later(function() {
5 resolve({ msg: "Hold Your Horses" });
6 }, 3000);
7 });
8 },
9
10 setupController: function(controller, model) {
11 console.log(model.msg); // "Hold Your Horses"
12 }
13 });
When transitioning into TardyRoute, the model hook will be called and
return a promise that won’t resolve until 3 seconds later, during which time
the router will be paused in mid-transition. When the promise eventually
fulfills, the router will continue transitioning and eventually call
TardyRoute’s setupController hook with the resolved object.
This pause-on-promise behavior is extremely valuable for when you need to guarantee that a route’s data has fully loaded before displaying a new template.
When Promises Reject…
We’ve covered the case when a model promise fulfills, but what if it rejects?
By default, if a model promise rejects during a transition, the transition is aborted, no new destination route templates are rendered, and an error is logged to the console.
You can configure this error-handling logic via the error handler on
the route’s actions hash. When a promise rejects, an error event
will be fired on that route and bubble up to ApplicationRoute’s
default error handler unless it is handled by a custom error handler
along the way, e.g.:
1 App.GoodForNothingRoute = Ember.Route.extend({
2 model: function() {
3 return Ember.RSVP.reject("FAIL");
4 },
5
6 actions: {
7 error: function(reason) {
8 alert(reason); // "FAIL"
9
10 // Can transition to another route here, e.g.
11 // this.transitionTo('index');
12
13 // Uncomment the line below to bubble this error event:
14 // return true;
15 }
16 }
17 });
In the above example, the error event would stop right at
GoodForNothingRoute’s error handler and not continue to bubble. To
make the event continue bubbling up to ApplicationRoute, you can
return true from the error handler.
Recovering from Rejection
Rejected model promises halt transitions, but because promises are chainable,
you can catch promise rejects within the model hook itself and convert
them into fulfills that won’t halt the transition.
1 App.FunkyRoute = Ember.Route.extend({
2 model: function() {
3 return iHopeThisWorks().then(null, function() {
4 // Promise rejected, fulfill with some default value to
5 // use as the route's model and continue on with the transition
6 return { msg: "Recovered from rejected promise" };
7 });
8 }
9 });
beforeModel and afterModel
The model hook covers many use cases for pause-on-promise transitions,
but sometimes you’ll need the help of the related hooks beforeModel
and afterModel. The most common reason for this is that if you’re
transitioning into a route with a dynamic URL segment via {{link-to}} or
transitionTo (as opposed to a transition caused by a URL change),
the model for the route you’re transitioning into will have already been
specified (e.g. {{#link-to 'article' article}} or
this.transitionTo('article', article)), in which case the model hook
won’t get called. In these cases, you’ll need to make use of either
the beforeModel or afterModel hook to house any logic while the
router is still gathering all of the route’s models to perform a
transition.
beforeModel
Easily the more useful of the two, the beforeModel hook is called
before the router attempts to resolve the model for the given route. In
other words, it is called before the model hook gets called, or, if
model doesn’t get called, it is called before the router attempts to
resolve any model promises passed in for that route.
Like model, returning a promise from beforeModel will pause the
transition until it resolves, or will fire an error if it rejects.
The following is a far-from-exhaustive list of use cases in which
beforeModel is very handy:
- Deciding whether to redirect to another route before performing a
potentially wasteful server query in
model - Ensuring that the user has an authentication token before proceeding
onward to
model - Loading application code required by this route
1 App.SecretArticlesRoute = Ember.Route.extend({
2 beforeModel: function() {
3 if (!this.controllerFor('auth').get('isLoggedIn')) {
4 this.transitionTo('login');
5 }
6 }
7 });
See the API Docs for beforeModel
afterModel
The afterModel hook is called after a route’s model (which might be a
promise) is resolved, and follows the same pause-on-promise semantics as
model and beforeModel. It is passed the already-resolved model
and can therefore perform any additional logic that
depends on the fully resolved value of a model.
1 App.ArticlesRoute = Ember.Route.extend({
2 model: function() {
3 // App.Article.find() returns a promise-like object
4 // (it has a `then` method that can be used like a promise)
5 return App.Article.find();
6 },
7 afterModel: function(articles) {
8 if (articles.get('length') === 1) {
9 this.transitionTo('article.show', articles.get('firstObject'));
10 }
11 }
12 });
You might be wondering why we can’t just put the afterModel logic
into the fulfill handler of the promise returned from model; the
reason, as mentioned above, is that transitions initiated
via {{link-to}} or transitionTo likely already provided the
model for this route, so model wouldn’t be called in these cases.
See the API Docs for afterModel
More Resources
Loading / Error Substates
In addition to the techniques described in the
Asynchronous Routing Guide,
the Ember Router provides powerful yet overridable
conventions for customizing asynchronous transitions
between routes by making use of error and loading
substates.
loading substates
The Ember Router allows you to return promises from the various
beforeModel/model/afterModel hooks in the course of a transition
(described here).
These promises pause the transition until they fulfill, at which point
the transition will resume.
Consider the following:
1 App.Router.map(function() {
2 this.resource('foo', function() { // -> FooRoute
3 this.route('slowModel'); // -> FooSlowModelRoute
4 });
5 });
6
7 App.FooSlowModelRoute = Ember.Route.extend({
8 model: function() {
9 return somePromiseThatTakesAWhileToResolve();
10 }
11 });
If you navigate to foo/slow_model, and in FooSlowModelRoute#model,
you return an AJAX query promise that takes a long time to complete.
During this time, your UI isn’t really giving you any feedback as to
what’s happening; if you’re entering this route after a full page
refresh, your UI will be entirely blank, as you have not actually
finished fully entering any route and haven’t yet displayed any
templates; if you’re navigating to foo/slow_model from another
route, you’ll continue to see the templates from the previous route
until the model finish loading, and then, boom, suddenly all the
templates for foo/slow_model load.
So, how can we provide some visual feedback during the transition?
Ember provides a default implementation of the loading process that implements
the following loading substate behavior.
1 App.Router.map(function() {
2 this.resource('foo', function() { // -> FooRoute
3 this.resource('foo.bar', function() { // -> FooBarRoute
4 this.route('baz'); // -> FooBarBazRoute
5 });
6 });
7 });
If a route with the path foo.bar.baz returns a promise that doesn’t immediately
resolve, Ember will try to find a loading route in the hierarchy
above foo.bar.baz that it can transition into, starting with
foo.bar.baz’s sibling:
foo.bar.loadingfoo.loadingloading
Ember will find a loading route at the above location if either a) a Route subclass has been defined for such a route, e.g.
App.FooBarLoadingRouteApp.FooLoadingRouteApp.LoadingRoute
or b) a properly-named loading template has been found, e.g.
foo/bar/loadingfoo/loadingloading
During a slow asynchronous transition, Ember will transition into the first loading sub-state/route that it finds, if one exists. The intermediate transition into the loading substate happens immediately (synchronously), the URL won’t be updated, and, unlike other transitions that happen while another asynchronous transition is active, the currently active async transition won’t be aborted.
After transitioning into a loading substate, the corresponding template
for that substate, if present, will be rendered into the main outlet of
the parent route, e.g. foo.bar.loading’s template would render into
foo.bar’s outlet. (This isn’t particular to loading routes; all
routes behave this way by default.)
Once the main async transition into foo.bar.baz completes, the loading
substate will be exited, its template torn down, foo.bar.baz will be
entered, and its templates rendered.
Eager vs. Lazy Async Transitions
Loading substates are optional, but if you provide one, you are essentially telling Ember that you want this async transition to be “eager”; in the absence of destination route loading substates, the router will “lazily” remain on the pre-transition route while all of the destination routes’ promises resolve, and only fully transition to the destination route (and renders its templates, etc.) once the transition is complete. But once you provide a destination route loading substate, you are opting into an “eager” transition, which is to say that, unlike the “lazy” default, you will eagerly exit the source routes (and tear down their templates, etc) in order to transition into this substate. URLs always update immediately unless the transition was aborted or redirected within the same run loop.
This has implications on error handling, i.e. when a transition into another route fails, a lazy transition will (by default) just remain on the previous route, whereas an eager transition will have already left the pre-transition route to enter a loading substate.
The loading event
If you return a promise from the various beforeModel/model/afterModel hooks,
and it doesn’t immediately resolve, a loading event will be fired on that route
and bubble upward to ApplicationRoute.
If the loading handler is not defined at the specific route,
the event will continue to bubble above a transition’s pivot
route, providing the ApplicationRoute the opportunity to manage it.
1 App.FooSlowModelRoute = Ember.Route.extend({
2 model: function() {
3 return somePromiseThatTakesAWhileToResolve();
4 },
5 actions: {
6 loading: function(transition, originRoute) {
7 //displayLoadingSpinner();
8
9 // Return true to bubble this event to `FooRoute`
10 // or `ApplicationRoute`.
11 return true;
12 }
13 }
14 });
The loading handler provides the ability to decide what to do during
the loading process. If the last loading handler is not defined
or returns true, Ember will perform the loading substate behavior.
1 App.ApplicationRoute = Ember.Route.extend({
2 actions: {
3 loading: function(transition, originRoute) {
4 displayLoadingSpinner();
5
6 // substate implementation when returning `true`
7 return true;
8 }
9 }
10 });
error substates
Ember provides an analogous approach to loading substates in
the case of errors encountered during a transition.
Similar to how the default loading event handlers are implemented,
the default error handlers will look for an appropriate error substate to
enter, if one can be found.
1 App.Router.map(function() {
2 this.resource('articles', function() { // -> ArticlesRoute
3 this.route('overview'); // -> ArticlesOverviewRoute
4 });
5 });
For instance, an error thrown or rejecting promise returned from
ArticlesOverviewRoute#model (or beforeModel or afterModel)
will look for:
- Either
ArticlesErrorRouteorarticles/errortemplate - Either
ErrorRouteorerrortemplate
If one of the above is found, the router will immediately transition into
that substate (without updating the URL). The “reason” for the error
(i.e. the exception thrown or the promise reject value) will be passed
to that error state as its model.
If no viable error substates can be found, an error message will be logged.
error substates with dynamic segments
Routes with dynamic segments are often mapped to a mental model of “two separate levels.” Take for example:
1 App.Router.map(function() {
2 this.resource('foo', {path: '/foo/:id'}, function() {
3 this.route('baz');
4 });
5 });
6
7 App.FooRoute = Ember.Route.extend({
8 model: function(params) {
9 return new Ember.RSVP.Promise(function(resolve, reject) {
10 reject("Error");
11 });
12 }
13 });
In the URL hierarchy you would visit /foo/12 which would result in rendering
the foo template into the application template’s outlet. In the event of
an error while attempting to load the foo route you would also render the
top-level error template into the application template’s outlet. This is
intentionally parallel behavior as the foo route is never successfully
entered. In order to create a foo scope for errors and render foo/error
into foo’s outlet you would need to split the dynamic segment:
1 App.Router.map(function() {
2 this.resource('foo', {path: '/foo'}, function() {
3 this.resource('elem', {path: ':id'}, function() {
4 this.route('baz');
5 });
6 });
7 });
The error event
If ArticlesOverviewRoute#model returns a promise that rejects (because, for
instance, the server returned an error, or the user isn’t logged in,
etc.), an error event will fire on ArticlesOverviewRoute and bubble upward.
This error event can be handled and used to display an error message,
redirect to a login page, etc.
1 App.ArticlesOverviewRoute = Ember.Route.extend({
2 model: function(params) {
3 return new Ember.RSVP.Promise(function(resolve, reject) {
4 reject("Error");
5 });
6 },
7 actions: {
8 error: function(error, transition) {
9
10 if (error && error.status === 400) {
11 // error substate and parent routes do not handle this error
12 return this.transitionTo('modelNotFound');
13 }
14
15 // Return true to bubble this event to any parent route.
16 return true;
17 }
18 }
19 });
In analogy with the loading event, you could manage the error event
at the Application level to perform any app logic and based on the
result of the last error handler, Ember will decide if substate behavior
must be performed or not.
1 App.ApplicationRoute = Ember.Route.extend({
2 actions: {
3 error: function(error, transition) {
4
5 // Manage your errors
6 Ember.onerror(error);
7
8 // substate implementation when returning `true`
9 return true;
10
11 }
12 }
13 });
Legacy LoadingRoute
Previous versions of Ember (somewhat inadvertently) allowed you to define a global LoadingRoute
which would be activated whenever a slow promise was encountered during
a transition and exited upon completion of the transition. Because the
loading template rendered as a top-level view and not within an
outlet, it could be used for little more than displaying a loading
spinner during slow transitions. Loading events/substates give you far
more control, but if you’d like to emulate something similar to the legacy
LoadingRoute behavior, you could do as follows:
1 App.LoadingView = Ember.View.extend({
2 templateName: 'global-loading',
3 elementId: 'global-loading'
4 });
5
6 App.ApplicationRoute = Ember.Route.extend({
7 actions: {
8 loading: function() {
9 var view = this.container.lookup('view:loading').append();
10 this.router.one('didTransition', view, 'destroy');
11 }
12 }
13 });
This will, like the legacy LoadingRoute, append a top-level view when the
router goes into a loading state, and tear down the view once the
transition finishes.
Preventing And Retrying Transitions
During a route transition, the Ember Router passes a transition
object to the various hooks on the routes involved in the transition.
Any hook that has access to this transition object has the ability
to immediately abort the transition by calling transition.abort(),
and if the transition object is stored, it can be re-attempted at a
later time by calling transition.retry().
Preventing Transitions via willTransition
When a transition is attempted, whether via {{link-to}}, transitionTo,
or a URL change, a willTransition action is fired on the currently
active routes. This gives each active route, starting with the leaf-most
route, the opportunity to decide whether or not the transition should occur.
Imagine your app is in a route that’s displaying a complex form for the user to fill out and the user accidentally navigates backwards. Unless the transition is prevented, the user might lose all of the progress they made on the form, which can make for a pretty frustrating user experience.
Here’s one way this situation could be handled:
1 App.FormRoute = Ember.Route.extend({
2 actions: {
3 willTransition: function(transition) {
4 if (this.controller.get('userHasEnteredData') &&
5 !confirm("Are you sure you want to abandon progress?")) {
6 transition.abort();
7 } else {
8 // Bubble the `willTransition` action so that
9 // parent routes can decide whether or not to abort.
10 return true;
11 }
12 }
13 }
14 });
When the user clicks on a {{link-to}} helper, or when the app initiates a
transition by using transitionTo, the transition will be aborted and the URL
will remain unchanged. However, if the browser back button is used to
navigate away from FormRoute, or if the user manually changes the URL, the
new URL will be navigated to before the willTransition action is
called. This will result in the browser displaying the new URL, even if
willTransition calls transition.abort().
Aborting Transitions Within model, beforeModel, afterModel
The model, beforeModel, and afterModel hooks described in
Asynchronous Routing
each get called with a transition object. This makes it possible for
destination routes to abort attempted transitions.
1 App.DiscoRoute = Ember.Route.extend({
2 beforeModel: function(transition) {
3 if (new Date() < new Date("January 1, 1980")) {
4 alert("Sorry, you need a time machine to enter this route.");
5 transition.abort();
6 }
7 }
8 });
Storing and Retrying a Transition
Aborted transitions can be retried at a later time. A common use case for this is having an authenticated route redirect the user to a login page, and then redirecting them back to the authenticated route once they’ve logged in.
1 App.SomeAuthenticatedRoute = Ember.Route.extend({
2 beforeModel: function(transition) {
3 if (!this.controllerFor('auth').get('userIsLoggedIn')) {
4 var loginController = this.controllerFor('login');
5 loginController.set('previousTransition', transition);
6 this.transitionTo('login');
7 }
8 }
9 });
10
11 App.LoginController = Ember.Controller.extend({
12 actions: {
13 login: function() {
14 // Log the user in, then reattempt previous transition if it exists.
15 var previousTransition = this.get('previousTransition');
16 if (previousTransition) {
17 this.set('previousTransition', null);
18 previousTransition.retry();
19 } else {
20 // Default back to homepage
21 this.transitionToRoute('index');
22 }
23 }
24 }
25 });