How do I handle canceling the *first* transition on page load?


#1

I have a router with two routes:

App.Router.map(function() {
  this.route("dashboard", { path: "/" });
  this.route("message", { path: "/messages/:id" });
});

If the user navigates directly to /messages/33, but message 33 doesn’t exist or they don’t have permission to view it, the MessageRoute will return a promise that rejects, canceling the transition. That leaves the router in an invalid state. How do I tell the router to go to / – a.k.a. dashboard – if that initial transition aborts?

I thought of using replaceWith:

App.MessageRoute = Em.Route.extend({
  model: function(params) {
    // return a promise that always succeeds, but possibly with a null model:
    return new RSVP.Promise(function(resolve, reject) {
      App.Message.fetch(params.id).then(resolve, resolve.bind(undefined, null));
    });
  },

  afterModel: function(message) {
    if (message == null) { this.replaceWith('dashboard'); }
  }
});

But that doesn’t work because you could be coming from /messages/32, which does exist.

The only other option seems to be checking the sequence of the transition:

afterModel: function(message, transition) {
  if (message == null) {
    if (transition.sequence === 1) { return this.replaceWith('dashboard'); }
    return transition.abort();
  }
}

#2

For now, I’ve done the following:

Ember.Route.reopen({
  abortOrHome: function(transition) {
    if (transition.sequence === 1) {
      this.replaceWith('dashboard');
    } else {
      transition.abort();
    }
  }
});

(Technically, I’ve put that in our own App.RouteSupport, which is mixed in to all of our routes, but the logic is the same.)


#3

If you’re looking to intercept the first visit to your application, you can transition to dashboard from the activate hook on the Application route: http://jsbin.com/fehayaza/2/

In your first post you have some routing logic based on errors. If you want to catch an error for a route, you can add a handler in the route’s actions, and transition from there: http://jsbin.com/fehayaza/3/

Note that errors bubble up, so if you don’t catch the error in the route where it occurred, then you can catch it in a parent route. Else Ember will redirect to the ErrorRoute route if defined: http://jsbin.com/fehayaza/4/


#4

Yes, I can do the redirecting in ApplicationRoute#activate, but that happens before MessageRoute#model, so it can’t know whether you’re going to a message that doesn’t exist.

Still, the error handler needs to know whether this is the first transition so it can figure out whether to call replaceWith or not.

The fundamental issue is that abort (or returning a promise that rejects), by itself, is bad behavior when transition.sequence === 1 because you’ll be left with a blank page.


#5

Perhaps I don’t fully understand the problem. Do these three scenarios cover the intended behaviour?

  • Load application via /messages/32 (ok)
  • Load application via /messages/33 (error, redirect to /dashboard)
  • Load application via /messages/32 (ok) and navigate to /messages/33 (error, stay on /messages/32)

#6

They cover it very well!


#7

I’m guessing your application gets stuck in the loading state, as in trying to access message 2 in this example: http://jsbin.com/fehayaza/6/

The obvious answer is not to show links to things that don’t exist, e.g. in pagination disable the “next page” button on the last page. I’m not foolish enough to suppose you haven’t considered this (boy can people on Q/A sites ever irk me). The record may have been deleted on the server before the user navigates to it, or it may be impractical to determine the availability of records ahead of time. Also the user can always navigate to items directly through the URL.

I recommend keeping a reference to the last successful transition, e.g. the transition to message 32, and retry the last successful transition when the next fails. If you want this behaviour across the entire application, then this could be done by setting a lastTransition property on the application, probably in the afterModel hook of all routes (using a mixin) which receives the current transition as the second argument: http://jsbin.com/fehayaza/7/ (take a look at afterModel and the error handler)

Not saying it is the best approach but it is workable, and you can adapt this to confine the behaviour to a particular route if needed.

If someone comes up with a better solution, I’ll learn something too.


#8

Note, if you use a mixin on routes to register the last transition, then anywhere you override the afterModel hook, be sure to call this._super(model, transition);

http://jsbin.com/fehayaza/9/


#9
Ember.Route.reopen({   
  abortOrHome: function(transition) {
    if (transition.sequence === 1) {
      this.replaceWith('dashboard');
    } else {
      transition.abort();
    }   } });

When using transition.sequence on integration tests, we need to reset Transition.currentSequence on teardown callbacks.

import {Transition} from 'router/transition';

teardown: function() {
  .....
  Transition.currentSequence = 0;
}