I'm using firebase in combination with Ember CLI. I have the following setup:
ember.debug.js:6401 DEBUG: Ember : 2.4.5
ember.debug.js:6401 DEBUG: Ember Data : 2.5.1
ember.debug.js:6401 DEBUG: Firebase : 2.4.2
ember.debug.js:6401 DEBUG: EmberFire : 1.6.6
ember.debug.js:6401 DEBUG: jQuery : 2.2.3
I have two simple models
<!-- app/models/user.js -->
import Model from 'ember-data/model';
export default Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
email: DS.attr('string'),
profile: DS.belongsTo('profile', {async: true})
});
And a second model for my profile
<!-- app/models/profile.js -->
import Model from 'ember-data/model';
export default Model.extend({
companyName: DS.attr('string'),
user: DS.belongsTo('user', {async: true})
});
I have the following profile route:
<!-- app/routes/profile.js -->
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.store.query('user', { orderBy: 'email', equalTo: this.get('session.currentUser.email')}).then(function(user){
console.log(user);
return user;
});
}
});
I check if the current session email address is equalTo a email address in the database. And return the user objet. This is working. (don't know if this is the right way to do this?)
In my profile handlebars template i have the following code.
<!-- app/templates/profile.hbs -->
{{#each model as |user|}}
{{user.firstName}}
{{user.lastName}}
{{user.profile}}
{{/each}}
This returns the following on screen:
frank spin <DS.PromiseObject:ember545>
My guess is that the relationship data has not yet been received. I don't know how to solve this issue. And second question: Is my checking for the current logged in user the right way?
You're correct that the relationship data has not been received. But if you write your template like this, you should still see the profile information when it loads:
{{#each model as |user|}}
{{user.firstName}}
{{user.lastName}}
{{user.profile.companyName}}
{{/each}}
PromiseObjects in templates
The goal of promise objects (and promise arrays) is to allow you to bind data in Ember before it's loaded, and have those bindings update once the promise resolves. This is great for secondary-importance information, which can safely be rendered after the rest of the page loads.
If you try to render your profile model properties when the promise has not resolved, you'll get a blank space. You can display loading state information using the isPending property:
{{#each model as |user|}}
{{user.firstName}}
{{user.lastName}}
{{#if user.profile.isPending}}
<span class="spinner">…</span>
{{else}}
{{user.profile.companyName}}
{{/if}}
{{/each}}
And you can also use the isRejected property to detect API failures, and allow users to retry.
Load before render
If this asynchronous behaviour is not what you want, you can force the relationship promise to resolve before rendering the template in the afterModel hook of your route:
export default Ember.Route.extend({
model() {
return this.store.query('user', { orderBy: 'email',
equalTo: this.get('session.currentUser.email')});
},
afterModel(users) {
return Ember.RSVP.all(users.invoke('get', 'profile');
}
});
(With a simpler single model, you could just write return model.get('profile') in the afterModel hook.)
Any promise returned from afterModel will block loading of the route until it resolves. Then your template will always have the profile of the user available when rendering.
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.
BEWARE:
You want to be careful about whether or not returning multiple models in your model hook is appropriate. Ask yourself this simple question:
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
NOTE: for Ember 3.16+ apps, here is the same code, but with updated syntax / patterns: https://stackoverflow.com/a/62500918/356849
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
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>
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;
});
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'))
});
}
});
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.
I have a list of products that will be loaded under the /products route, from there you can navigate to a single product under the /products/:product_id. This is my models and the route:
var Product = DS.Model.extend({
page_title: DS.attr('string'),
image: DS.attr('string')
});
var ProductComment = DS.Model.extend({
contents: DS.attr('string')
});
var ProductRoute = Ember.Route.extend({
model: function(params) {
return App.Product.find(params.product_id)
},
setupController: function(controller, model) {
controller.set('content', model);
}
});
On the product page I want to load the products and additionally the comments for a product. As I use an external Api I cant load the id of the comments into the product model. So now I want to load the comments in to the ProductsController. I tried like described in this SO but it doesn't work. I'm using EmberDatas RESTAdapter.
I came up with solution. In the modelAfter hook of the products route, check if the comments are loaded into the model using this.get('product_comments').content.length. If not, load the data using App.ProductComment.find({product_id: this.id}) and store them into the model.
App.ProductRoute = Ember.Route.extend({
afterModel: function(model) {
model.ensureComments();
}
});
Product = DS.Model.extend({
page_title: DS.attr('string'),
image: DS.attr('string'),
product_comments: DS.hasMany('App.ProductComment'),
ensureComments: function() {
var productcomments = this.get('product_comments');
if (!productcomments.content.length) {
App.ProductComment.find({product_id: this.id}).then(function(result) {
productcomments.pushObjects(result)
});
}
}
});
Struggling with populating Ember with data.
I'm using Rails as the backend, and when I hit /contacts.json (ContactsRoute), it returns a list of id, first, last -- works as expected.
However, when visiting a detail view (ContactRoute), I would like to hit /contacts/1.json and fetch details like email address, anniversaries, etc. But since I have a dynamic segment the model hook is skipped and none of the associations are available.
Question: what is the best approach for fetching additional data in a list/detail scenario?
Models:
App.Contact = DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
company: DS.attr('string'),
emails: DS.hasMany('App.Email'),
});
App.Email = DS.Model.extend({
contact: DS.belongsTo('App.Contact'),
emailAddress: DS.attr('string'),
});
Route:
App.Router.map(function() {
this.resource('contacts');
this.resource('contact', {path: 'contacts/:contact_id'});
});
App.ContactsRoute = Ember.Route.extend({
init: function() {},
model: function() {
return App.Contact.find();
}
});
App.ContactRoute = Ember.Route.extend({
model: function(params) {
return App.Contact.find(params.contact_id);
}
});
Thanks in advance.
I just posted an answer to a similar problem here: https://stackoverflow.com/a/18553153/1254484
Basically, in your App.ContactRoute, override the setupController method:
setupController: function(controller, model) {
controller.set("model", model);
model.reload();
return;
}
I'm using this with the latest ember-data (commit ef11bff from 2013-08-26).
Have an upcoming weekend project and using it to evaluate Ember.js and I cannot figure out why I cannot display nested objects in my template. This does not work:
{{#each emails}}
{{email_address}}
{{/each}}
When I try just {{emails}} I get a hint that something is right:
Models:
App.Contact = DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
company: DS.attr('string'),
emails: DS.hasMany('App.Email')
});
App.Email = DS.Model.extend({
contact: DS.belongsTo('App.Contact'),
emailAddress: DS.attr('string'),
});
Route:
App.Router.map(function() {
this.resource('contacts', function() {
this.resource('contact', {path: ':contact_id'});
});
});
App.ContactsRoute = Ember.Route.extend({
init: function() {},
model: function() {
return App.Contact.find();
}
});
App.ContactRoute = Ember.Route.extend({
model: function(params) {
return App.Contact.find(params.contact_id);
}
});
I have no idea what to try next. I'm using active_model_serializer in Rails. I've tried embedding, side-loading to no avail. I'm sure it's something simple that I'm missing.Thanks in advance!
When using the each helper it's recomendable to be more specific about the items you are looping over to avoid such problems.
Try the following:
{{#each email in model.emails}}
{{email.emailAddress}}
{{/each}}
This should also work:
{{#each emails}}
{{this.emailAddress}}
{{/each}}
And also, your model property is called emailAddress and not email_address.
Hope it helps.