I'm trying to build the following view with Ember.js:
Users: (x in total)
* User 1: y Posts
* User 2: z Posts
I've created a itemController that is responsible for getting the number of posts of each user.
App.IndexItemController = Ember.ObjectController.extend({
postCount: function() {
var posts = this.get('content').get('posts');
return posts.get('length');
}.property()
});
Full code on jsbin.
Somehow I always get 0 posts for each user, I guess that is because the relationship is not resolved correctly at this.get('content').get('posts'). What would be the right way to do this? Or am I going a completely wrong way?
Bonus question: What can I pass to the property() and should I pass something to it?
You need to set the dependent keys of your computed property, in your case content.posts.length. So the postCount knows when need to be updated.
App.IndexItemController = Ember.ObjectController.extend({
postCount: function() {
var posts = this.get('content').get('posts');
return posts.get('length');
}.property('content.posts.length')
});
Now your computed property is correct, but no data is loaded, this happen because there isn't posts associated with your users, no in the user -> post direction. So you need to add it in the fixture:
App.User.FIXTURES = [
{
id: 1,
name: 'Jon',
nick: 'Jonny',
posts: [1]
},
{
id: 2,
name: 'Foo',
nick: 'Bar',
posts: [2]
}
];
After this an error is raised Uncaught Error: Assertion Failed: You looked up the 'posts' relationship on '<App.User:ember280:1>' but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (`DS.hasMany({ async: true })`).
Ember data identified that you have an async relationship, and warns you to setup the property with async: true
App.User = DS.Model.extend({
name: DS.attr('string'),
nick: DS.attr('string'),
posts: DS.hasMany('post', { async: true })
});
This your updated jsbin
Related
I have 3 different fixture models, as shown below.
var Room = DS.Model.extend({
title: DS.attr('string'),
categories: DS.hasMany('Category', { async: true }),
isSelected: DS.attr('boolean')
});
var Category = DS.Model.extend({
title: DS.attr('string'),
room: DS.belongsTo('Room', {async: true }),
materials: DS.hasMany('Material', { async: true }),
isSelected: DS.attr('boolean')
});
var Material = DS.Model.extend({
title: DS.attr('string'),
category: DS.belongsTo('Category', {async: true} ),
isSelected: DS.attr('boolean')
});
I find when I try to view the contents inside the Materials model it is blank. In my controller I expose the materials by doing this:
currentMaterials: function() {
var room = this.filterBy('isSelected', true).get('firstObject');
var categories = room.get('categories');
var selectedCategory = categories.get('firstObject');
var material = selectedCategory.get('materials');
return material;
}.property('#each.isSelected')
However when I try to access currentMaterials the value is null. I am ONLY able to access its values if I first access the Rooms/Categories using a {{#each} loop. Oddly once I do the {{#each}} I am then able to access the values in currentMaterials.
Does anyone understand why?
It's due to fact of promises existance. Your categories relationship is async, which means that it's not present initially and ember-data should fetch it if needed. However, it takes time to fetch data, therefore ember-data returns a promise from this: var categories = room.get('categories'). After that promise, you first get firstObject from it, which does not exist for a promise (is null), and than you get materials relationship from that null. It simply is null.
However, ember templates are smart and if you put an each on them, they know that these relationships are needed and makes ember-data fetch these data.
What you can do? If you need this data to perform page-specific job, you should make sure that you have access to it before showing the page to the user - therefore in the model hook. You can use Ember.RSVP to make multiple fetch calls and set them up in the controller:
model: function() {
data =
room: store.find("room")
categories: store.find("category)
materials: store.find("material")
return Ember.RSVP.hash(data)
}
However, take notice that it will fetch all the materials, etc. If you need only the ones connected to your model, you should consider speeding up your data fetching using side loading. If you are using fixtures, it won't work.
Last that I can think of is making computed property a method that would fetch the data, but set them on other variable. You can use some kind of flag to inform the app when the data is ready:
currentMaterials: function() {
var room = this.filterBy('isSelected', true).get('firstObject');
room.get('categories').then(function(categories) {
return categories.get('firstObject').get('materials');
}).then(function(materials) {
// here you have your materials
// you can pass _this to that method and set these materials
// on some kind of controller property (e.g. materialsChosen)
// and use a flag like setting 'is Fetching' on the start of this
// computed property and setting down right here
});
}.property('#each.isSelected')
I'm using ember.js with localstorage-adaper.js.
My problem started with an already answered question: EmberJS - record with hasMany relation fails to load
So having the models:
// Models
App.List = DS.Model.extend({
name: DS.attr('string'),
items: DS.hasMany('item')
});
App.Item = DS.Model.extend({
name: DS.attr('string') ,
list: DS.belongsTo('list')
});
When the #/list/1 template got rendered the the items weren't shown on the page and an assertion failed was thrown in the console:
Assertion failed: You looked up the 'items' relationship on 'App.List:ember236:1' but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (DS.attr({ async: true }))
As specified, the solution is to make the hasMany async like this:
App.List = DS.Model.extend({
name: DS.attr('string'),
items: DS.hasMany('item',{async:true})
});
It works great for that scenario!
Next:
I'm loading data from the server and push it into the store when the application first loads like this: (You can find the JSBin for the example here: http://jsbin.com/lejizo/1/)
var data = { //in real life this is fetched through an AJAX request
'list': { id: '1', name: 'The List', items: ['1','2'] },
'items': {
'1': { id: '1', name: 'item 1', list: '1' },
'2': { id: '2', name: 'item 2', list: '1' }
}
};
...
this.store.push('list', data.list).save();
this.store.pushMany('item', data.items).forEach(function (item) {
item.save();
});
After setting the async:true option, I've noticed that the items ids are not persisted anymore into the localstorage. The JSON looks like this:
{"App.List":{"records":{"1":{"id":"1","name":"The List","items":[]}}},"App.Item":{"records":{}}}
Obviously the items are not shown since there is no reference to them.
I might think that I should find another way to populate the locastorage at first! How?
Or there is another workaround for this situation?
Working with aync:true on hasMany relationships beside the foreign keys not being persisted to the localstorae, also causes additional problems inside controllers. You get the Promise, not the actual object, so you always must use list.get('items').then(/*code here*/) (check out this question I've posted Ember local-storage How to use hasMany with async: true in controllers?), which in some scenarios where you use it inside a loop might cause a stackoverflow.
Using localstorage, you'll allways have all the data on the client side. It really makes no sense to work with async:true. The problem is that ember expects an array of vanilla objects for the list.items, instead of just ids. This is described here DS.FixtureAdapter loses fixture data with hasMany async attributes
There is an easy workaround for getting rid of async:true in your hasMany relationships. Please note that you have all needed data in localstorage! Ember doesn't throw the: "Assertion failed: You looked up the 'items' relationship on 'App.List:ember236:1' but some of the associated records were not loaded..." error anymore if he sees the items inside the memory store.
SOLUTION:
Ember.Route.reopen({
beforeModel: function(transition){
this.store.find('list');
this.store.find('items');
}
});
We override the Ember.Route object, so that in the beforeModel hook we call store.find('object'). What this does is forces Ember to load data from localstorage to the "in memory" store! You won't need the async:true anymore and no error will be thrown. Also, at first initialization the foreign keys will be persisted too!
You have to do this everytime (in the Route super class) because you'll never know on which route a refresh will occur. Also, for a store of ~10 models with up to 50 records, it runs in ~50-70ms. If this seems to much in your scenario, make sure you do this call only on the route you want and only for the models it needs.
Also, if you override beforeModel inside your routes, make sure you call
this._super(transition)
Hope this helps!
You defined your data wrong, try this:
var data = { //in real life this is fetched through an AJAX request
'list': { id: '1', name: 'The List', items: [1,2] },
'items': [
{ id: '1', name: 'item 1', list: '1' },
{ id: '2', name: 'item 2', list: '1' }
}];
}
First you do not need a index 1,2,... in front of your items.
Second hasMany relationship should be return an array that is why items is wrap as an array.
So to fix your issue you either have to fix the data from the server, or write a serializer to massage the data.
Hope it helps!
I have a model which 'owns' other models (belongTo, hasMany) and I'm attempting to create each in a multi-step form.
I am passing the parent model, campaign, through with each link-to so it can be used when creating the other records and displaying existing children. This worked when I originally had a nested route, but when I change it to a resource the campaign model seemingly isn't sent through.
My router:
App.Router.map(function () {
this.resource('campaigns', function() {
// I originally had a this.route('step2') which I sent the model to and it works
this.resource('campaign', { path: '/:campaign_id' }, function() {
this.route('edit');
this.resource('ad-groups', function() {
this.route('new'); // new route is irrelevant in this question
});
});
});
});
So in the campaign/edit router, I save the model and transition to the ad-groups/index route with the campaign as the model:
App.CampaignEditRoute = Ember.Route.extend({
actions: {
save: function(campaign) {
campaign.save().then(function() {
this.transitionTo('ad-groups', campaign);
}.bind(this));
}
}
});
But this is where it renders the template, but the Ember inspector shows it's not loading a model. And the relevant parts of my models for completeness:
App.Campaign = DS.Model.extend({
adGroups: DS.hasMany('ad-group', { async:true }),
...
});
App.AdGroup = DS.Model.extend({
campaign: DS.belongsTo('campaign'),
...
});
If I manually return the campaign within the model hook of ad-groups/index then Ember throws an Uncaught #error, and renders the template but without any of the model data showing.
Edit: So I tried implementing the solution below by setting the model as this.modelFor('campaign') and it threw another Uncaught #error. Bizarrely, when I remove the relationship in my FIXTURES it no longer errors, but of course I lose that relationship. My fixtures:
App.Campaign.FIXTURES = [
// If I remove the adGroups: [1] it removes the Uncaught #error but I lose the relationship
{ id: 1, name: "Campaign #1", app: 1, adGroups: [1] },
{ id: 2, name: "Campaign #2", app: 1 }
];
App.AdGroup.FIXTURES = [
{ id: 1, name: 'My first AdGroup', bid: 2, budget: 100, campaign: 1 }
];
I had a similar issue to this before, and I fixed it by adding { async: true } to my campaign model as you can see above. So this looks like it's more the issue, admittedly async confuses me a little still. Could anyone shed some light on why this might be happening?
After all of this, the two part solution was:
1.) Instead of passing the model using the link-to, implementing this:
App.MyRoute = Ember.Route.extend({
model: function() {
return this.modelFor('campaign');
},
...
});
Which in my opinion shouldn't be necessary, but it's actually really nice in that it uses a parent routes' model - link for reference.
2.) The remaining Uncaught #error error that Ember was throwing was extremely unhelpful. It turns out I was linking to adgroup instead of ad-group in the template:
{{link-to 'adgroup'}} instead of {{link-to 'ad-group'}}
I have models defined as :
App.Answer = DS.Model.extend({
name: DS.attr('string'),
layoutName: DS.attr('string')
});
App.Question = DS.Model.extend({
name: DS.attr('string'),
answers: DS.hasMany('answer', {async: true})
});
I have a component that allows for deleting and adding answers to question model. The component comes with apply and cancel button and when the user clicks on cancel, I wanted all the changes(adds/deletes of answers) to be reverted. Currently rollback doesn't do the trick, I event tried model.reload() when using rest adapter and that didn't work for me either. Any idea how I can go about doing a rollback in this situation?
When using the rest adapter, I pretty much fall to the issue pointed here : EmberJS cancel (rollback) object with HasMany
Thanks, Dee
UPDATE :
Since I couldn't perform rollback the intended way, I performed these steps:
1) get all the answers back from the server
2) remove answer association from the question
3) manually add answer association to the question model from data received from server
This seems to be working well BUT sadly I am getting this one error that I cannot shake off.
Here is a jsbin of updated progress: http://jsbin.com/uWUmUgE/2/
Here you can create new answer and then append it to question and do rollback too. BUT, if you follow these steps, you will see the issue I am facing:
1) delete an answer
2) add an answer
3) perform rollback
4) add an answer
It throws this error:
Error: Attempted to handle event didSetProperty on while in state root.deleted.uncommitted. Called with {name: position, oldValue: 1, originalValue: 1, value: 2}.
I will super appreciate any help you can provide.
WORK-AROUND:
One simple workaround was to just hide the answers on delete. I modified the model a bit like:
App.Answer = DS.Model.extend({
name: DS.attr('string'),
layoutName: DS.attr('string'),
markToDelete: DS.attr('boolean', {default: false})
});
And my rollback function had this logic:
answers.forEach(function (answer) {
if(!answer.get('id')){
//newly created answer models that has not persisted in DB
question.get('answers').removeObject(answer);
answer.deleteRecord();
} else {
answer.rollback();
}
});
I'm not sure of your scope but for this relationship (I'm actually rolling back the belongsTo here but I'm curious if this helps in any way)
App.Appointment = DS.Model.extend({
name: DS.attr('string'),
customer: DS.belongsTo('customer', {async: true})
});
App.Customer = DS.Model.extend({
name: DS.attr('string'),
appointments: DS.hasMany('appointment', {async: true})
});
I'm able to rollback both the appointment and it's hasMany customer model like so (from within my route)
App.AppointmentRoute = Ember.Route.extend({
actions: {
willTransition: function(transition) {
var context = this.get('context');
var dirty =context.get('isDirty');
var dirtyCustomer=context.get('customer.isDirty');
var message = "foo";
if ((dirty || dirtyCustomer) && confirm(message)) {
transition.abort();
}else{
context.get('customer').get('content').rollback();
context.rollback();return true;
}
}
});
Has anybody come up with an answer for polymorphic associations and ember-data?
We would need some way of being able to query the type at the other end of the relationship from what I can tell.
Anybody any thoughts on this?
With the latest ember-data build you can now use polymorphic associations:
You need to configure your Models to make it polymorphic:
/* polymorphic hasMany */
App.User = DS.Model.extend({
messages: DS.hasMany(App.Message, {polymorphic: true})
});
App.Message = DS.Model.extend({
created_at: DS.attr('date'),
user: DS.belongsTo(App.User)
});
App.Post = App.Message.extend({
title: DS.attr('string')
});
/* polymorphic belongsTo */
App.Comment = App.Message.extend({
body: DS.attr('string'),
message: DS.belongsTo(App.Message, {polymorphic: true})
});
You also need to configure alias properties on your RESTAdapter
DS.RESTAdapter.configure('App.Post' {
alias: 'post'
});
DS.RESTAdapter.configure('App.Comment' {
alias: 'comment'
});
The result expected from your server should be like this:
{
user: {
id: 3,
// For a polymorphic hasMany
messages: [
{id: 1, type: "post"},
{id: 1, type: "comment"}
]
},
comment: {
id: 1,
// For a polymorphic belongsTo
message_id: 1,
message_type: "post"
}
}
More information in this github thread
So I have something. It's not finished, or entirely clean, but it works. Basically, I use a mixin to bypass the Ember associations entirely. I'm sure that this could be rolled into the adapter or the store, but for now this works.
Polymorphic models come through the the JSON with an itemId and itemType:
App.Follow = DS.Model.extend
user: DS.belongsTo('App.User')
itemId: DS.attr("number")
itemType: DS.attr("string")
I add a mixin to the models that are associated with it :
App.Hashtag = DS.Model.extend App.Polymorphicable,
follows:(->
name: DS.attr("string")
#polymorphicFilter(App.Follow, "Hashtag")
).property('changeCount') #changeCount gives us something to bind to
followers: (->
#get('follows').map((item)->item.get('user'))
).property('follows')
The mixin implements three methods, one that updates the changeCount, one that returns the model's type and the polymorphicFilter method that filters a model by itemType and id:
App.Polymorphicable = Ember.Mixin.create
changeCount: 1
polymorphicFilter: (model, itemType)->
App.store.filter model,
(data) =>
if data.get('itemId')
#get('id') is data.get('itemId').toString() and data.get('itemType') is itemType
itemType:()->
#constructor.toString().split('.')[1]
updatePolymorphicRelationships:()->
#incrementProperty('changeCount')
The controller layer is protected from all this jankyness, except for having to call updatePolymorphicRelationship to make sure the bindings fire:
App.HashtagController = Ember.ObjectController.extend
follow:()->
App.Follow.createRecord({
user: #get('currentUserController.content')
itemId: #get('id')
itemType: #get('content').itemType()
})
#this provides a way to bind and update. Could be refactored into a didSave()
#callback on the polymorphic model.
#get('content').updatePolymorphicRelationships()
App.store.commit()
That's what I have so far. I'm trying to keep things in the model layer as it's just one step removed from the adapter layer. If it looks like Ember Data is not going to look at polymorphics at all in future, then it would make sense to pull this all up to a higher level, but for now, this works and leaves my controllers (relatively) clean.
Polymorphic associations are now supported in ember data
https://github.com/emberjs/data/commit/e4f7c3707217c6ccc0453deee9ecb34bd65c28b9