I have following models:
App.Parent = DS.Model.extend({
foo: DS.attr('string'),
children: DS.hasMany('child', {async: true})
});
App.Child = DS.Model.extend({
bar: DS.attr('string')
});
I filled them with some fixture data:
App.ApplicationAdapter = DS.FixtureAdapter.extend();
App.Parent.FIXTURES = [
{
id:0,
foo: 'parent',
children: [0,1]
}
];
App.Child.FIXTURES = [
{
id: 0,
bar: 'child 0'
},
{
id: 1,
bar: 'child 1'
},
{
id: 2,
bar: 'child 2'
}
];
After I do some changes to the children relation, how can I rollback children relation to its latest saved state?
I push new child to manyArray in the following way:
this.store.find('child', 2).then(function(child){
model.get('children').pushObject(child);
})
This does change the relation (I see new child in my view), but parent record does not become dirty. Thus, when I try model.rollback() it does nothing. I also tried solution I found here How to rollback relationship changes in EmberData which is adding model.send('becomeDirty') before rollback, but it does not help.
Maybe I am adding children to my relation in a wrong way?
Thanks!
I used this to rollback related records that are dirty.
App.ParentRoute = Ember.Route.extend
model: ->
#get('store').createRecord('parent', child: #get('store').createRecord('child'))
actions:
willTransition: (transition) ->
rollbackRecords(#)
rollbackRecords = (context) ->
if context.get("controller.content.isDirty")
relationships = Ember.get(App[context.get('controller').resourceName], "relationshipsByName")
content = context.get('controller.content')
relationships.forEach (name, relationship) ->
relatedModel = content.get(name)
relatedModel.rollback() if relatedModel? and relatedModel.get('isDirty')
content.rollback()
true
Here is some code that will rollback a model, as well as it's relationships that I use:
var model = this.get('model');
var relatedModel, relatedModels;
model.constructor.eachRelationship(function (key, relationship) {
if (relationship.kind === 'hasMany') {
relatedModels = model.get(key);
if (relatedModels) {
relatedModels.invoke('rollback'); //since this is an array, need to call the rollback function differently
}
}
else {
relatedModel = model.get(key);
if (relatedModel) {
relatedModel.rollback();
}
}
});
model.rollback();
Hope this helps
I believe the other answers listed here only partially solve this problem. If you add a new related model or remove an existing one, rollback should reverse those as well and I believe the other answers don't address this. Here is a complete solution that provides proper dirty checking and a complete rollback for hasMany and belongsTo relationships:
https://stackoverflow.com/a/27184207/188740
I like to do the good ol' model.reload()
- it'll blow away all changes you haven't synced with your server though.
Related
My question is highly related to "Ember Data: Saving relationships" but I don't care about embedding - I just want a working OneToMany (bi-directional) relationship. Take for example the following models:
App.Child = DS.Model.extend({
name: DS.attr('string'),
toys: DS.hasMany('toy'),
});
App.Toy = DS.Model.extend({
name: DS.attr('string'),
child: DS.belongsTo('child')
});
and the following object creations/saves:
var store = this.get('store');
store.createRecord('child', {name: 'Herbert'}).save().then(child => {
return store.createRecord('toy', {name: 'Kazoo', child: child}).save())
}).then(toy => {
child.get('toys').pushObject(toy);
return child.save();
});
I would expect the child, when serialized, to reference the toy. E.g. something like
{
'name': 'Herbert',
'toys': [ 1 ]
}
But it doesn't. Because this is a "manyToOne" relation ship and ember-data won't serialize these: https://github.com/emberjs/data/blob/v1.0.0-beta.18/packages/ember-data/lib/serializers/json-serializer.js#L656
If you make it a ManyToNone relation by removing the belongsTo it will work but you will lose the back reference.
Why is this special behaviour? Why is ManyToOne that different from ManyToNne or ManyToMany that it deserves such special treatment?
Where is this behaviour documented? I totally missed it and assumed it was a bug in the Serializer / Adapter I'm using.
What is the correct way to achieve my desired serialization?
I ended up creating my own, trivially modified serializer:
import EmberPouch from 'ember-pouch';
export default EmberPouch.Serializer.extend({
serializeHasMany: function(snapshot, json, relationship) {
// make sure hasMany relationships are serialized.
// http://stackoverflow.com/questions/20714858/ember-data-saving-relationships
var key = relationship.key;
if (this._canSerialize(key)) {
var payloadKey;
// if provided, use the mapping provided by `attrs` in
// the serializer
payloadKey = this._getMappedKey(key);
if (payloadKey === key && this.keyForRelationship) {
payloadKey = this.keyForRelationship(key, "hasMany", "serialize");
}
var relationshipType = snapshot.type.determineRelationshipType(relationship);
if (relationshipType === 'manyToNone' || relationshipType === 'manyToMany' || relationshipType === "manyToOne") {
json[payloadKey] = snapshot.hasMany(key, { ids: true });
// TODO support for polymorphic manyToNone and manyToMany relationships
}
}
},
});
the main difference being that it also accepts manyToOne relations,
and use it in application/adapter.js:
export default EmberPouch.Adapter.extend({
defaultSerializer: "pouchserial",
....
In this specific (pouch) case it would probably be better to store the reference to the parent on the child only so the parent doesn't need updates when children are added (which also avoids conflicts).
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'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
I'm trying to fetch the belongsTo ID without fetching the actual record. My JSON API returns the ID for the belongsTo relation.
I have the following models
App.Recipe = DS.Model.extend(
title: DS.attr()
ingredients: DS.hasMany('ingredient', async: true)
)
App.Ingredient = DS.Model.extend(
recipe: DS.belongsTo('recipe')
product: DS.belongsTo('product', async: true)
)
App.Product = DS.Model.extend(
title: DS.attr()
)
This is my route:
App.RecipeIndexRoute = Ember.Route.extend(
model: (args) ->
#store.find('recipe', args.recipe_id)
)
This is my controller:
I'm trying to find the product id within this controller.
App.RecipeIndexController = Ember.ObjectController.extend(
hasRecipeInCart: null
actions:
toggleCart: ->
#content.get('ingredients').then((ingredients) =>
# how do I get product id here, without lookup up the product relation?
# this 'get' makes Ember call: /api/v1/products/1
# I simply want the product ID (in this case 1), without having to call the server again.
ingredient.get('product').then((product) =>
product.id # => 1
)
My JSON looks like this:
HTTP GET: /api/v1/recipes/1
{
"recipe": {
"id":1,
"title":"Recipe 1",
"ingredients":[2,1]
}
}
HTTP GET: /api/v1/ingredients?ids[]=2&ids[]=1
{
"ingredients": [
{
"id":2,
"recipe":1,
"product":1
},
{
"id":1,
"recipe":1,
"product":2
}
]
}
This is now much easier with the ds-reference feature that was added to ember data. You can now do:
var authorId = post.belongsTo("author").id();
See http://emberjs.com/blog/2016/05/03/ember-data-2-5-released.html#toc_code-ds-references-code
A ton of work is going into redoing the relationships, you can pull it out of the underlying properties from the data attribute model.get('data.product.id')
Example: http://emberjs.jsbin.com/OxIDiVU/16/edit
For ED 2.x, relationships have been reworked and were broken for getting underlying data until the ds-references feature landed.
To do this in ED 2.4+, you need to use the new belongsTo method to work with underlying data.
To get an id for a belongsTo ref:
var id = this.get('model').belongsTo('myBelongsToKey').value();
I'm using ember 1.0 and ember-data 1.0.0 beta 1. I have the following routes and controller to create and save simple notes ('AuthenticatedRoute' is just a custom made route for logged-in users):
App.Note = DS.Model.extend({
title: DS.attr(),
author: DS.attr(),
body: DS.attr(),
createdAt: DS.attr()
});
App.NotesRoute = App.AuthenticatedRoute.extend({
model: function() { return this.store.find('note'); },
});
App.NotesNewRoute = App.AuthenticatedRoute.extend({
model: function() {
return this.store.createRecord('note');
}
});
App.NotesNewController = Ember.ObjectController.extend({
actions: {
save: function() {
var self = this, model = this.get('model');
model.set('author', localStorage.username);
model.set('createdAt', new Date());
model.save().then(function() {
self.get('target.router').transitionTo('notes.index');
});
}
}
});
When I save a new note everything works as expected. But when I navigate away from the notes route and then back into it, the notes list is populated with a duplicate entry. One entry has an id and can be edited, deleted etc, the other has all the data of the first entry except the id attribute is null. It seems to me ember-data keeps the newly created record (that hasn't been committed to the database and thus has no id yet) alive even when the record becomes committed but I am uncertain as to why. When I reload the page, the list is correctly displayed, no duplicates appear. What am I doing wrong?
For the record, I am using mongodb so I use a custom serializer to convert '_id' attributes to ember-data friendly 'id's, essentially copied from here:
App.NoteSerializer = DS.RESTSerializer.extend({
normalize: function(type, hash, property) {
// normalize the '_id'
var json = { id: hash._id };
delete hash._id;
// normalize the underscored properties
for (var prop in hash) {
json[prop.camelize()] = hash[prop];
}
// delegate to any type-specific normalizations
return this._super(type, json, property);
}
});
I should also mention that this problem existed in ember-data 0.13 as well.
It was a stupid mistake in my RESTful server. I was responding to POST requests with a 204 (empty) response instead of what ember-data expected, that is a 201 ("created") response with the newly created record as the payload. This post made me realize it.
It would be nice though to include this information in the official REST adapter documentation.
That is certainly strange behaviour indeed. Unfortunately I'm not able to explain why you're experiencing this, however:
You can use the willTransition callback in the actions object in your Route to ensure that when it is transitioned away from, if NotesNewController's content property is dirty (i.e. has not been persisted yet), it will have its transaction rolled back.
App.NotesNewRoute = App.AuthenticatedRoute.extend({
model: function() {
return this.store.createRecord('note');
},
actions: {
willTransition: function (transition) {
var model = this.controllerFor('notesNew').get('content');
if (model.get('isDirty') === true) {
model.get('transaction').rollback();
}
return this._super(transition);
}
}
});