Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@machty
Last active November 11, 2023 06:44
Show Gist options
  • Star 71 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save machty/5723945 to your computer and use it in GitHub Desktop.
Save machty/5723945 to your computer and use it in GitHub Desktop.
Guide to the Router Facelift

Ember Router Async Facelift

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.

Why?

  • 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

Solution

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.

willTransition

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();
      }
    }
  }
});

model and Friends

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).

Error handling with error event

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.

Global Error Handling

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.

beforeModel and afterModel

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]);
    }
  }
});

Transitions as Promises

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);
});

Abort and retry transitions

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).

Advanced: Promises in hooks for fine-grained async/error handling

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)
    });
  }
});
@huafu
Copy link

huafu commented Aug 14, 2013

Ok got it working in a weird way, take a look at the SO question, i've answered my own question, not sure it's the best way, just lemme know if it has to be like this or not ;-)

http://stackoverflow.com/questions/18222762/best-practices-with-deep-routes-in-ember-with-ember-data

Copy link

ghost commented Sep 5, 2013

Love the new hooks, but I'm seeing that the application template is not being rendered until the beforeModel hook has resolved. Is this expected? In my app, the application template was showing a splash screen, which was disabled after the model was loaded. Any suggestions on how to do that with the latest router? Thanks, Andrew

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment