Upcoming Async Router API

Hi all,

The Router API is getting a facelift very shortly. Most of the work is done already, but I wanted to give everyone a chance to give it a try before settling in on the final API.

Here are the relevant Pull Requests for the router.js microlib and Ember.js.

In short, the existing router API is awesome, but lacks in the async department; if your app needs any sort of logic, sync or async, to be performed before a transition takes to determine whether the transition should take place, you donā€™t have a lot of options, and at the very least, your code will suffer, and in many cases, what youā€™re after literally cannot be done way that yields a satisfying user experience.

In short:

  • transitionTo and handleURL (internal method for URL transitions) are async and return Promises that resolve/reject if the transition succeeds/redirects, respectively.
  • Ember.Route is getting some new transition validation hooks that get called before the routeā€™s actually entered.

At the time of writing, both the router.js and Ember code is finished and is passing test cases, so Iā€™d love it if everyone could give the code at the Ember code a try. Thereā€™s also a more in-depth description of the API in this gist, complete with a lot of live JSBin demos of cool stuff you can do with this API that couldnā€™t be (easily) done before.

This change probably wonā€™t go into the next release candidate, but definitely will go in before 1.0, so Iā€™d really like to make sure we do this thing right. Please have a look at the API and maybe get started toying with the examples in the above gist.

Lemme know:

  • Do the validation hooks have proper names?
  • How easy is it to grok this stuff?
  • What could be made easier?
  • How do you like the approaches to the solving the example problems?
6 Likes

I looked at the examples and they all look really cool, donā€™t have much need for them yet, but can definitely invision the need in the future.

One question that I donā€™t see answered anywhere, at least not prominentlyā€¦ Is does this break existing apps and if so in what way? A clear list of what needs to be changed / reworked in existing apps would be really helpful.

Thanks for the hard work!

True, I havenā€™t really made a list yet.

A few things come to mind now (itā€™ll be more definitive once weā€™re totally settled on the API)

  • redirect should still work the way most people expect, though itā€™s deprecated (itā€™ll spew a warning telling you to use validateTransition instead). Also, itā€™ll run before the route is actually officially entered, so there are some cases where apps depend on the router already been part-way into the transitioned-to route, and those might need a change.
  • transitionTo is async now, so if you have a transitionTo immediately followed by code that expects to run after the transition is in place, youā€™ll need to change it to transitionTo('foo').then(function() { doStuffAfterTransition() });.

This is going to make my life easier. +1 on all the great examples! I was having to do gymnastics before but this will allow me to use Emberā€™s features to take care of that pre-transition logic. Thanks guys :slight_smile:

1 Like

Iā€™m wondering what happens in the cases where the statechart is suspended (because of an async state) and you receive a valid event on one of the entered substates (or concurrent states). What happens in this case?

I think the promise should get rejected, no?

Oh, replace statechart with router and substates / states with routes if itā€™s confusing. Iā€™m thinking in terms of the design pattern that the routerā€™s implementing.

1 Like

Great question.

In the present state of the API, the destination routes arenā€™t considered ā€œenteredā€ until all promises have resolved, so (without doing anything extra along the way), your events would fire on the old routes until the transition has totally completed. If any events are fired that transition the router, the first transition promise will get rejected with a TransitionRedirected object (so, youā€™ll be able to distinguish between transition failures if need be).

There is some talk of an intermediate loading state that might be entered, but the API hasnā€™t been totally ironed out. Plus, the intermediate loading state behavior isnā€™t always desirable ā€“ you might want a situation where you click a click or navigate, and you donā€™t want it to transition until all the information is there.

Anyway, let me know your thoughts given the present state of the API and limitations/assumptions therein.

That seems completely correct to me. In my opinion, there shouldnā€™t be any intermediary states, since that would make the state transition a state which just is completely out of place in statechart land. I think the loading state falls under a different feature of ā€œrouteless routesā€, where the routes change the state of the application, but itā€™s not reflected in the URL.

I currently find how to implement these transitions to be obtuse. Iā€™d prefer to do the following instead:

App.BooksAppRoute = Em.Route.extend({
  // Upon entering the route,
  // load the module code for the books app.
  // When the code is fully loaded, then
  // the route is considered "active" and actions
  // are enabled on the route.
  enter: function () {
    return $.ajax('books.js');
  },
  // Anytime this route is exited, wait a second
  // before transitioning to the new route.
  exit: function () {
    return Ember.RSVP.Promise(function (resolve, reject) {
       Em.run.later(resolve, 1000);
    });
  }
});

As for the validateTransition, this brings up a similar (rejected) pull request in SproutCore: https://github.com/sproutcore/sproutcore/pull/920?source=cc

Itā€™s a long read, but I think itā€™s worth it, because it involves the fruits of a lot of development time and thought about how to make this stuff work. I am of the opinion that validating transitions seems crufty and smelly. Preventing actions from making transitions from being able to occur in a given route makes way more sense to me.

Hope this helps with some feedback.

This is great feedback. Iā€™ll be reading the PR and responding more fully in a bit.

One major thing: have you checked out the routeTo component of the new API? routeTo is the name of an event that gets fired on the current routes. If no one handles it (or itā€™s handled and then bubble to the root), then the transition is performed. This is exactly where youā€™d put logic to prevent a transition if, say, you were on a route with a half-filled out form and wanted the user to confirm before they navigated elsewhere. routeTo handlers are passed a TransitionEvent object that they can call .perform() on, or potentially save for later and call .perform() on later. Thereā€™s a ton of flexibility here and itā€™s one of the most crucial components of the API.

How does that sit with your understand of the lessons learned from SC times?

fwiw Iā€™m also delving into some competing approaches, a la https://github.com/afterglowtech/angular-detour . If youā€™ve had experience with this (or anything else related) before, itā€™d be great to hear how it went.

routeTo seems like a good hook to me. You could even just override the current routeTo function instead of adding the hook:

App.MyRouteThatDisallowsGlobalNavigation = Em.Route.extend({
  routeTo: function (event) {
    return false;
  }
});

Alex, insanely nice job! This is going to be really useful. You managed to keep it just as easy to use as the old API, but much, much more flexible.

I have a few comments:


Transition instantly with promises

How can I return a promise from a routeā€™s model, but let the transition occur right away? If it takes 500ms to load a specific record, I would in most cases rather have the transition to occur right away and show ā€œpartial dataā€, than let the UI stay at the old route until the record is resolved. With the new router API this is the default behavior when returning an unloaded DS.Model from a routeā€™s model, which happens automagically if the routeā€™s path is something like /:thing_id. See this example


Multiple async transitions

This one is more of a question. How does router.js handle when multiple transitions are waiting for promises to resolve, and they resolve in an unpredictable order?

Take this example. First click Bar (which will take 5 seconds to load), then click Foo (which will take 2 seconds to load). The user would expect the UI to transition to Foo after 2 seconds and never transition to Bar. And this seems to work. The question is, what are the rules for this? Does a new TransitionEvent ā€œcancelā€ all other outstanding transition events?

A case where it doesnā€™t seem to work, is when the user is already at Foo, and then hit Bar, then Foo. The following happens:

  • User clicks Bar
    • Loading text appears
  • User clicks Foo
    • Loading text goes away (since we are already at Foo, so there is no need to transition).
  • Barā€™s promise resolves after 5 seconds
    • UI is transitioned to Bar (not what we expected, since we clicked Foo last).

Again, big :+1: !!

@seilund Thank you so much; this is exactly the kind of discussion needed for such a change so Iā€™m glad you all are bringing this stuff up.

Transition instantly with promises

Youā€™re right; if model or the transitionTo context that you pass in is a ā€œthennableā€ (and object with a .then property, such as a promise), the transition will ā€œpauseā€ until the thennable has resolved, and then continue onward. When thing thatā€™s been tricky about solidifying this new api is that thereā€™s about 6000 permutations of options that you can set to describe how a transition ought to occur, so I/weā€™ve done our best to boil things down to their predictable conventions. One thing to keep in mind though is that likely around the time this code drops (rc6?) there will be changes to ember-data such that DS.Model will not longer have a .then property; the fact that it ever did have a .then is strange, and almost everyoneā€™s in agreement that promises (thennables) should represent discrete actions that succeed or fail, rather than represent the entire entities themselves. So, with this split (which again will probably happen the same time as this router code is released), the transition behavior can be configured depending on whether you provide a thennable (slow transition) or a DS.Model (fast/classic transition). Iā€™m not concerning myself with how the persistence libraries like ember-data or ember-model expose both options, but Iā€™ve heard the following proposed: a) make DS.Model#find return a promise and make DS.Model#findRecord return a (possibly unloaded) record without a .then, or b) have DS.Model#find return a then-less record, but with a .loadingPromise, which is a thennable that you could pass to the router for a slow transition.

TL;DR: if you want a fast transition, you need to pass the router an object without a .then property (or make sure the .then resolved immediately. If you want fast transitions, you are also necessarily opting into uglier/clunkier redirect semantics (to catch instances where your record errors out, youā€™ll need to do something like listen for failures, and then cause a redirect, at which point youā€™ve already flashed the new UI to the screen, etc etc etc).

Also, if we reaaaally wanted it, I could expose a chainable modifier to transitionTo and routeTo that would force the transitions to be fast, e.g. transitionTo('foo').immediate().then(success), but Iā€™m extremely reluctant to expose that before fully considering the primitives/alternatives.

Multiple async transitions

The rule is that once you begin validating a transition (e.g. the phase where prepareTransition, etc., hooks are being called), any previous transition attempts will be cancelled.

The use case you described at the end is a bug, and I think Iā€™ve already fixed it in the latest iteration (which Iā€™ll hopefully push in a few hours). Thank you for pointing it out!

@machty great answers.

Transition instantly with promises

Sounds reasonable. In the data framework that I use for Ember, records are not promises, so I wonā€™t have this problem anyway. And I agree that records shouldnā€™t be promises themselves. So if Ember Data changes this behavior I donā€™t think this issue is a problem at all.


Multiple async transitions

Sounds good!


Iā€™m looking forward to this feature landing in master. Iā€™ll update Ember Animated Outlet to work with the async routing in some cases, instead of the hacks I had to do in the old router. I also showed the changes to the rest of our team, and they were impressed by your work, too :slight_smile:

Sebastian

@seilund Iā€™m halfway through updating the examples on that gist with the latest iteration; can you confirm that http://jsbin.com/ipehoj/2/edit is working the way youā€™d expect? (i believe it fixes the bug you pointed out)

@machty Confimed, it works!

Check out the latest: https://gist.github.com/machty/5647589

Thereā€™s some bugfixes, but also a slight tweak of the API to make things more backwards compat:

  • validation hooks now get a transitionEvent that can be saved/refired later
  • modelFor will look up resolved parent models in the present transition (before the api required that you look up the object in some resolvedParentContexts object passed to each hook

This has made the authentication example way more sexy: http://jsbin.com/axarop/25/edit

Lemme know them thoughts! Weā€™re getting really close.

TODO:

  • Prevent URL changes between, say, /foo/5 and /foo/6 from refiring all the parent .model hooks. This is left over behavior from the old router
  • Even more test coverage

Can you clarify this?

The API facelift his gone to great lengths to separate the phase in which routes and their validation/model promises are being resolved from the phase where routes are actually being fully entered with their resolved contexts. Beforehand, you had a mishmash of stuff being stored on Routes even though they werenā€™t fully entered, and a lot of the code suffered because of it.

That said, if youā€™re transitioning into /posts/123/comments/456, the CommentsRoute may very well need the resolved Post object from a parent route to do its thing. In the previous iteration of the API, you could look up the resolved post object in a parameter passed to each of the validation hooks called resolvedParentModels, but that API was kinda crappy and that parameter has since been replaced with a transitionEvent parameter. Instead of looking up this during-transition resolved parent model, I just made Route#modelFor smart enough to try and look up any parent models on the current transitionEvent if weā€™re in mid-transition.

So, if CommentsRoute needed the parent post, it can just do this.modelFor('post') (which is how it always worked in the past).

Hi all,

Iā€™m still updating the examples gist to the latest, but here is a guide to the new router facelift and how it works, starting with the simpler concepts and moving to the more advanced use cases. Please let me know how it reads, and of course, what you think of the API.

https://gist.github.com/machty/5723945

And insane amounts of thanks to @seilund for helping ironing out the last remaining awkwardness.

2 Likes

Really great work @machty. And thanks for collecting all this feedback and feeding it into the pull request.

I think this will be the reason why we will have to update Ember.js for our app. Even if it will certainly break in some ways, I think this will allow us to remove some workaround. As I already said, I donā€™t know if with this facelift, all corner cases are handled, but it seems definitely better than what we have today.

That beeing said, Iā€™ve just a question concerning the view rendering part of the router. I want to know if this behave exactly the same way, and when does it occur. For example, here,

I think about master/detail case, would the posts view beeing rendered even if we transitionTo an other route (if I understand well, it aborts the current transition) ? If not, what would be the way of going to the first item of a collection when the collection has been resolved ? (perhaps it just easy and I just dumbā€¦)