Loading multiple models in a single route

I have a fairly simply app I’m working on. The ApplicationRoute loads a list of categories and displays them:

  • Videos
  • Articles

Clicking on each of these loads the CategoryRoute:

/videos/
/articles/

Within the CategoryRoute I’d like to load a list of Items to be displayed within that route:

/videos/video-one
/articles/article-one
/articles/article-two

This seems like it should be really simple, but I can’t figure it out. Here’s the code I’ve got so far, could I please get some input?

The index page loads just fine, I can click into a category and that loads fine as well. I’m just having trouble trying to load the list of items relevant to a specific category.

Note that I haven’t begun implementing a “details” view for each individual item. That’ll come later.

var App = Ember.Application.create();

App.Router.map(function() {
    this.resource('index', { path: '/'});
    this.resource('category', { path: '/:category_id'});
});

App.ApplicationRoute = Ember.Route.extend({
    model: function(params) {
        return this.store.find('category');
    }
});

App.CategoryRoute = Ember.Route.extend({
    model: function(params) {
        return this.store.find('category', params.category_id);
    }
});

App.Category = DS.Model.extend({
    name: DS.attr('string'),
    description: DS.attr('string'),
    items: DS.hasMany('item'),
});

App.Item = DS.Model.extend({
    category: DS.belongsTo('category'),
    name: DS.attr('string'),
    description: DS.attr('string'),
    url: DS.attr('string'),
    repository: DS.attr('string')
});
1 Like

I think I was just figuring out a similar problem: Does this help? ember.js - Ember js: Render Nested Navigation Sidebar together with a main view - Stack Overflow

Okay. Progress, but I’m getting weird behavior now. Here’s the updated CategoryRoute:

App.CategoryRoute = Ember.Route.extend({
    model: function (params) {
        return Ember.RSVP.hash({
            category: this.store.find('category', params.category_id),
            items: this.store.find('item', params.category_id)
        });
    }
});

When I click from / to /components I see no API call, but I do get the following error:

Uncaught Error: Assertion Failed: You looked up the 'items' relationship on '<App.Category:ember308:components>' but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record

If I simply reload the page I see an API call to /api/items/components which returns 2 hard-coded items, but only displays the first. That seems consistent with the find method which Returns the first item in the array for which the callback returns true. So I changed:

// this line
items: this.store.find('item', params.category_id)
// to this:
items: this.store.find('item', {category: params.category_id})

Then I get the same error as previous when clicking into the route, but when I reload I get an API call to /api/items?category=components instead of /api/items/components. This method does however work, and I get the correct data on the page.

So I have two questions. 1) Why isn’t the item data loading when I click into the route? 2) I’d prefer to call an API endpoint directly rather than passing a query string. How can I accomplish this?

Anyone have ideas on the last issue I’m having?

These are both Ember Data issues. I’ve only been toying with Fixtures so far so maybe someone else can clarify if incorrect;

  1. I believe if you pass in the model with a {{link-to}} helper, the Router model: hook will not fire. But if you refresh, then the router fires the model find() for you.
  2. That is a drawback to the current Ember Data adapter, there has been discussion about it. If you haven’t yet, I’d recommend reading the latest Ember Data article from last week.

That makes total sense. Thanks for your input.

Glad to help, you’ve helped me with ColdFusion and jQM things as well.

I forgot to mention, for #1, did you define the relation as {async:true} on your Model? That was my point for linking the Ember Data article. Not sure how I mixed that up…

Well then, that’s what paying it forward is I guess! I’ll take a look at making the relationship async and see if that helps.

FYI, adding async: true stopped the errors. Ember still doesn’t go off and retrieve the items on click like it does when the page is reloaded. I’ll open a new question about that though. Thanks!

I wrote a blog post recently covering this exact topic that might be of help to others that come across this post, Transform Your Customer Experience | Rightpoint

3 Likes

From the tutorial linked:

I don’t see the point of RSVP hashing in model hook. Wouldn’t have the same result set the variables directly in the controller hook instead of finding and hashing them in model only for de-hashing in the controller hook? Something like:

setupController: function(controller, model) {
  var users =  this.store.findAll('user');
  var tweets =  this.store.findAll('tweet');

  controller.set('users', users);
  controller.set('tweets', tweet);
}

According to Ember docs, router should wait for promise resolution even if they are created in the controller hook…

@nandosan, your suggestion should actually work, but you will be going against convention, and there are some downsides.

The Ember docs do a better job of explaining one of those downsides for handing errors,

This hook follows the asynchronous/promise semantics described in the documentation for beforeModel. In particular, if a promise returned from model fails, the error will be handled by the error hook on Ember.Route.

from http://emberjs.com/api/classes/Ember.Route.html#method_model

Some other downsides are that I’m fairly certain that this would prohibit the use of beforeModel and afterModel, as well, if needed.

1 Like

@mikepmunroe Why use an ArrayController? I believe you can still iterate over the records in the {{#each}} without it. A simpler solution would be to use the ObjectController instead, as that will pass the users and tweets request directly to the model hash/object, eliminating the need for setupController().

app/routes/foo.js

App.FooRoute = Ember.Route.extend({
    model: function() {
        return Ember.RSVP.hash({
            users: this.store.findAll('user'),
            tweets: this.store.findAll('tweet')
        });
    }
});

app/controllers/foo.js

App.FooController = Ember.ObjectController.extend({});

app/templates/foo.hbs

<ul>
    {{#each users}}
        <li>{{name}}</li>
    {{/each}}
</ul>

<ul>
    {{#each tweets}}
        <li>{{content}}</li>
    {{/each}}
</ul>
7 Likes

@Panman8201, great suggestion. The first time I used this pattern I used an ArrayController, but using an ObjectController does make the code simpler and cleaner. I updated my post with these changes.

So fetching multiple models in a single route is not very straight forward - does that imply we should not be doing this?

Is the “ember way” to create a new data endpoint that gets the aggregated data? Then simply use that single model for the route?

2 Likes

can you access params from setupController?

if your model updates based on query params, then using refreshModel: true for query params won’t fire the setupController hook.

as of april 2015, i believe array controllera and object controller are deprecated, and you should use controller instead.

Not really. There are some specific use-cases in which you may need to load multiple models separately, without aggregating them under a new entity.

A good example for this is a dashboard, where you may need to load very different models or collections. Routes can do this (mostly via RSVP.hash) and so can data-loading components through the use of Services. I wrote about this topic a few months back: http://emberigniter.com/load-multiple-models-single-route/

ArrayController and ObjectController read like prehistoric words nowadays :slight_smile: And it is totally possible to load multiple models without even the use of controllers.