Skip to content

Instantly share code, notes, and snippets.

@machty
Last active April 16, 2020 22:03
Show Gist options
  • Star 112 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save machty/5647589 to your computer and use it in GitHub Desktop.
Save machty/5647589 to your computer and use it in GitHub Desktop.
How to do cool stuff with the new Router API

Update 6/14/13

Router Facelift Examples

Guide to latest API enhancements router.js PR Ember PR Latest Ember build with these changes

Below are some examples of how to use the newly enhanced async router API.

How do I prevent a transition when a form is half filled out?

Catch the Transition in FormRoute's willTransition event handler and call .abort() on the provided transition.

Demo

Note: if you intercept a URL transition, your URL will get out of sync until the next successful transition. Not really sure there's a perfect solution for this for now, but either way, I'm punting on it for now.

How do I put up a (global) Loading Spinner during a transition w/ Promises?

Existing LoadingRoute behavior is preserved: define LoadingRoute which displays a spinner in enter/setup and removes it in exit.

Demo

How can I let the destination route define the UI?

e.g. we're entering the AdminRoute from some other route, and we want Admin route to put up some UI that says "Loading Admin Panel".

You can use the above setup, combined with logic in the destination route's validation hooks, e.g:

AdminRoute = Ember.Route.extend({
  beforeModel: function() {

    displayAdminRouteLoadingUI();

    // Pretend this method exists. You can dig
    // into container if you want.
    this.routeFor('loading').one('doneLoading', this, function() {
      hideAdminRouteLoadingUI();
    });
  }
});

Here's another approach that puts this logic in the model hook (which makes sense in this case since it's a non-dynamic route, which means you can't pass a context to it via transitionTo).

Demo

How do I approximate the pivot loading behavior of old router?

This is beyond the scope of this iteration. Soon, though.

How do I load code async?

Use the beforeModel hook (the first one fired when transitioning into a route). Return a promise from this hook to pause the transition while the code loads.

App.ArticlesRoute = Ember.Route.extend({
  beforeModel: function() {
    if (!App.Article) {
      return $.getScript('/articles_code.js');
    }
  }
});

Basically, the beforeModel in this case should augment global state (e.g. make Articles code available to the container or global App namespace or whatever) so that if the promise resolves, the model hook or the transitionTo-provided context should be able to resolve under the assumption that that code has been loaded.

Demo

How do I redirect to a login form for an authenticated route and retry the original transition later?

The easiest way to do this is save the transition object passed to the beforeModel hook before redirecting to a login route. Then the login route can check for that saved transition and resume it later.

App.AuthenticatedRoute = Ember.Route.extend({
  beforeModel: function(transition) {
    if (!authTokenPresent) { 
      return RSVP.reject();
      // Could also just throw an error here too...
      // it'll do the same thing as returning a rejecting promise.

      // Note that we could put the redirecting `transitionTo`
      // in here, but it's a better pattern to put this logic
      // into `error` so that errors with resolving the model
      // (say, the server tells us the auth token expired)
      // can also get handled by the same redirect-to-login logic.
    }
  },

  error: function(reason, transition) {
    // This hook will be called for any errors / rejected promises
    // from any of the other hooks or provided transitionTo promises.

    // Redirect to `login` but save the attempted Transition
    var loginController = this.controllerFor('login')
    loginController.set('afterLoginTransition', transition);
    this.transitionTo('login');
  }
});

App.LoginController = Ember.Controller.extend({
  loginSucceeded: function() {
    var transition = this.get('afterLoginTransition');
    if (transition) {
      transition.retry();
    } else {
      this.transitionToRoute('welcome');
    }
  }
});

Check out the demo below which takes this approach:

Demo

How do I disable transitions while saving?

Override willTransition handler on leaf route where items are saved, save the transition for later, and then retry it once saving is complete.

App.ThingRoute = Ember.Route.extend({
  events: {
    willTransition: function(transition) {
      if (this.controller.get('isSaving')) {
        this.controller.set('afterSaveTransition', transition);
      }
    }
  }
});

App.ThingController = Ember.Controller.extend({
  afterSave: function() {
    var transition = this.get('afterSaveTransition');
    if (transition) {
      this.set('afterSaveTransition', null);
      transition.retry();
    } else {
      this.transitionToRoute('default');
    }
  }
});

Demo

How do I handle errors?

Here's the docs straight from the PR for the events hash in Ember.Route:

/**
  ## Bubbling

  By default, an event will stop bubbling once a handler defined
  on the `events` hash handles it. To continue bubbling the event,
  you must return `true` from the handler.

  ### `error`

  When attempting to transition into a route, any of the hooks
  may throw an error, or return a promise that rejects, at which
  point an `error` event will be fired on the partially-entered
  routes, allowing for per-route error handling logic, or shared
  error handling logic defined on a parent route. 
  
  Here is an example of an error handler that will be invoked
  for rejected promises / thrown errors from the various hooks
  on the route, as well as any unhandled errors from child
  routes:

  ```js
  App.AdminRoute = Ember.Route.extend({
    beforeModel: function() {
      throw "bad things!";
      // ...or, equivalently:
      return Ember.RSVP.reject("bad things!");
    },

    events: {
      error: function(error, transition) {
        // Assuming we got here due to the error in `beforeModel`,
        // we can expect that error === "bad things!",
        // but a promise model rejecting would also 
        // call this hook, as would any errors encountered
        // in `afterModel`. 

        // The `error` hook is also provided the failed
        // `transition`, which can be stored and later
        // `.retry()`d if desired.

        this.transitionTo('login');
      }
    }
  });
  ```

  `error` events that bubble up all the way to `ApplicationRoute` 
  will fire a default error handler that logs the error. You can
  specify your own global defaut error handler by overriding the 
  `error` handler on `ApplicationRoute`:

  ```js
  App.ApplicationRoute = Ember.Route.extend({
    events: {
      error: function(error, transition) {
        this.controllerFor('banner').displayError(error.message);
      }
    }
  });
  ```

  @see {Ember.Route#send}
  @see {Handlebars.helpers.action}

  @property events
  @type Hash
  @default null
*/
@jaaksarv
Copy link

jaaksarv commented Jul 5, 2013

@machty thanks for the hard work, now we can really start implementing proper authentication logic.

But I have same problem as @kylenathan and @domasx2. When I go directly to dynamic nested route eg. /articles/1 then I get nicely redirected to login. The transition passed to login is also correct and has article_id=1 passed as param. But when the login resolves both the ArticlesRoute.model and ArticleRoute.model gets called. The later is called with article_id==undefined. If I ignore the error then it works after all articles are loaded by ArticlesRoute.model, but I think that the ArticleRoute.model should not get called at all as we area already loading all models in ArticlesRoute.model.

@jaaksarv
Copy link

jaaksarv commented Jul 5, 2013

I dug deeper into the problem is in ember-data 0.13 that is not using promises yet. So calling just 'return App.Article.find()' from the model hook will trigger the afterModel instantly. Is this correct? Any workarounds until we get next version of ember-data?

@jaaksarv
Copy link

jaaksarv commented Jul 8, 2013

I created a JSBin of the problem.
http://jsbin.com/efulor/1#/articles/1
After login we should be redirected back to /articles/1, but instead it redirects to /articles/undefined

@jaaksarv
Copy link

jaaksarv commented Jul 8, 2013

Second problem I have with ember-data and afterLogin hook.
http://jsbin.com/umoqob/1/
The articles route should redirect to first article, but it is not working on first load. When visiting articles route second time (going back to index for example) then works fine.

@machty
Copy link
Author

machty commented Jul 9, 2013

@jaaksarv @kylenathan @domasx2 Can you confirm you still run into this on ember master?

@jaaksarv
Copy link

@machty Is there a nighly build I can check out? I currently don't have tools for building Ember master, but I can install if needed. You can easily check it out yourself also by replacing the ember version in my jsbins.

@bschaeffer
Copy link

This is great.... but how do you handle Router 404s, not just model/promise errors?

@jaaksarv
Copy link

@machty I tried out with ember-latest. The login redirect problem is now solved:
http://jsbin.com/efulor/8#/articles/1

But I still have a problem with redirect to first article from afterLogin hook. Maybe doing something wrong.
http://jsbin.com/umoqob/3

Thanks for helping:)

@andreisoare
Copy link

@jaaksarv isn't this the issue you're having? emberjs/data#1013

@samselikoff
Copy link

I'm having trouble getting the global loading route working. It works on initial app load, but it doesn't show for the rest of my transitions. I'm using transitionToRoute in a view, rather than a {{#linkTo}} helper... is that a problem?

edit: didn't realize the LoadingRoute isn't a 'child' of the ApplicationRoute. I'm all set.

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