How to Load Multiple Models in a Single Route

Last reviewed in September 2015 with Ember 2.0 Update to Ember Octane in progress!

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.

diagram

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:

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

{{!– 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.

Octane news & best practices, straight to your inbox?

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)

Unrelated models

(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?

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:

  • Rendering will not start until all promises resolve (can be a pro or con depending on your case)
  • When passing models to link-to (as in {{link-to 'post' post}}) you will have to pass the exact same structure of the hash, as model() will not be called

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

Octane news & best practices, straight to your inbox?

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)

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);
     });
  });

  // TODO this should be an Ember Concurrency task; .on('didInsertElement')

});

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):

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? Join Snacks!

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)