Proposal: Nested Loading Routes

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.
15 Likes

This proposal is awesome.

Perhaps it’s just habit but I pretty much always have promises in ApplicationRoute (usually the current session). Perhaps they should be in initializers but “to make the loading routes work” doesn’t seem like good justification (on its own) for changing this. Perhaps it could be special cased and render a loading route instead of application.hbs when ApplicationRoute returns a slow promise?

@alexspeller I think I agree, it’s just I’m not sure what the convention is going to be. This is one of these areas where things were already broken/unusable enough that we’re fine with a little further corrective breakage, but there’s probably a nicer solution than prohibiting ApplicationRoute from returning promises; I too have written apps that depend on this behavior.

The tricky bit is that, given loading.hbs, it’s unclear whether that refers to the loading route/template that’d get inserted in application route/template’s outlet, or whether its the top-level loading route that only gets used when ApplicationRoute returns a promise.

It seems strange to me that we would come up with a solution only for when ApplicationRoute returns a slow promise and not for when the time between advanceReadiness and deferReadiness is long and noticeable. Would it be so crazy to suggest that all pre-app loading display should just be static dom within the application’s rootElement that gets cleared out once the first route renders? Actually, unfortunately, yes, it would, since by default rootElement is <body> and that very often has lots of other scripty stuff riddled inside of it, yes?

application_loading.hbs vs loading.hbs? Is that stupid?

This could be the same - i.e. if there is something that should be shown when application route returns a slow promise, show the same thing before advanceReadiness is called. I.e., the application is in “initial loading state” up until all readiness stuff is complete and the application routes promises are resolved

Seems like for the sake of backwards compatibility and least surprise, in both the promise-returned-from-application-route and promises-returned-from-children-of-application-route cases, we’ll look up ‘template:loading’. In the former case, we’ll render loading as a top level view (sibling of ApplicationView), and in the latter, it’ll render in the outlet of application template.

Very good proposal. +1

+1 for this amazing proposal!

seems a good compromise to me…

Do you mean having just one loading template and rendering it into the appropriate outlet (whatever that may be)?

Your proposal is good in general, however, I’m not sure it would work well with what I’d like to happen in my case, so I’ll explain my use case below:

I have a number of templates that have a lot of visualisations and lists in them. Some of them appear as dropdowns, etc. Currently, when we need to show a loading template, we render the full template that would be rendered anyway (e.g. ‘dashboard’ in ‘DashboardRoute’) and, for every part of that template that isn’t loaded, we render the loading template instead (i.e. not instead of the dashboard template, we render inside the dashboard). Currently this is done with a component.

Hopefully that makes sense (it should at least make sense to @alexspeller). How would this work with that?

I believe that there is definitely a use-case for separate application-not-yet-loaded loading (think, iOS splash screen) compared to application-has-loaded-something-else-is-slow (think, progressive rendering?) type loading. Not having those as separate things will likely have people turning their application route into just {{outlet}} and then pushing their “real” application down one level in the route hierarchy so that they can manually handle the two different scenarios.

I’d not complain too much about forcing application-not-yet-loaded to be the default browser-rendered child of <body> but that still seems a bit kludgy: “Here is the only place you actually have to write HTML!”

As an aside, in our project we’ve actually pulled a reference to the parentRoute into scope to accomplish a dirty trick anyway so I’m a fan of exposing that as a property on the route. We can assume/hope that if you’re using it that you know what you’re doing.


Interaction: a user clicks to open a full screen task pane (think, iOS mail compose, but with further navigation available). Our goals:

  • Route changes to reflect the task pane.
  • Task pane route has its own main outlet.
  • Existing content remains in the application template outlet.
  • All new navigation within that route now targets the taskpane’s main outlet instead of the application’s main outlet.

Because of this line of code:

if (options.outlet === 'main') { this.lastRenderedTemplate = name; }

We do something like this.

// Parent (original task pane)
renderTemplate: function() {
  this.render(TaskPaneRoute, {
    into: 'application',
    outlet: 'task-pane'
  });
  this.render(currentRouteName, {
    into: 'application'
  });
}

// Child (anything rendered inside a task pane outlet)
renderTemplate: function() {
  this.render(this.routeName, {
    into: this.parentRoute.routeName
  });
}

@nathanhammond Yeah, we’re definitely not gonna do the rootElement way. I’m sitll undecided on how we want to handle the root loading case. We might end up just adding a special-named loading template/route calling RootLoading or GlobalLoading… not certain yet, but I’m shying away from the solution that tries to reuse a thing called LoadingRoute or template-name="loading" both as a global toplevel view as well as the one that renders into application template’s outlet depending on whether ApplicationRoute is returning the slow promise or one of its child routes is.

BTW, everyone, here’s the PR for work done so far; pretty close to being finished. Adding some more JSBin demos shortly. https://github.com/emberjs/ember.js/pull/3568

Lemme know them thoughts.

@nathanhammond there have been multiple issues reported around that line, some of which might be related to what you’re running into, so I wouldn’t be surprised if we fixed that in such a way that wouldn’t necessitate parentRoute.

FWIW I’m probably going to remove the addition of parentRoute in this PR as I thought of a better way to fire loading/error events (and any custom user events) on currently route hierarchies that are currently being transitioned into (but aren’t yet the currently active routes). In short I want to add Transition#trigger which has the same Route#send semantics.

Definitely liking the sound of this discussion: improved loading status would definitely be a great thing.

Also plus one for the idea of a RootLoading or GlobalLoading (or whatever you call it) solution. I have an Ember app at the moment that has to do a load of startup loading at the moment, and the next App that I’m moving onto will need the same. A pre-app loading state would be brilliant from my perspective.

@machty I’d agree that it’d be sensible to differentiate this loading state from the solution for route loading states, as the two are effectively different states: one is the app loading/initialising, the other is loading states within an initialised app.

Great proposal!

App loading and route loading

I agree that global/initial loading of the app could be kept separate, preferably out of ember all together, i.e. with plain html/css that’s hidden when Ember is ready.

  • For desktop apps it makes sense since you don’t want to wait for Ember to load before you starting showing ‘ember global loading’, and
  • for mobile apps you may opt to keep the splash screen until ember has loaded instead. In both cases it’s useful that global loading is kept outside ember.

Inline loading

A possibly related issue is that sometimes you don’t want to keep a separate loading screen, but rather handle it inline in the template, e.g. {{#if model.isLoading}} I'm loading... {{else}} show model {{/if}}.

  • One case is if the modal is a minor part of the view, where it makes sense to show the view and some progress indicator for the model.
  • Another case could be if one uses transitions (e.g. slide transition on mobile), which partly hides the loading, because it typically finishes before the transition has completed. In this case you need to go inline so that the template is in place, so that there is something to transition to.

Right now I think the route is never entered until the promise returned from model has returned. What are your thoughts on this?

Would it be possible to allow progression even if model promises hadn’t returned by some configuration, probably on the route e.g. immediateModel: true;, modelDefer: false or something similar (came up with these just now, there is probably a better name for the setting).

@machty I love this proposal! I just setup a loading route in a client’s app a couple weeks ago and was pretty disappointed by the lack of control. I have a couple thoughts on what you’ve mentioned so far:

I can definitely see cases where it would make sense to render both Foo & Baz LoadingRoutes. However, I’d think a major use case for loading routes is to show a spinner. So if both FooLoadingRoute and BazLoadingRoute get entered, does that mean I see 2 spinners? In that case, it’d make sense to only enter one of the loading routes. Which one? My gut says the lowest level, but I’m not sure.

Along those lines, one thing I’ve wanted to do is delay the LoadingRoute until I’m sure we actually have a slow request going on. Currently the loading route shows every single time a promise doesn’t resolve immediately, and in many cases it results in a flash of unwanted content as the promise resolves within milliseconds (or very few seconds) of the loading template rendering.

Obviously this is a hard, non-deterministic problem to solve, but it would be nice if we could define what “slow” means. Maybe we could set Ember.LOADING_ROUTE_DELAY = 3000 to make sure it doesn’t enter any loading routes until the promise has taken at least 3 seconds to resolve. Better yet, perhaps we could check the model for a LOADING_ROUTE_DELAY. That would allow for longer requests to show a loading route right away.

  • Joe

I’m writing up a guide right now that’ll hopefully answer most of these questions and the ones other people have had, but real quick:

  • You can’t enter two states/routes at the same time; you won’t ever see two spinners. You might have a situation, depending on how your app’s architected, where you’ll see spinner 1, data loads, see spinner 2, data loads, transition finishes.
  • I’m gonna try to come up with some advanced examples of handling the timing of loading route entry. Highly doubt we’d need to resort to anything like LOADING_ROUTE_DELAY
  • fwiw a promise “immediately resolving” means it resolves on the same run loop, e.g., if it doesn’t actually need to do anything async, it can immediately resolve, which causes it to resolve within the same run loop in which it was created. This can still lead to the flashing behavior you’re talking about, if, say, the promise resolves in 200ms and you’d only like to displaying loading UI after 300ms. I’m gonna try to have an example for this

Here’s a draft of the guide, lemme know how it reads:

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

1 Like

Hey yall, just merged this in so that it’ll be there for 1.2 beta. Probably will tweak a few things per the feedback in this thread, but doesn’t seem like any tweaks will be all that fundamental. Thanks for the feedback thus far and please continue to provide it.

3 Likes

:thumbsup: thanks @machty! WIll try this asap

I have a simple application, which I used to test how routing works in Ember.js and I tried it with newest changes. The app displays a list of albums and when you click on an album it displays details. I use jquery-mockjax to mock responses along with response times.

The thing that struck me after update is that when you return a promise from a model hook, the entire router is stopped, ie. no requests from child routes will be done unless parent’s promise is resolved.

Check the following example: https://dl.dropboxusercontent.com/u/3093257/albums/index.html#/albums/1

Both requests are set to return within 5 seconds. When you enter the /albums/1 route, a loading template will be displayed for 5 seconds and then albums/loading template will be displayed for another 5 seconds. Is this correct? I love the new functionality, but for apps with nested routes, where list can take more time to load than a detailed view, it will mean no concurrency for loading list and detail view (and yeah, I know that ideally you can just get an object from the list when it’s loaded, but it’s sometimes the case that the object is not on the list so you need to fire a separate request).

Am I missing something?