How to use multiple models with a single route in EmberJS / Ember Data?

30,382

Solution 1

Last update forever: I can't keep updating this. So this is deprecated and will likely be this way. here's a better, and more up-to-date thread EmberJS: How to load multiple models on the same route?

Update: In my original answer I said to use embedded: true in the model definition. That's incorrect. In revision 12, Ember-Data expects foreign keys to be defined with a suffix (link) _id for single record or _ids for collection. Something similar to the following:

{
    id: 1,
    title: 'string',
    body: 'string string string string...',
    author_id: 1,
    comment_ids: [1, 2, 3, 6],
    tag_ids: [3,4]
}

I have updated the fiddle and will do so again if anything changes or if I find more issues with the code provided in this answer.


Answer with related models:

For the scenario you are describing, I would rely on associations between models (setting embedded: true) and only load the Post model in that route, considering I can define a DS.hasMany association for the Comment model and DS.belongsTo association for the User in both the Comment and Post models. Something like this:

App.User = DS.Model.extend({
    firstName: DS.attr('string'),
    lastName: DS.attr('string'),
    email: DS.attr('string'),
    posts: DS.hasMany('App.Post'),
    comments: DS.hasMany('App.Comment')
});

App.Post = DS.Model.extend({
    title: DS.attr('string'),
    body: DS.attr('string'),
    author: DS.belongsTo('App.User'),
    comments: DS.hasMany('App.Comment')
});

App.Comment = DS.Model.extend({
    body: DS.attr('string'),
    post: DS.belongsTo('App.Post'),
    author: DS.belongsTo('App.User')
});

This definition would produce something like the following:

Associations between models

With this definition, whenever I find a Post, I will have access to a collection of comments associated with that post, and the comment's author as well, and the user which is the author of the post, since they are all embedded. The route stays simple:

App.PostsPostRoute = Em.Route.extend({
    model: function(params) {
        return App.Post.find(params.post_id);
    }
});

So in the PostRoute (or PostsPostRoute if you're using resource), my templates will have access to the controller's content, which is the Post model, so I can refer to the author, simply as author

<script type="text/x-handlebars" data-template-name="posts/post">
    <h3>{{title}}</h3>
    <div>by {{author.fullName}}</div><hr />
    <div>
        {{body}}
    </div>
    {{partial comments}}
</script>

<script type="text/x-handlebars" data-template-name="_comments">
    <h5>Comments</h5>
    {{#each content.comments}}
    <hr />
    <span>
        {{this.body}}<br />
        <small>by {{this.author.fullName}}</small>
    </span>
    {{/each}}
</script>

(see fiddle)


Answer with non-related models:

However, if your scenario is a little more complex than what you described, and/or have to use (or query) different models for a particular route, I would recommend to do it in Route#setupController. For example:

App.PostsPostRoute = Em.Route.extend({
    model: function(params) {
        return App.Post.find(params.post_id);
    },
    // in this sample, "model" is an instance of "Post"
    // coming from the model hook above
    setupController: function(controller, model) {
        controller.set('content', model);
        // the "user_id" parameter can come from a global variable for example
        // or you can implement in another way. This is generally where you
        // setup your controller properties and models, or even other models
        // that can be used in your route's template
        controller.set('user', App.User.find(window.user_id));
    }
});

And now when I'm in the Post route, my templates will have access to the user property in the controller as it was set up in setupController hook:

<script type="text/x-handlebars" data-template-name="posts/post">
    <h3>{{title}}</h3>
    <div>by {{controller.user.fullName}}</div><hr />
    <div>
        {{body}}
    </div>
    {{partial comments}}
</script>

<script type="text/x-handlebars" data-template-name="_comments">
    <h5>Comments</h5>
    {{#each content.comments}}
    <hr />
    <span>
        {{this.body}}<br />
        <small>by {{this.author.fullName}}</small>
    </span>
    {{/each}}
</script>

(see fiddle)

Solution 2

Using Em.Object to encapsulate multiple models is a good way to get all data in model hook. But it can't ensure all data is prepared after view rendering.

Another choice is to use Em.RSVP.hash. It combines several promises together and return a new promise. The new promise if resolved after all the promises are resolved. And setupController is not called until the promise is resolved or rejected.

App.PostRoute = Em.Route.extend({
  model: function(params) {
    return Em.RSVP.hash({
      post:     // promise to get post
      comments: // promise to get comments,
      user:     // promise to get user
    });
  },

  setupController: function(controller, model) {
    // You can use model.post to get post, etc
    // Since the model is a plain object you can just use setProperties
    controller.setProperties(model);
  }
});

In this way you get all models before view rendering. And using Em.Object doesn't have this advantage.

Another advantage is you can combine promise and non-promise. Like this:

Em.RSVP.hash({
  post: // non-promise object
  user: // promise object
});

Check this to learn more about Em.RSVP: https://github.com/tildeio/rsvp.js


But don't use Em.Object or Em.RSVP solution if your route has dynamic segments

The main problem is link-to. If you change url by click link generated by link-to with models, the model is passed directly to that route. In this case the model hook is not called and in setupController you get the model link-to give you.

An example is like this:

The route code:

App.Router.map(function() {
  this.route('/post/:post_id');
});

App.PostRoute = Em.Route.extend({
  model: function(params) {
    return Em.RSVP.hash({
      post: App.Post.find(params.post_id),
      user: // use whatever to get user object
    });
  },

  setupController: function(controller, model) {
    // Guess what the model is in this case?
    console.log(model);
  }
});

And link-to code, the post is a model:

{{#link-to "post" post}}Some post{{/link-to}}

Things become interesting here. When you use url /post/1 to visit the page, the model hook is called, and setupController gets the plain object when promise resolved.

But if you visit the page by click link-to link, it passes post model to PostRoute and the route will ignore model hook. In this case setupController will get the post model, of course you can not get user.

So make sure you don't use them in routes with dynamic segments.

Solution 3

For a while I was using Em.RSVP.hash, however the problem I ran into was that I didn't want my view to wait until all models were loaded before rendering. However, I found a great (but relatively unknown) solution thanks to the folks at Novelys that involves making use of the Ember.PromiseProxyMixin:

Let's say you have a view that has three distinct visual sections. Each of these sections should be backed by its own model. The model backing the "splash" content at the top of the view is small and will load quickly, so you can load that one normally:

Create a route main-page.js:

import Ember from 'ember';

export default Ember.Route.extend({
    model: function() {
        return this.store.find('main-stuff');
    }
});

Then you can create a corresponding Handlebars template main-page.hbs:

<h1>My awesome page!</h1>
<ul>
{{#each thing in model}}
    <li>{{thing.name}} is really cool.</li>
{{/each}}
</ul>
<section>
    <h1>Reasons I Love Cheese</h1>
</section>
<section>
    <h1>Reasons I Hate Cheese</h1>
</section>

So let's say in your template you want to have separate sections about your love/hate relationship with cheese, each (for some reason) backed by its own model. You have many records in each model with extensive details relating to each reason, however you'd like the content on top to render quickly. This is where the {{render}} helper comes in. You can update your template as so:

<h1>My awesome page!</h1>
<ul>
{{#each thing in model}}
    <li>{{thing.name}} is really cool.</li>
{{/each}}
</ul>
<section>
    <h1>Reasons I Love Cheese</h1>
    {{render 'love-cheese'}}
</section>
<section>
    <h1>Reasons I Hate Cheese</h1>
    {{render 'hate-cheese'}}
</section>

Now you'll need to create controllers and templates for each. Since they're effectively identical for this example, I'll just use one.

Create a controller called love-cheese.js:

import Ember from 'ember';

export default Ember.ObjectController.extend(Ember.PromiseProxyMixin, {
    init: function() {
        this._super();
        var promise = this.store.find('love-cheese');
        if (promise) {
            return this.set('promise', promise);
        }
    }
});

You'll notice that we are using the PromiseProxyMixin here, which makes the controller promise-aware. When the controller is initialized, we indicate that the promise should be loading the love-cheese model via Ember Data. You'll need to set this property on the controller's promise property.

Now, create a template called love-cheese.hbs:

{{#if isPending}}
  <p>Loading...</p>
{{else}}
  {{#each item in promise._result }}
    <p>{{item.reason}}</p>
  {{/each}}
{{/if}}

In your template, you'll be able to render different content depending on the state of promise. When your page initially loads, your "Reasons I Love Cheese" section will display Loading.... When the promise is loaded, it will render all the reasons associated for each record of your model.

Each section will load independently and not block the main content from rendering immediately.

This is a simplistic example, but I hope everyone else finds it as useful as I did.

If you're looking to do something similar for many rows of content, you may find the Novelys example above even more relevant. If not, the above should work fine for you.

Solution 4

This might not be best practice and a naïve approach, but it shows conceptually how you would go about having on multiple models available on one central route:

App.PostRoute = Ember.Route.extend({
  model: function() {
    var multimodel = Ember.Object.create(
      {
        posts: App.Post.find(),
        comments: App.Comments.find(),
        whatever: App.WhatEver.find()
      });
    return multiModel;
  },
  setupController: function(controller, model) {
    // now you have here model.posts, model.comments, etc.
    // as promises, so you can do stuff like
    controller.set('contentA', model.posts);
    controller.set('contentB', model.comments);
    // or ...
    this.controllerFor('whatEver').set('content', model.whatever);
  }
});

hope it helps

Solution 5

Thanks to all the other excellent answers, I created a mixin that combines the best solutions here into a simple and reusable interface. It executes an Ember.RSVP.hash in afterModel for the models you specify, then injects the properties into the controller in setupController. It does not interfere with the standard model hook, so you would still define that as normal.

Example use:

App.PostRoute = Ember.Route.extend(App.AdditionalRouteModelsMixin, {

  // define your model hook normally
  model: function(params) {
    return this.store.find('post', params.post_id);
  },

  // now define your other models as a hash of property names to inject onto the controller
  additionalModels: function() {
    return {
      users: this.store.find('user'), 
      comments: this.store.find('comment')
    }
  }
});

Here is the mixin:

App.AdditionalRouteModelsMixin = Ember.Mixin.create({

  // the main hook: override to return a hash of models to set on the controller
  additionalModels: function(model, transition, queryParams) {},

  // returns a promise that will resolve once all additional models have resolved
  initializeAdditionalModels: function(model, transition, queryParams) {
    var models, promise;
    models = this.additionalModels(model, transition, queryParams);
    if (models) {
      promise = Ember.RSVP.hash(models);
      this.set('_additionalModelsPromise', promise);
      return promise;
    }
  },

  // copies the resolved properties onto the controller
  setupControllerAdditionalModels: function(controller) {
    var modelsPromise;
    modelsPromise = this.get('_additionalModelsPromise');
    if (modelsPromise) {
      modelsPromise.then(function(hash) {
        controller.setProperties(hash);
      });
    }
  },

  // hook to resolve the additional models -- blocks until resolved
  afterModel: function(model, transition, queryParams) {
    return this.initializeAdditionalModels(model, transition, queryParams);
  },

  // hook to copy the models onto the controller
  setupController: function(controller, model) {
    this._super(controller, model);
    this.setupControllerAdditionalModels(controller);
  }
});
Share:
30,382

Related videos on Youtube

Anonymous
Author by

Anonymous

Updated on May 13, 2020

Comments

  • Anonymous
    Anonymous almost 4 years

    From reading the docs, it looks like you have to (or should) assign a model to a route like so:

    App.PostRoute = Ember.Route.extend({
        model: function() {
            return App.Post.find();
        }
    });
    

    What if I need to use several objects in a certain route? i.e. Posts, Comments and Users? How do I tell the route to load those?

  • Anonymous
    Anonymous almost 11 years
    Thank you so much for taking the time to post that, I found it really useful.
  • intuitivepixel
    intuitivepixel almost 11 years
    @MilkyWayJoe, really good post! Now my approach looks really naïve :)
  • MilkyWayJoe
    MilkyWayJoe almost 11 years
    This approach is fine, just not taking too much advantage of Ember Data. For some scenarios where the models are not related, I'd got for something similar to this.
  • MilkyWayJoe
    MilkyWayJoe over 10 years
    My answer applies to an earlier version of Ember & Ember-Data. This is a really good approach +1
  • darkbaby123
    darkbaby123 over 10 years
    Actually there is. if you want to pass model id instead of model itself to link-to helper, the model hook is always triggered.
  • miguelcobain
    miguelcobain over 10 years
    The problem with your non-related models is that it doesn't accept promises like the model hook does, right? Is there any workaround for that?
  • yorbro
    yorbro about 10 years
    This should be documented somewhere on the Ember guides (best practices or something). It's an important use case that I'm sure many people encounter.
  • Duncan Walker
    Duncan Walker about 10 years
    I think using this inside the promise would give an error message. You could set var _this = this before the return and then do _this.set( inside the then( method to get the desired outcome
  • Fabio
    Fabio about 10 years
    If I understand your problem correctly, you can adapt it in an easy way. Just wait for the promise to fulfill and only then set the model(s) as a variable of the controller
  • Damon Aw
    Damon Aw almost 10 years
    Other than iterating and displaying comments, it will be lovely if you could show an example of how someone might add an new comment into post.comments.
  • David Tuite
    David Tuite almost 10 years
    The syntax for specifying associations has changed since Ember Data beta2 was released. It should now be posts: DS.hasMany('post') etc. See this question for more details.
  • Eoin Kelly
    Eoin Kelly almost 10 years
    Ember-data will make all relationships async before 1.0 so your advice to use model relationships is probably not the best option anymore as it would make the controller deal with the promises (which is possible but painful IME). Perhaps you could edit your answer to promote your "non related models" solution?
  • Mateusz Nowak
    Mateusz Nowak over 9 years
    If you are using controller.setProperties(model); do not forget to add these properties with default value to controller. Otherwise it will throw exception Cannot delegate set...
  • josiah
    josiah about 9 years
    This answer is superb. Answered one of the nagging questions about the correct way to do such things in Ember for the longest time!
  • Joe
    Joe almost 9 years
    I have a post edit route/view where I want to render all the existing tags in the DB so that the use can click to add them to the post being edited. I want to define a variable that represents an array/collection of these tags. Would the approach you used above work for this?
  • AWM
    AWM almost 9 years
    Sure, you would create a PromiseArray (e.g. "tags"). Then, in your template you would pass it to the selection element of the corresponding form.
  • Anuj Kulkarni
    Anuj Kulkarni over 8 years
    For associated models, in the route when I load root model and I want to load all the child model data before the controller is set up, how can I achieve that ?