The Ember router is getting number of enhancements that will greatly enhance its power, reliability, predictability, and ability to handle asynchronous loading logic (so many abilities), particularly when used in conjunction with promises, though the API is friendly enough that a deep understanding of promises is not required for the simpler use cases.
- Semantic differences between app-initiated transitions and URL-initiated transitions made it very challenging in certain cases to handle errors or async logic
- Authentication-based apps were especially difficult to implement
redirect
was sometimes called when a promise model was resolved, sometimes not, depending on in-app/URL transition
The solution was to embrace async and make router transitions first
class citizens. In the new API you are provided with the necessary hooks
to prevent/decorate transition attempts via a Transition
object passed
to various hooks. These hooks are:
willTransition
events fired on current routes whenever a transition is about to take place.beforeModel/model/afterModel
hooks during the async validation phase.
All transitions types (URL changes and transitionTo) will fire a
willTransition
event on the currently active routes. This gives
presently active routes a chance to conditionally prevent
(or decorate) a transition. One obvious example is preventing navigation
when you're on a form that's half-filled out:
App.FormRoute = Ember.Route.extend({
events: {
willTransition: function(transition) {
if (!this.controller.get('formEmpty')) {
transition.abort();
}
}
}
});
Previous iterations of the router exposed a hook called redirect
,
which gave you the opportunity to transitionTo
another route, thus
aborting the present transition attempt. The problem with this is that
when async data was involved, the behavior between transitionTo/linkTo
behavior and URL navigation behavior was very different and not easily
predictable. For instance, reloading the page or navigating
with the back/forward buttons between routes with promises as models
would pause the transition until the promise resolved (or rejected), but
calling transitionTo
would not pause the transition. This means that
in some cases, redirect
would be called with loaded models and in
other cases, the data used to the decide whether the transition should
be redirected wouldn't be loaded by the time redirect
was called.
This was problematic and often resulted in multiple, repetitive
approaches to handling errors / redirect logic.
In this router iteration, transitionTo
and URL changes behave the same
way, in that any models provided via transitionTo
or any models
returned from the model
hook will pause the transition if the model has a
.then
property (which indicates that it's a promise).
So what happens if a promise rejects? In previous iterations, this
would look for a FailureRoute
defined on your app namespace and then
run its enter
/setup
handlers, essentially treating it as a global
handler for all transition failures. Nowadays, you'll define
an error
event handler within the events
hash on Ember.Route
,
which gets called if a promise returned from
model
(or the one you provided in transitionTo
) rejects. (fwiw,
it'll also fire if any errors are thrown in the model
hook.) The
error
handler will be passed the error value/reason.
App.PostsIndexRoute = Ember.Route.extend({
model: function(params, transition) {
// Presently, this returns a promise-like object (it has
// a `.then` property).
return App.Post.find(123);
},
events: {
error: function(reason, transition) {
alert('error loading posts!');
this.transitionTo('login');
}
}
});
This allows you to keep your error handling in one place; regardless of
whether you URL-navigated into the route or you call
transitionTo('posts.index', App.Post.find(123))
, if there's an error
with the model promie, the same error
hook will get called.
Last but not least, since error
is an event, errors thrown (or
promises rejected) from leafier routes that don't have error
handlers defined will bubble up to the nearest parent with
error
defined. This allows you to share common error handling
logic between a hierarchy of routes. Note that if so desired, you
can continue to bubble an error (or any event) by returning true
from the handler.
We got rid of FailureRoute
, which was barely documented, kind of
misleading, and not all that useful (most people just used it for
redirecting anyway). But if you still want global shared error handling
logic, you can just define the error
handler on ApplicationRoute
.
App.ApplicationRoute = Ember.Route.extend({
events: {
error: function(reason, transition) {
this.controllerFor('banner').displayError(reason);
}
}
});
Note that if you don't specify your own, a default handler will be supplied that just logs the error and throws an exception on a setTimeout to escape from the internal promise infrastructure.
Oftentimes, you might have enough information to redirect or abort a
transition before ever trying to query the server. One example is if the
user follows a link to "/posts/123" but has never retrieved an auth
token to view these private posts, you don't want to waste a query to
the server and have to wait for its return value to come back. Rather,
you can make this check in the beforeModel
hook:
App.PostsIndexRoute = Ember.Route.extend({
beforeModel: function(transition) {
if (!this.controllerFor('auth').get('token')) {
this.transitionTo('login');
}
}
});
On the flip side, there might be some redirect logic that can only take
place after a router's model promise has totally resolved, in which case
you'd want to use the afterModel
hook.
App.PostsIndexRoute = Ember.Route.extend({
afterModel: function(posts, transition) {
if (posts.length === 1) {
this.transitionTo('post.show', posts[0]);
}
}
});
transitionTo
returns a Transition
object, and Transition
is a
promise (it has a .then
property that you can attach resolve/reject
handlers to). If you need to run code after a transition has succeeded
(or failed), you have to use .then
. Example:
// From, say, an event in the `events` hash of an `Ember.Route`
var self = this;
this.transitionTo('foo').then(function() {
self.router.send('displayWelcomeMessage');
}, function(reason) {
logFailure(reason);
});
You can abort an active transition by calling .abort()
on it. This
will halt a transition without redirecting there. Note that performing
another transitionTo
while a previous transition is in process will
immediately cancel the previous transition (so you don't need to call
.abort()
on the original transition before invoking a new
transitionTo
).
If you save a reference to a transition, you can re-attempt it later
by calling .retry()
on it, which returns a new Transition
object
(which you can call .abort()/retry()
on, etc).
The aforementioned beforeModel/model/afterModel
hooks already give you
the opportunity to abort or redirect the transition elsewhere, but they
also let you manage some pretty complex async operations and error
handling if you have an understanding of promises.
If you return a promise from any of these hooks, the transition won't
proceed to the next step until that promise resolves, and if it rejects,
the error
hook will be called with its reject value. If all the
possible errors that can go wrong can be well-handled in the single
error
hook, then look no further, but if, say, you perform very
particular async logic in beforeModel
or afterModel
, and you want
their errors to be handled in a particular way, you can return a promise
that's already had .then
called on it, e.g.
App.FunkRoute = Ember.Route.extend({
beforeModel: function(transition) {
var self = this;
// Load some async Ember code that FunkRoute needs.
return loadAsyncCodePromise().then(null, function(reason) {
// We can do any number of things here, with different
// implications:
// 1) self.transitionTo('elsewhere');
// - aborts the transition, redirects to elsehere
// 2) transition.abort();
// - aborts the transition
// 3) return Ember.RSVP.reject("some reason");
// - aborts the transition, calls the `.error` hook with
// "some reason" as the error
// 4) throw "some reason";
// - does the same as (3)
});
}
});
Will there still be the global
FailureRoute
or something like that still or does every route have to handle errors?