Loading animations with asynchronous models in Ember.js

People don’t like waiting. Unfortunately, latency is an inherent part of our world so waiting is something that you have to consider when you’re designing an interface for a web application. However, people tend to be more tolerant of waiting if they know that something is going on. Therefore, it is very important to show loading animations to indicate that the application is still responsive and is doing some computations or communication behind the curtains.

In Ember.js, there’s a solution to do this that works for models that are loaded in the model hook of a Route. However, a different solution is required if your application contains asynchronously loaded models. I presented this at December’s Brussels Ember.js meetup.

Hooks in Ember.js Routes

In Ember.js, models are loaded in the model hook of a Route. First, before this function is executed, the beforeModel is called. The model function itself returns a Promise and when it resolves, the afterModel function is called.

Assume we have a model Post and a route PostRoute that shows a single Post. We can implement a waiting animation as follows. We assume here that adding a loading class to the DOM body displays a loading animation, such as a spinner.

App.PostRoute = Ember.Route.extend({
  beforeModel: function() {
    // Assume the 'loading' class displays an overlay with a loading animation
    Ember.$('body').addClass('loading');
  },
  model: function(params) {
    return this.store.find('post', params.post_id);
  },
  afterModel: function() {
    Ember.$('body').removeClass('loading');
  }
});

This works fine. What if each Post now has a list of comments. The comments are loaded asynchronously because there can be many and we only want to show them when we look at a single Post. The model is defined as follows.

App.Post = DS.Model.extend({
  title: DS.attr('string'),
  comments: DS.hasMany('comment', { async: true })
});

An example JSON response for a Post might then be as follows.

{
  post: {
    id: 1,
    title: "This is a post",
    links: {
      comments: "/post/1/comments"
    }
  }
}

In the template, we show the comments for each individual Post.

<article class="post">
  <h2>{{title}}</h2>
  <p>{{content}}</p>
  <aside>{{render "comments" comments}}</aside>
</article>

Because they are defined as loaded asynchronously, the comments won’t be loaded in the model hook of the Route. The comments will only be loaded after the template has started rendering. This is after the afterModel hook. Therefore, our current solution for displaying the loading animation won’t work. After the post has loaded, the loading animation disappears because the afterModel will be called. However, the comments will not have been loaded yet so they will appear as “blank” on the page. As soon as they are loaded, a “blink” will appear and the comments will suddenly become visible. Clearly, this is not UX-friendly.

Dealing with asynchronous data with Promises

The key is to load all the necessary asynchronous models when we’re setting up the controller in the Route. When we call get('comments') on a Post model, it either returns an array of type PromiseArray if the comments have not been loaded yet, or a ManyArray in case the comments are already available on the client. To make it easy, we’ll work with Promises everywhere so we will define a function makePromise which will wrap an arbitrary object in a Promise object.

Ember.RSVP.makePromise = function(maybePromise) {
  // Test if it's a promise
  if (maybePromise.then) {
    // Then return it
    return maybePromise;
  } else {
    // Wrap it in a Promise that resolves directly
    return Ember.RSVP.resolve(maybePromise);
  }
};

If maybePromise is a Promise already, it simply returns it. Otherwise, it wraps it in a Promise object that resolves directly.

We then load the comments when we’re setting up PostController in the setupController hook.

App.PostRoute = Ember.Route.extend({
  beforeModel: function() {
    // Assume the 'loading' class displays an overlay with a loading animation
    Ember.$('body').addClass('loading');
  },
  model: function(params) {
    return this.store.find('post', params.post_id);
  },
  setupController: function(post, controller) {
    // Pre-load the comments
    // The 'get' call will result in an AJAX call to get
    // the comments and returns a promise
    var comments = Ember.RSVP.makePromise(post.get('comments'));

    // Wait until the promise has been resolved
    comments.then(function() {
      // Wait until all templates have finished rendering
      Ember.run.scheduleOnce('afterRender', this, function() {
        // Remove the loading animation
        Ember.$('body').removeClass('loading');
      });
    });
  }
});

The code works as follows. We know that post.get('comments') returns either a PromiseArray (acts like a Promise) or a ManyArray (does not act like a Promise), so we wrap it in a Promise. We wait until it resolves. Then we schedule a function in the afterRender queue. This is a queue that is processed after the render queue, i.e. after the templates in the route have been rendered (see this post about lifecycle hooks in Ember.js).

Now, the loading animation stays until:

  • our “synchronous” model (the Post) is loaded
  • our “asynchronous” models (the Comments) are loaded
  • all templates in the route are rendered
Write us your thoughts about this post. Be kind & Play nice.
  1. I have to say that I’m pretty new to Ember, especially when it comes to Ember Data. I really like your solution how to greatly improve the UX of the application. However, as an technical guy, I would also try to improve the solution 😉 The two things that come to my mind are:

    – reduce the number of HTTP requests to the server when we are already eager loading the relation and declare it as embedded, so we get the parent and its child records within one reponse (that should be fairly simple to do)
    – find a hook where we can add that logic, similiar to the first example. Unfortunately I couldn’t find anything like that in the docs. Do you know if something like that exists?

    Looking forward to more such posts!

    Reply
    • Yoran Brondsema says:

      Hi Christoph,

      Thanks your comment! I agree that you can reduce the number of HTTP requests by combining of the parent and its association (in this case a post and his comments) in a response. There are situations where you don’t want to do that though. Imagine you have a single-page app where you first show an overview of all the posts to the user (a blog for instance). A user has to click on a single post in order to the post’s comments. When that happens, you only want to load the post’s comments because you already got all the data for the post when you showed the overview of all posts. If you would have eager-loaded all the comments for each post, you would have loaded them when showing the overview page. If you have 100 posts with each 100 comments, that means the response for your request will contain 10,000 records, and that for the comments alone. By using asynchronous associations, you would load only the posts when showing the overview (100 records) and 100 records each time a user visits a post (for the comments). If it turns out that the user visits most of the individual posts, your solution is definitely faster because you’d have only one HTTP request. If not, then you save time by not loading unnecessary data!

      Long story short, in the end it all depends on the nature of your application and the expect behaviour of the user in order to determine whether eager-loading or asynchronous loading is faster!

      For your answer to your second question, if you don’t specify an association explicitly as “async” in your Ember-Data Model, it assumes that it’s going be present in the same request as the parent’s (so kind of eager-load by default).

      Reply
  2. Uladzimir says:

    Hi, Yoran!

    Thanks for the article. It was quite informative. Especially on synchronous and asynchronous stuff and how models are loaded.

    However, I have a simpler solution that should word for both async and sync requests (this is not my solution – I got it somewhere but could not find a link to the original post).

    Every time jQuery ajax requist is started/stopped ‘ajaxStart’ and ‘ajaxStop’ hooks are triggered. So we can do something like this:

    $(document).ajaxStart(function(){ $(‘.ajax-spinner’).show() });
    $(document).ajaxStop(function(){ $(‘.ajax-spinner’).hide() });

    And it works perfectly for me (in fact this code is now in production)

    Thanks again!

    Reply
    • Yoran Brondsema says:

      Hi Uladzimir, thanks for you interesting comment. That is actually a pretty elegant solution, I wasn’t aware of these events in jQuery! The problem I see however is that you have to make a distinction between the requests that occur while loading a route (where you want to show the Ajax spinner) and the other requests that may occur: for instance when submitting a comment or when refreshing data on a page (see my post here http://yoranbrondsema.com/live-polling-system-ember-js/). For those requests, you may want to show a different spinner or no spinner at all. How did you solve that?

      Reply
      • Hi Yoran, i think that your approach has the same problem, because on afterModel, beforeModel and setupController hooks, the template hasn´t been loaded and therefore any selection over DOM on this template didn´t work. At least this is the problem that i found in my case. Did you know how to solve this?

        Thanks!

        Reply
        • Yoran Brondsema says:

          Hi Jonathan, thanks for your comment! You’re right that in these hooks of the Route, the DOM has not been rendered yet. However, if you look in my solution, I add a class to the element of the DOM, of which we’re sure that it’s always there.

          Having said that, H1D’s solution of using Loading subroutes (see http://guides.emberjs.com/v1.12.0/routing/loading-and-error-substates/#toc_code-loading-code-substates) is I think the right way to go to solve this. This blog post was written when Ember.js didn’t have this functionality yet.

          You may find it interesting though that in the Hstry application, we are trying to move away completely from loading models in the ‘model’ hook. The reason is that we already want to show parts of the page that don’t need the data immediately on opening the page, as it gives a better UX.

          Reply
          • Hi, thanks for your answer. Maybe loading subroutes are the solutions for many cases, but in my case i have a model, which async relation is not loaded yet, when the subroute loads, in which case lead to a short-period of time in which one section of my template remains blank, or worst it says “No information found”. This is because i have something like the following:

            {{#each event in model.events}}
            // show data
            {{else}}

            Sorry, but no events found on this account.

            {{/each}}

            When route loads, the async relation hasn´t been loaded, and because of this it shows “Sorry, but no events found on this account.”, until it loads the model and the events are displayed on my template.

            I found a little weird solution to overcome this, which is simple add a condition on else section of each block:

            {{#each event in model.events}}
            // show data
            {{else}}

            {{#if (compare model.events.length 0) }}
            Loading data, please wait.
            {{else}}
            Sorry, but no events found on this account.
            {{/if}}

            {{/each}}

            The solution described above really works, and that is because the async behavior of this model, when route load, no data is show, and because the “events” is an hasMany relation it behaves like an array and has a .length property. But when model loads, this property changes to reflect the real number of elements in the model, which causes the loading alert hide and shows the content if was found or the “No elements” message when the relation is resolved without elements found.

            Maybe i´m doing this wrong, and i have to rewrite this to work with loading subroutes, but anyway it works for me. Thanks for your help.

          • Yoran Brondsema says:

            Hi Jonathan, I think your solution is better than blocking in the `model` hook! Because from a UX point of view it’s better to show as much of the template as you can. This reduces the perceived loading time of the page.

            Now we have done a similar thing but with a slightly different method. We created a component {{promise-block}} which has the following template:

            {{#if promise.isPending}}
            {{partial 'helpers/loader'}}
            {{/if}}

            {{#if promise.isFulfilled}}
            {{yield}}
            {{/if}}

            It takes a Promise object as an argument and a block, which would be shown when the promise resolves. So you would use it something like this: (this code has not been tested)

            {{#promise-block promise=model.events}}
            // show data
            {{/promise-block}}

          • Wow, i didn´t know that in the view, the model is still a promise. I`ll take a try later. Thanks for your help.

  3. H1D says:

    Why not to use loading sub-route (http://emberjs.com/guides/routing/loading-and-error-substates/) much cleaner solution

    Reply