EmberJS: How to load multiple models on the same route?
Asked Answered
M

6

76

While I am not new to web development, I am quite new to to client-side MVC frameworks. I did some research and decided to give it a go with EmberJS. I went through the TodoMVC guide and it made sense to me...

I have setup a very basic app; index route, two models and one template. I have a server-side php script running that returns some db rows.

One thing that is very confusing me is how to load multiple models on the same route. I have read some information about using a setupController but I am still unclear. In my template I have two tables that I am trying to load with unrelated db rows. In a more traditional web app I would have just issued to sql statements and looped over them to fill the rows. I am having difficulty translating this concept to EmberJS.

How do I load multiple models of unrelated data on the same route?

I am using the latest Ember and Ember Data libs.

Update

although the first answer gives a method for handling it, the second answer explains when it's appropriate and the different methods for when it isn't appropriate.

Muscadine answered 11/12, 2013 at 14:38 Comment(1)
Does this answer your question? Request Two Models togetherLiddy
M
95

NOTE: for Ember 3.16+ apps, here is the same code, but with updated syntax / patterns: https://mcmap.net/q/243642/-emberjs-how-to-load-multiple-models-on-the-same-route

The below is for Ember < 3.16, even though the code would work as 3.16+ as fully backwards compatible, but it's not always fun to write older code.


You can use the Ember.RSVP.hash to load several models:

app/routes/index.js

import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return Ember.RSVP.hash({
      people: this.store.findAll('person'),
      companies: this.store.findAll('company')
    });
  },

  setupController(controller, model) {
    this._super(...arguments);
    Ember.set(controller, 'people', model.people);
    Ember.set(controller, 'companies', model.companies);
  }
});

And in your template you can refer to people and companies to get the loaded data:

app/templates/index.js

<h2>People:</h2>
<ul>
  {{#each people as |person|}}
    <li>{{person.name}}</li>
  {{/each}}
</ul>
<h2>Companies:</h2>
<ul>
  {{#each companies as |company|}}
    <li>{{company.name}}</li>
  {{/each}}
</ul>

This is a Twiddle with this sample: https://ember-twiddle.com/c88ce3440ab6201b8d58

Marindamarinduque answered 11/12, 2013 at 15:44 Comment(4)
This approach is fine provided you do not have any dynamic segments in your route. If you have dynamic segments and the route is entered via {{link-to 'index' someModel}} then the model hook is skipped entirely which will break this example. A more robust approach is to load any extra models in setupController which is always run.Assiduity
@EoinKelly I'd actually use the controller#afterModel event here since you can return a promise from afterModel and expect it to behave like model except afterModel won't be skipped.Criminology
How do you access the dynamic segment, params, or query params feom within setupcontroller?Malchus
@Eoin Kelly : you can get around it by passing n the ID (or slug) instead of the modelMalchus
L
155

BEWARE:

You want to be careful about whether or not returning multiple models in your model hook is appropriate. Ask yourself this simple question:

  1. Does my route load dynamic data based on the url using a slug :id? i.e. this.resource('foo', {path: ':id'});

If you answered yes

Do not attempt to load multiple models from the model hook in that route!!! The reason lies in the way Ember handles linking to routes. If you provide a model when linking to that route ({{link-to 'foo' model}}, transitionTo('foo', model)) it will skip the model hook and use the supplied model. This is probably problematic since you expected multiple models, but only one model would be delivered. Here's an alternative:

Do it in setupController/afterModel

App.IndexRoute = Ember.Route.extend({
  model: function(params) {
    return $.getJSON('/books/' + params.id);
  },
  setupController: function(controller, model){
    this._super(controller,model);
    controller.set('model2', {bird:'is the word'});
  }
});

Example: http://emberjs.jsbin.com/cibujahuju/1/edit

If you need it to block the transition (like the model hook does) return a promise from the afterModel hook. You will need to manually keep track of the results from that hook and hook them up to your controller.

App.IndexRoute = Ember.Route.extend({
  model: function(params) {
    return $.getJSON('/books/' + params.id);
  },
  afterModel: function(){
    var self = this;
    return $.getJSON('/authors').then(function(result){
      self.set('authors', result);
    });
  }, 
  setupController: function(controller, model){
    this._super(controller,model);
    controller.set('authors', this.get('authors'));
  }
});

Example: http://emberjs.jsbin.com/diqotehomu/1/edit

If you answered no

Go ahead, let's return multiple models from the route's model hook:

App.IndexRoute = Ember.Route.extend({
  model: function() {
    return {
           model1: ['red', 'yellow', 'blue'],
           model2: ['green', 'purple', 'white']
    };
  }
});

Example: http://emberjs.jsbin.com/tuvozuwa/1/edit

If it's something that needs to be waited on (such as a call to the server, some sort of promise)

App.IndexRoute = Ember.Route.extend({
  model: function() {
    return Ember.RSVP.hash({
           model1: promise1,
           model2: promise2
    });
  }
});

Example: http://emberjs.jsbin.com/xucepamezu/1/edit

In the case of Ember Data

App.IndexRoute = Ember.Route.extend({
  var store = this.store;
  model: function() {
    return Ember.RSVP.hash({
           cats: store.find('cat'),
           dogs: store.find('dog')
    });
  }
});

Example: http://emberjs.jsbin.com/pekohijaku/1/edit

If one is a promise, and the other isn't, it's all good, RSVP will gladly just use that value

App.IndexRoute = Ember.Route.extend({
  var store = this.store;
  model: function() {
    return Ember.RSVP.hash({
           cats: store.find('cat'),
           dogs: ['pluto', 'mickey']
    });
  }
});

Example: http://emberjs.jsbin.com/coxexubuwi/1/edit

Mix and match and have fun!

App.IndexRoute = Ember.Route.extend({
  var store = this.store;
  model: function() {
    return Ember.RSVP.hash({
           cats: store.find('cat'),
           dogs: Ember.RSVP.Promise.cast(['pluto', 'mickey']),
           weather: $.getJSON('weather')
    });
  }, 
  setupController: function(controller, model){
    this._super(controller, model);
    controller.set('favoritePuppy', model.dogs[0]);
  }
});

Example: http://emberjs.jsbin.com/joraruxuca/1/edit

Loux answered 11/12, 2013 at 15:40 Comment(4)
So if my model changes based on query params, not a dynamic segment, is my answer a yes or a no?Malchus
and how do i pass query params to the beforeModel, afterModel, and setController hooks?Malchus
You can still have multiple models and not break your links. if you have two dynamic segments, pass in an id (or string if you're constructing your model based on a slug via the serialize: hook in your route) rather than passing in a model in the link. FYI the syntax to access model properties in templates would be model.model1.someProperty or model.puppyModel.someOtherPropertyMalchus
You sure can, most of the queries above aren't dynamic segments, and most of the time from different routes you don't want to have to fetch a bunch of different models in a different route just to set up the links properly (once again applicable to non-dynamic models).Loux
M
95

NOTE: for Ember 3.16+ apps, here is the same code, but with updated syntax / patterns: https://mcmap.net/q/243642/-emberjs-how-to-load-multiple-models-on-the-same-route

The below is for Ember < 3.16, even though the code would work as 3.16+ as fully backwards compatible, but it's not always fun to write older code.


You can use the Ember.RSVP.hash to load several models:

app/routes/index.js

import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return Ember.RSVP.hash({
      people: this.store.findAll('person'),
      companies: this.store.findAll('company')
    });
  },

  setupController(controller, model) {
    this._super(...arguments);
    Ember.set(controller, 'people', model.people);
    Ember.set(controller, 'companies', model.companies);
  }
});

And in your template you can refer to people and companies to get the loaded data:

app/templates/index.js

<h2>People:</h2>
<ul>
  {{#each people as |person|}}
    <li>{{person.name}}</li>
  {{/each}}
</ul>
<h2>Companies:</h2>
<ul>
  {{#each companies as |company|}}
    <li>{{company.name}}</li>
  {{/each}}
</ul>

This is a Twiddle with this sample: https://ember-twiddle.com/c88ce3440ab6201b8d58

Marindamarinduque answered 11/12, 2013 at 15:44 Comment(4)
This approach is fine provided you do not have any dynamic segments in your route. If you have dynamic segments and the route is entered via {{link-to 'index' someModel}} then the model hook is skipped entirely which will break this example. A more robust approach is to load any extra models in setupController which is always run.Assiduity
@EoinKelly I'd actually use the controller#afterModel event here since you can return a promise from afterModel and expect it to behave like model except afterModel won't be skipped.Criminology
How do you access the dynamic segment, params, or query params feom within setupcontroller?Malchus
@Eoin Kelly : you can get around it by passing n the ID (or slug) instead of the modelMalchus
M
6

Taking the accepted answer, and updating it for Ember 3.16+

app/routes/index.js

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class IndexRoute extends Route {
  @service store;

  async model() {
    let [people, companies] = await Promise.all([
      this.store.findAll('person'),
      this.store.findAll('company'),
    ]);


    return { people, companies };
  }

}

Note, it's recommended to not use setupController to setup aliases, as it obfuscates where data is coming from and how it flows from route to template.

So in your template, you can do:

<h2>People:</h2>

<ul>
  {{#each @model.people as |person|}}
    <li>{{person.name}}</li>
  {{/each}}
</ul>

<h2>Companies:</h2>

<ul>
  {{#each @model.companies as |company|}}
    <li>{{company.name}}</li>
  {{/each}}
</ul>
Mernamero answered 21/6, 2020 at 15:48 Comment(0)
M
3

I use something like the answer that Marcio provided but it looks something like this:

    var products = Ember.$.ajax({
        url: api + 'companies/' +  id +'/products',
        dataType: 'jsonp',
        type: 'POST'
    }).then(function(data) {
        return data;
    });

    var clients = Ember.$.ajax({
        url: api + 'clients',
        dataType: 'jsonp',
        type: 'POST'
    }).then(function(data) {
        return data;
    });

    var updates = Ember.$.ajax({
        url: api + 'companies/' +  id + '/updates',
        dataType: 'jsonp',
        type: 'POST'
    }).then(function(data) {
        return data;
    });

    var promises = {
        products: products,
        clients: clients,
        updates: updates
    };

    return Ember.RSVP.hash(promises).then(function(data) {
      return data;
    });  
Mcqueen answered 8/5, 2014 at 14:37 Comment(4)
This doesn't look like the Ember wayOverspread
This returns an array of resolved promises from Ember.RSVP, why is not the ember way? maybe not the ember data way, but I wasn't using ember data. You can see the docs here: emberjs.com/api/classes/RSVP.Promise.htmlMcqueen
That's right, I meant the Ember Data way, ignore my first commentOverspread
This does not work as expected. The logic in then functions does not make any difference. You are messing up with async code and returns.Calc
G
2

If you use Ember Data, it gets even simpler for unrelated models:

import Ember from 'ember';
import DS from 'ember-data';

export default Ember.Route.extend({
  setupController: function(controller, model) {
    this._super(controller,model);
    var model2 = DS.PromiseArray.create({
      promise: this.store.find('model2')
    });
    model2.then(function() {
      controller.set('model2', model2)
    });
  }
});

If you only want to retrieve an object's property for model2, use DS.PromiseObject instead of DS.PromiseArray:

import Ember from 'ember';
import DS from 'ember-data';

export default Ember.Route.extend({
  setupController: function(controller, model) {
    this._super(controller,model);
    var model2 = DS.PromiseObject.create({
      promise: this.store.find('model2')
    });
    model2.then(function() {
      controller.set('model2', model2.get('value'))
    });
  }
});
Gasp answered 5/5, 2015 at 12:58 Comment(0)
S
2

The latest version of JSON-API as implemented in Ember Data v1.13 supports bundling of different resources in the same request very well, if you don't mind modifying your API endpoints.

In my case, I have a session endpoint. The session relates to a user record, and the user record relates to various models that I always want loaded at all times. It's pretty nice for it all to come in with the one request.

One caveat per the spec is that all of the entities you return should be linked somehow to the primary entity being received. I believe that ember-data will only traverse the explicit relationships when normalizing the JSON.

For other cases, I'm now electing to defer loading of additional models until the page is already loaded, i.e. for separate panels of data or whatever, so at least the page is rendered as quickly as possible. Doing this there's some loss/change with the "automatic" error loading state to be considered.

Sinh answered 5/8, 2015 at 17:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.