Loading Route Facelift
LoadingRoute
has existed for a while, but it’s barely useful
and largely broken. It’s never gotten much love due to it largely being
a hangover of the router.js microlib, and we can make it better. The problems with
LoadingRoute
are intertwined with issues with facelift router’s
handling of async transitions in some cases, but of which need to be
addressed, but here’s a list of problems related to both:
- There’s only one global
LoadingRoute
that gets activated when a transition promise doesn’t immediately resolve. No support for nested loading states. - No customizability; if it’s a loading route, it’s top level, and it’s the same loading route you see for any transitions involving promises.
- There are no active handlers (in router.js terms) if any of the to-be-entered routes return promises on full page refresh / app load.
- Actions fired from
loading
template bypassLoadingRoute
and go fromLoadingController
to the Router, which means the action fires on currently active routes (which will be exited once the transition completes). Worse still, if this is a full page refresh, there are no active handlers to fire on, so, error. - The loading template can only be rendered as a top level view, as a sibling to ApplicationView. Maybe this is circumventable with hacks, but any good-seeming solutions will be stifled by no the “no active handlers” crap mentioned above.
- There are valid use cases that are at odds with the present day non-eager behavior of transitions, whereby the teardown of source routes and entry of newly entered routes doesn’t occur until all destination promises resolve. While this is a nice, safe, often fool-proof default, there should be a way for destination routes to govern loading behavior and to make the transition behavior more “eager”.
Goals
- Intuitive convention for specifying loading route substates.
- This default convention should be overridable; ideally nothing more than an event that is fired, with a wise default implementation of a handler provided to achieve goal 1.
- Zero to minimal backwards incompatability (but within reason, seems
fine that a few things break given how broken the current
implementation of
LoadingRoute
is).
Solution
- Add a new
loading
event/action that fires on destination routes when, during a transition, a newly-entered destination routemodel/beforeModel/afterModel
hook returns a promise that doesn’t resolve on the same run loop. - Define a default
loading
handler for routes that will achieve the overridable behavior described in the “Nested Loading Route Resolution” section below.
Nested Loading Route Resolution
The desired behavior is best described by example, but in short, when a
loading event is fired, Ember will try and find the closest nested
loading state to where the loading
event fired from. If it finds one,
consider the transition eager, tear down the source route templates, and
enter the nested loading state. If no loading states are found, the
transition is “lazy” as it is today, in that no source route teardown
will occur until all destination route promises have resolved.
Examples
Full page reload with url ‘/foo/bar/baz’
If the slow promises occurs on BazRoute
’s model
hook, then the
following loading states will be entered, if present, in the following
priority:
-
BarLoadingRoute
; will render ‘bar/loading’ in ‘bar’ template’s default{{outlet}}
-
FooLoadingRoute
; will render ‘foo/loading’ in ‘foo’ template’s default{{outlet}}
-
LoadingRoute
; will render ‘loading’ in ‘application’ template’s default{{outlet}}
If the slow promise occurs on BarRoute
, then:
-
FooLoadingRoute
; will render ‘foo/loading’ in ‘foo’ template’s default{{outlet}}
-
LoadingRoute
; will render ‘loading’ in ‘application’ template’s default{{outlet}}
Transition from ‘/foo/yeah/woot’ to ‘/foo/bar/baz’
The loading state resolution that occurs when transitioning between two
routes is not fundamentally different from the full page reload example;
there’s only one extra constraint, which is that by default, Ember will
stop looking for nested loading routes about the shared parent route
(aka the “pivot” route). In this case, FooRoute
is the pivot route.
If the slow promise occurs on BazRoute
:
-
BarLoadingRoute
; will render ‘bar/loading’ in ‘bar’ template’s default{{outlet}}
-
FooLoadingRoute
; will render ‘foo/loading’ in ‘foo’ template’s default{{outlet}}
- (unlike full page reload example, algorithm stops here, because
both source and destination routes of the transition are children
of
FooRoute
)
If the slow promise occurs on BarRoute
:
-
FooLoadingRoute
; will render ‘foo/loading’ in ‘foo’ template’s default{{outlet}}
The loading
handler
The above default resolution logic will be implemented via an
overridable default loading
handler. Here are some things you should
be able to do by overriding the loading
handler:
Override the stop-at-pivot default
App.FooRoute = Ember.Route.extend({
actions: {
loading: function(transition) {
// Even when `foo` is the pivot route,
// we want the `loading` route that gets rendered
// into `application` template's `{{outlet}}` to
// be entered, so we override the default behavior
// by causing the `loading` event to bubble (by
// returning true)
return true;
}
}
});
Prevent default loading resolution for specific routes
App.BazRoute = Ember.Route.extend({
actions: {
loading: function(transition) {
// Do nothing in the handler and don't bubble the
// event. This stops the loading state resolution in
// its tracks and forces any transitions into this
// route to be "lazy", even if parent nested loading
// states have been defined.
// Could also just set `loading: Ember.K`.
}
}
});
Remaining Questions
- Given a transition from ‘/foo/yeah/woot’ to ‘/foo/bar/baz’,
if both
BarRoute
andBazRoute
return slow promises, andFooLoadingRoute
andBarLoadingRoute
exist, do we enter both of those loading routes asbar
andbaz
’s promises sequentially resolve? Or should we just enterFooLoadingRoute
and stay there until all promises resolve? I’m leaning toward the former, as it gives the developer more control. - What about promises returned from
ApplicationRoute
? This is weird becauseLoadingRoute
is expected to render intoapplication
template’s{{outlet}}
. For when slow promises occur within routes that are children ofApplicationRoute
. One idea discussed among the Core Team was to prevent apps from returning promises fromApplicationRoute
and have them move that kind of logic to initializers if possible, or usedeferReadiness
/advancedReadiness
.