Skip to content

Instantly share code, notes, and snippets.

@machty
Created October 8, 2013 05:23
Show Gist options
  • Save machty/6879857 to your computer and use it in GitHub Desktop.
Save machty/6879857 to your computer and use it in GitHub Desktop.
Proposal for nested loading routes in Ember.js

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 bypass LoadingRoute and go from LoadingController 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

  1. Intuitive convention for specifying loading route substates.
  2. 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.
  3. 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

  1. Add a new loading event/action that fires on destination routes when, during a transition, a newly-entered destination route model/beforeModel/afterModel hook returns a promise that doesn't resolve on the same run loop.
  2. 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 and BazRoute return slow promises, and FooLoadingRoute and BarLoadingRoute exist, do we enter both of those loading routes as bar and baz's promises sequentially resolve? Or should we just enter FooLoadingRoute 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 because LoadingRoute is expected to render into application template's {{outlet}}. For when slow promises occur within routes that are children of ApplicationRoute. One idea discussed among the Core Team was to prevent apps from returning promises from ApplicationRoute and have them move that kind of logic to initializers if possible, or use deferReadiness/advancedReadiness.
@lalitindoria
Copy link

Did this proposal see light of the day?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment