Skip to content

Instantly share code, notes, and snippets.

@machty machty/router.md
Last active Dec 16, 2015

Embed
What would you like to do?
The smallest possible router refinement.

Proposal

  1. No longer encourage/allow external calls to a Route's transitionTo method.
  2. Instead, make use of a new routeTo event, with the same semantics as transitionTo, only that it's an event that routes can respond to.
  3. ApplicationRoute will have a default routeTo handler which passes its args to an internal transitionTo.
  4. URL changes will be converted into a routeTo event event.

Motivation

This is an attempt at the simplest possible primitive to allow routes to halt, redirect, or decorate transitions, while maintaining a grounded state-based approach to handling these scenarios. Beforehand, the state machine paradigm was broken by controllers and linkTo ruthlessly commanding the router to change to a particular state via transitionTo, but with state machines, the only interaction from the outside world should be with events. Hence the routeTo event.

There's been been a lot of talk about preventable transitions, but the end result of this proposal is no specific solution to such a thing, but rather just the primitives to allow for sensible solutions to this problem down the road.

The Hard Part: URL-based Transitions

It's simple enough to convert your app's transitionTo's, if they exist, into routeTo events, since their semantics are the same, but transitions via URL have the following complications:

  • They must deserialize a requested URL into context objects that will be passed to the routes. (With transitionTo, you directly provide these context objects.)
  • Deserialization of leafier route parameters must happen after parent routes' deserialized promise contexts have resolved, since a leafier deserialization may depend on the data of a resolved parent promise.
  • Since the goal is to convert to a routeTo event that can be intercepted/handled by any route, there needs to be a way to avoid immediately deserializing these context objects and potentially causing server requests when a routeTo handler doesn't need that information to know to halt / redirect the transition. For example, if I'm on a route with a half-filled in form, and I navigate to /posts/123, the form route ought to be able to handle the generated routeTo event and prevent it from bubbling, without the post route's deserialize being called and firing a request to the server to load data for Post 123.

Possible Solution 1: Context Resolvers

Rather routeTo handlers expecting context objects, they would be passed a context resolver, which is nothing more than a function you can call that will return the context object. In the case of a URL-based transition, a context resolver would simply return the result of a route's deserialize. This would solve the problem of not immediately firing deserialize once a URL change occurs, letting the routeTo handlers along the way decide whether that data should be fetched.

But, for a transition to a nested route, like /posts/123/comments/456, which would get converted to a routeTo event with a Post promise and a Comment promise as contexts, what would the context resolver for Comment yield? It would have to yield a promise that only resolved after

  1. The Post promise resolved, and
  2. The Comment promise resolved.

with both of these conditions happening sequentially. So, basically, context resolvers from URL changes would have to yield somewhat complicated promise chains depending on the resolution of parent route promises. So, it gets... complicated.

Possible Solution 2: Lazy Promises

Solution 1 is solid, but it's a little annoying to have two different concepts of "deferred" values: context resolvers, and then promises. Perhaps another way to go would be to implement a "lazy" promise, which doesn't try to resolve itself until someone's actually called .then on it. This would eliminate the need for context resolvers, but would require some special coding to implement the lazy promise class.

Finally: Shall We Resurrect Per Route Loading States?

In router v1, there was a concept of per-route loading states, where if a route's deserialize return a promise (an object with a .then property), the router would go into a loading state for that route, if one was specified, and it would remain there until the promise resolved, and then the transition would continue.

In the present router, there's still a concept of a loading state, but it's not specific to a route, but is little more than a global loading flag that you could, for instance, use to show a loading spinner until the transition has completed. The router doesn't actually perform any sort of teardown or state transition until all the promise objects have been resolved, so any events sent to the router would be handled be the pre-transition state.

So, the major question that remains is:

Do we resurrect per-route loading states? If so, we need to find a way with the new router DSL to signify that a route has a loading state that should be entered when its deserialize returns a promise. Or do we just assume that each state has an implicit loading state? If we don't resurrect per-route loading states, are we happy with a single global loading "flag"? And are we happy with only actually performing the transition (template teardown, controller setup, rendering templates, etc) once the promises have resolved?

Or perhaps we don't make the router transition pause while waiting for these promises to resolve. Maybe we just perform the transitionTo and immediately pass these promises to their routes and let them handle the async-ness. We've already gone through the complicated work of assembling a chain of promises that depends on parent promises resolving, so perhaps this will do the trick?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.