How to Load Multiple Models in a Single Route

Most Ember examples show how to load one model in a route.

Sometimes, though, you need to load several types of objects in a given route.

Is it possible to load them together in the same request? How do we get multiple data objects before the template is rendered?

We have options! Let’s explore best practices for each possible scenario.

Your models are related if they are associated by relationships (such as hasMany, belongsTo) in Ember Data. The classical example is a post with a collection of comments:

// app/models/post.js

export default DS.Model.extend({
  title: DS.attr('string'),
  body: DS.attr('string'),
  comments: DS.hasMany('comment', { async: true })
});

This route will load a certain Post model and Ember Data will take care of asynchronously loading the comments.

In this case, we can refer to model.comments directly from our template. Easy!

Models can also be related implicitly. What if we have a files collection which (for whatever reason) hasn’t been declared as a relationship in the Post class?

Nested routes

Implicit or explicit, we can use nested routing to load multiple models at once.

In our example, it would look like:

// app/router.js

Router.map(function() {
  this.route('post', { path: '/posts/:post_id' }, function() {
    this.route('files', function() {
      this.route('other');
    });
  });
});

So if we visit /posts/1/files we could leverage the model() hook of the PostFilesRoute to retrieve the files related to this model:

// app/routes/post/files.js

export default Ember.Route.extend({

  model() {
    const post = this.modelFor('post');
    return this.store.query('file', { param: post });
  }

});

Not sure how this works? Quickly try it out!

Build a (super simple) Ember CLI app based on the router.js above. Generate the routes:

$ ember generate route post
$ ember g route post/files
$ ember g route post/files/other

Override each route’s model() function to return some string. Then update their corresponding templates like so:

{{!-- app/templates/post.hbs --}}
<h1>post.hbs</h1>
model: {{model}}
{{outlet}}

{{!-- app/templates/post/files.hbs --}}
<h2>post/files.hbs</h2>
model: {{model}}
{{outlet}}

{{!-- app/templates/post/files/other.hbs --}}
<h3>post/files/other.hbs</h3>
model: {{model}}
{{outlet}}

To get something like this:

Tip: As seen above, you can refer to outer routes’ models via this.modelFor.

Nested routes can address a variety of multiple model loading cases. Depending on our URL structure, though, creating one route for each additional model could be overkill.

Ember best practices delivered straight to your inbox? Tell me where:

(One e-mail every month. No BS. Unsubscribe anytime!)

(Or, more accurately, not-necessarily-related models.)

Now let’s imagine that the /posts URL has to display a list of all posts… as well as all tags and categories available site-wide and, why not, weather information.

The latter are not necessarily related to this route’s model (a collection of posts). So how do we approach this?

Easy, we include them in the URL: http://localhost:4200/posts/tags/categories/weather… one route each and voilà!

Just kidding. That makes no sense.

Additional hooks

Besides model() we know that we have other hooks available: beforeModel(), afterModel() and setupController().

Could we make use of them?

export default Ember.Route.extend({

  model() {
    return this.store.findAll('post');
  },

  afterModel(model) {
    // like model(), if a promise is returned here
    // Ember will wait until it resolves before proceeding
  },

  setupController(controller, model) {
    controller.set('posts', model);
    this.store.findAll('tag').then(function(tags) {
      controller.set('tags', tags);
    });
    this.store.findAll('category').then(function(categories) {
      controller.set('categories', categories);
    });
  }

});

Okay. Not beautiful, but it works. The template will render once posts are loaded. Eventually, when their promises have resolved, tags and categories will show up in the template.

But… isn’t there a way to call everything in a more orderly fashion?

RSVP.hash to the rescue

Ember’s promise library, RSVP, has a handy function that allows us to load multiple promises in parallel:

// app/routes/posts.js

export default Ember.Route.extend({

  model() {
    return Ember.RSVP.hash({
      posts: this.store.findAll('post'),
      tags: this.store.findAll('tag'),
      category: this.store.findAll('category'),
      weather: this.get('weather').current()
    });
  },

  setupController(controller, models) {
    controller.set('posts', models.posts);
    controller.set('tags', models.tags);
    controller.set('categories', models.categories);
    controller.set('weather', models.weather);
    // or, more concisely:
    // controller.setProperties(models);
  }

});

In this case, setupController() will not be called until all four promises were fulfilled or rejected. Unlike our previous attempt, all of our data is already available by the time the template is rendered.

Tip: RSVP.hash lets us combine promises with non-promise objects.

This approach works well but comes with a few drawbacks:

This solution is practical. However, how about loading data without having to depend on the route mechanism?

Data-loading components

Don’t get too hooked on the hooks! These methods are tightly bound to the route (and therefore the URL).

Sometimes there is not even a single main model to use in a route. The URL has nothing to do with them.

Well, actually, you can load data anywhere you want! And in situations like this, data-loading components are a very good proposition.

How would we craft one of these? Let’s try:

// app/components/weather-list.js

export default Ember.Component.extend({

  weather: Ember.inject.service(),

  showWeather: Ember.on('didInsertElement', function() {
     this.get('weather').current().then((weather) => {
        this.set('weather', weather);
     });
  })

});

See, we have just injected a custom service in our component! It’s beautifully simple.

Let’s not forget its template companion:

{{!-- app/templates/components/weather-list.hbs  --}}
{{#if weather}}
  <h3>Weather information</h3>
  <div>{{ weather }}</div>
{{else}}
  <span>Loading...</span>
{{/if}}

The takeaway here is that you can also retrieve data from your components if that makes architectural sense in your application.

The service’s weather function returns a promise.

Below is a sample implementation of such service (yours will likely be more sophisticated – and useful):

// app/services/weather.js

export default Ember.Service.extend({

  current() {
    return new Ember.RSVP.Promise(function(resolve) {
      Ember.run.later(function() {
        resolve("weather data, 30ºC");
      }, 4000);
    });
  }

});

Note that Ember Data store is a plain Ember service and, as such, can also be injected with Ember.inject.service().

When to choose which?

Our holy guide in the Ember universe is the URL. Always have it present in your architectural decisions.

In the same spirit of the quote concluding the Should Components Load Data? article:

If the model (or models) are connected to the URL, use their respective model() hooks.

Otherwise, and if no links to that route are present, RSVP.hash can be a good tradeoff between power and simplicity.

Alternatively, use data-loading components.

Enjoyed this article? Don't miss my next one!

Leave me your e-mail for content that will help you master Ember:

Do you want to master Ember fast?

Leave me your e-mail for helpful updates delivered straight to your inbox.

(A few e-mails per month. No BS. Unsubscribe anytime!)