tildeio/router.js — pull request #19 by machty (2013-05-19 20:13:25+00:00)
Title: All transitions are now async

Previously, `transitionTo` was sync, while `handleURL` was
async if the target handlers' `deserialize` returned a
thennable. This sometimes sync / sometimes async approach
led to a lot of subtle problems with maintaining consistent
behavior between transitions and page-refreshes or back/forward
navigation. Issues include:

- Ember's `redirect` hook is neutered for transitionTo with
promise contexts since it will evaluate even before the
promise has had a chance to resolve. Of course, you can
manually put a .then handler on the object before you call
transitionTo, but this is needless complicated, and your
code is now forced to implement two styles of async transitions,
in in the case of a failure, you will have already fully changed
the router, rendered templates, etc etc etc.
- You can't perform any logic before the route's entered
to determine whether the transition should be halted or
redirected (unless you're in handleURL, which is a
minority of the time). This also leads to a lot of
complicated redirect tracking logic that's unfortunately
had to make its way into Ember.
- Generally speaking, any sort of authentication-based application
will get bitten by these inconsistencies in a manner that can't
actually be hacked/worked around in a satisfactory manner
(many, many people can confirm this).

The solution to all of the bizarre corners cases is to embrace
async, make it the expected/assumed case, and unify the behavior
between URL changes and direct/explicit transitionTo's. Specifically,
this PR has done the following:

`transitionTo` and `handleURL` are async
========================================

`transitionTo` and `handleURL` are both async operations that
return a promise that resolves if the transition ran to completion
and rejects if it failed. So, clients must change things like

router.transition('foo', Foo.find(123));
someOperationThatDependsOnTransitionHavingTakenPlace();

into:

router.transition('foo', Foo.find(123)).then(function() {
someOperationThatDependsOnTransitionHavingTakenPlace();
});

API Alteration: `deserialize` ALWAYS called on entered handlers
===============================================================

`deserialize` is now always called on each handler that's
attempting to be entered. This is true for both `handleURL` AND
`transitionTo`, even when a context has been supplied for a
given handler in a `transitionTo` transition. `deserialize`
used to only take a single argument, a hash of URL parameters
that it could use to deserialize; now it takes three: the URL
params hash, which is null if it's a `transitionTo` transition,
a `providedContext` parameter if it's a `transitionTo` transition
with a context provided for this handler, and thirdly, a
`resolvedContexts` object which contains all already-resolved
parent handler contexts for this attempted transition (as opposed
to the previous weird approach of relying on `context` having been
set on the not-yet-fully-entered route).

var postHandler = {
deserialize: function(urlParams, providedContext, resolvedContexts) {
if (urlParams) {
// Must be a URL-based transition.
return Post.find(urlParams.post_id);
} else {
// Must be `transitionTo`. We could just return
// providedContext, but lets perform some async validation
// on it first before we improperly enter the route
return new RSVP.Promise(function(resolve, reject) {
// ... do some ajax or something...
resolve(providedContext);

// ... or, if invalid:
reject(providedContext);
});
}
}
};

If you are crying, please keep in mind that in Ember, `deserialize`
is already private API. The plan is Ember will do the heavy lifting
to split `deserialize` into three hooks that can use to easily unify
behavior between `transitionTo`s and `handleURL`s:

beforeModel: function() {
// Put validation logic in here that can happen
// without any knowledge specific to the context object.
// So, for instance, you can check that your auth token is
// still valid.
if (somethingBad) {
this.transitionTo('login');
}
// ... or ...
return new RSVP.Promise(function(res, rej) {
// None shall pass... before 3 seconds have transpired.
setTimeout(function() {
res();
}, 3000);
});
},

model: function(urlParams, providedContext, resolvedContexts) {
// Same hook as before, maintains backwards compatibility.
return Post.find(123);
},

afterModel: function(model) {
// Put any logic in here that you used to put in `redirect`,
// which is any logic that can happen after the `model`
// promise has resolved.
}

Hopefully it's clear how this approach can be used to share
route entry validation logic between `transitionTo` and
`handleURL` in a way that is fully capable to deal with async.

Other Notes
===========

`loading` and `failure` handler logic behaves the same as it
did before, but given that `transitionTo` is now a promise that
you can call `.then` on for a more custom error-handling approach,
the classic `failure` method may fall out of style. We'll see.

TODO
====

- README.md to document the API changes.
- Preparing Ember for this change