View's API for handling async loading better


#1

I was thinking about latest @machty’s router changes in regard to loading states. This is a great step forward, but one thing bothers me a bit. We’re giving a lot of love to the scenario of loading routes with promises, but not a lot of love to a situation when for some reason I want to avoid using a promise.

The best example (which I also talk a lot about on most router related discussions, as you may have noticed) is a list of items and a detailed view. After thinking about approaching such a scenario, I would probably use a promise only for a detailed view, as this is the main thing to see, a list is just an addition, if it’s not loaded you don’t loose too much. That way, we can quickly render a list and move to fetching an individual view. A problem is, it usually involves a lot of boilerplate in templates, for example:


{{#if loading}}
  List is loading
{{else}}
  display list
{{/if}}

Additionally, if I would like to handle error properly, I would need to add extra logic for that.

In general there are 2 cases for rendering things, which are not a “main” view of the page (I’m being a bit simplistic here, but please bare with me):

  1. Without a nested UI, you may have a list, which will be rendered in the sidebar, but completely separately from the view, for example with {{render "posts"}}
  2. With a nested UI, you may have a list, which is rendered in the sidebar, and the outlet for the main view. In such situation a list will be rendered alongside with a child template

Let’s start with a first case. When you render such a resource, you will most probably fetch actual records in the controller, maybe in a content computed property. If you want to handle errors, you will have to wire your own code, which will render an error template or a loading template while resource is loading. These steps may be easily abstracted and the templates can be rendered automatically if present.

In the second case, things are a bit more complicated, let’s take such a template as an example:


<ul class="posts">
  {{#each post in controller}}
    <li>{{post.title}}</li>
  {{/each}}
</ul>

{{outlet}}

We can’t just swap the entire template for the time the list is loading, because it would also remove the outlet.

I think that the answer to both cases could be something like a {{resource}} helper. In the first case, we could use it instead of render, like {{resource "posts"}}, which would take care of creating a certain type of view and controller, which is able to deal with loading and error states. In the second case, we could use the same helper, instead of putting the posts list directly, but passing a block to it:


{{#resource "posts"}}
  <ul class="posts">
    {{#each post in controller}}
      <li>{{post.title}}</li>
    {{/each}}
  </ul>
{{/resource}}

{{outlet}}

It would preserve the context of a parent controller, but just as with the first example, it would deal with loading and error states.

What do you think about that? I’m happy to implement this feature. I guess I will still implement it as an extension, but I would love to get it into core, because I feel that we lack a good abstractions in that regard.


#2

I would like to give it a shot tomorrow, so I was going through these 2 cases again and here is a bit more detailed behaviour:

  1. {{resource "posts"}}:
  • it works in a similar way to {{render "posts"}}, ie. it will render a posts template, uses App.PostsController, etc.
  • if a content property on a controller is not a promise, we behave exactly as with regular render
  • if a content property of a controller is a promise
    • loading action on a controller will be called, by default it will render posts/loading template
    • when the promise is resolved, a target template (in our case posts) will be rendered
    • when the promise is rejected, an error action will be called, by default it will render posts/loading template
  1. {{#resource pathToPromise}}...{{/resource}} case, I’m assuming that it’s used in context of App.PostsController in a posts template
  • we use an inline template here and it will be a “target” template
  • instead of passing the name of the template, we pass a path to a promise - it will be used to handle reject/resolve cases
  • if for some reason a passed object is not a promise, a template passed to a helper is rendered
  • if a passed object is a promise, a loading action will be called. It will render posts/loading template by default (default name will be generated based on a parent)
  • when the promise is resolved, a template passed to a helper will be rendered
  • when the promise is rejected, an error action will be called. By default it will render posts/error template

Such a behaviour will be really close to a current loading/error behaviour of the Router. One thing that I’m not sure about is a view used in the second case. Let’s take routes defined as:

Router.map(function() {
  this.resource('posts', function() {
    this.resource('post', { path: ':post_id' });
  });
});

In this situation we would use {{#resource promise}} inside posts template, which would use App.PostsView. Ideally, there should not be a reason to create another view redner an inline template in context of PostsView and PostsController. However I’m not sure how would implementation look like, so if it’s not possible I will have to do something like PostsContentView.