Duplicate null-id records in ember-data - ember.js

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);
}
}
});

Related

Why does my Ember computed property on hasMany relationship not update?

I have an Ember Data model as:
export default DS.Model.extend({
name: DS.attr('string'),
friends: DS.hasMany('friend',{async:true}),
requestedFriendIds: Ember.computed('friends',function(){
return this.get('friends').then(function(friends){
return friends.filterBy('status','Requested').mapBy('id');
});
})
});
I have a route setup that uses it:
export default Ember.Route.extend({
model: function(params){
return Ember.RSVP.hash({
memberProfile:this.store.find('member-profile', params.memberprofile_id).then(function(memberProfile)
{
return Ember.RSVP.hash({
requestedFriendIds:memberProfile.get('requestedFriendIds'),
UserId:memberProfile.get('user.id'),
Id:memberProfile.get('id')
});
}),
});
}
});
},
And htmlbars that utilize the route model. My computed property is always correctly called on a reload, but isn't refreshed on a user action 'Add Friend', which changes the store by adding a friend and the profile.friends' record like this:
actions:
{
addFriend:function(profile_id,)
{
this.store.findRecord('member-profile',memberProfile).then(function(member){
var friend = this.store.createRecord('friend',
{
member:member,
status:'Requested',
timestamp: new Date(),
});
friend.save();
member.get('friends').pushObject(friend);
member.save();
}.bind(this));
}
}
Some notes: I've tried the computed property on 'friends','friends.[]'. My code base is Ember 2.0.1, with Ember.Data 1.13.12, and as such 'friends.#each' is deprecated. The underlying data is correctly updated in the backing store (EmberFire). I've debugged into EmberData and I see that the property changed notifications invalidation code is called. This is only a selection of the code...
What am I missing...? Is there a better way to approach this?
I think you should watch friends.[] instead of only friends:
requestedFriendIds: Ember.computed('friends.[]',function(){
return this.get('friends').then(function(friends){
return friends.filterBy('status','Requested').mapBy('id');
});
})
And you could probably put your action in your route and manually refresh model (it might be issue with promise result not binding to changes in CP). So, in your route:
actions: {
addFriend(profile_id) {
this.store.findRecord('member-profile', memberProfile).then(member => {
let friend = this.store.createRecord('friend',
{
member:member,
status:'Requested',
timestamp: new Date()
});
friend.save();
member.get('friends').pushObject(friend);
member.save();
this.refresh();
});
}
}
The most important part is using this.refresh() in Ember.Route.

Best Practice for Creating New Record with belongsTo Relationship

I am wondering about the best practice for creating a new record in Ember with createRecord() and then persisting it to the API? Specifically, should Ember's POST request generally be a single JSON that embeds all the model's relationships, or is it customary to POST each relationship individually?
In my code, I'm not able to get a single JSON, so I'm wondering if I'm missing the "Ember Way" or (more likely) I have a mistake in my code?
DETAILS:
Here are the details of my setup. I have two models:
/models/OrgUser.js:
DS.Model.extend({
...
orgPerson: DS.belongsTo('org-person', { inverse: 'org-user', async: true, embedded: 'always' }),
});
/models/OrgPerson.js:
DS.Model.extend({
...
orgUser: DS.belongsTo('org-user'),
})
I'm attempting to create a new user on the "Create New User" page. The route for that page is below. Is this the best place to call createRecord() for my new models?
/routes/org-users/add.js:
Ember.Route.extend({
model: function() {
var orgPerson = this.store.createRecord('org-person');
var orgUser = this.store.createRecord('org-user' );
orgUser.set('orgPerson', orgPerson);
return orgUser;
},
...
}
Using Chrome console to look at the orgUser object after I call set shows no evidence at all that I have added anything to orgUser. The "Ember" tab of Chrome Debug Tools does reflect the relationship, though.
On my "Create New User" page, my input fields all correspond to both OrgUser properties and OrgUser.OrgPerson properties. Here's an example:
/templates/org-users/add.hbs
...
{{input value=username}} // a property of OrgUser
{{input value=orgPerson.firstName}} // a property of OrgUser.orgPerson
...
In my route, when I go to save() Ember Data POSTs only the orgUser JSON with a null value for orgPerson. I'd like it to embed the orgPerson serialized object in the orgPerson property.
/routes/org-users/add.js:
Ember.Route.extend({
...
actions: {
submitForm: function() {
...
this.currentModel.save().then( onSuccess ).catch( onFailure );
...
}
}
});
This results in a POST request with the following body:
{
"orgUser":{
"username":"MyUsername",
"orgPerson":null,
"id":null
}
Note that orgPerson is null. Thanks in advance for any assistance!
UPDATE: Once again, I think I will need to take a fresh look at my serializer. Here's how it's currently defined.
/serializers/application.js:
DS.RESTSerializer.extend({
// Use the default approach to serializing, but add the id property
serialize: function(record, options) {
var json = this._super.apply(this, arguments);
json.id = record.id;
return json;
},
serializeBelongsTo: function(record, json, relationship) {
var key = relationship.key;
key = this.keyForRelationship ? this.keyForRelationship(key, 'belongsTo') : key;
var data = record.get('data');
if (relationship.options.embedded && relationship.options.embedded === 'always') {
json[key] = data[relationship.key] ? data[relationship.key].serialize( { includeId: true } ) : null;
}
else {
json[key] = data[relationship.key] ? data[relationship.key].get('id') : null;
}
if (relationship.options.polymorphic) {
this.serializePolymorphicType(record, json, relationship);
}
}
});
Per #Kingpin2k's comment, there appears to be some ambiguity (and bugs!) on how best to handle serialize() for a belongsTo relationship. My serializer customization above works great for records that are obtained through this.store.find(), but now I need to enable them for createRecord(). Additional suggestions, pointers are welcome!
It's a bug. https://github.com/emberjs/data/issues/1542#issuecomment-49443496
A workaround is to get the async belongsTo record before attempting to save (It tricks Ember Data into initializing it). In your case you could do it in the model hook.
model: function() {
var orgPerson = this.store.createRecord('org-person');
var orgUser = this.store.createRecord('org-user');
orgUser.set('orgPerson', orgPerson);
return orgUser.get('orgPerson').then(function(){
return orgUser;
});
},
So, I finally figured this out. With the release of Ember-Data-1.0.0-Beta.9, http://emberjs.com/blog/2014/08/18/ember-data-1-0-beta-9-released.html, the EmbeddedRecordsMixin has been introduced. This pretty much solves all my issues!
So, I wound up doing the following:
Upgraded to Ember-Data-1.0.0-Beta.9
Deleted my serializeBelongsTo customization from my serializer
I now define a custom serializer for each model using the EmbeddedRecordsMixin as documented at http://emberjs.com/api/data/classes/DS.EmbeddedRecordsMixin.html.
This wound up working perfectly, because I get full declarative control over how and when my records are embedded.
Special thanks to #Kingpin2k for helping me realize my serializer was the problem and for the discussion to help me understand the options.

Adding Model to a hasmany relationship wont unpdate

I am trying to add model to a hasmany relationship. In my route I have:
App.CourseRoute = Ember.Route.extend({
model: function(){
return this.store.find('course','-JNPcfHFQC8FNwyt_-Wh');
}
});
and in my controller I have a save action:
App.CourseController=Ember.ObjectController.extend({
actions: {
save: function() {
var _this = this;
var course = this.get('model');
_this.get('session').get('user').get("courses").pushObject(course)
_this.get('session').get('user').save()
}
}
});
The session property refers a global sessions object that is injected into the controller. This project is using firebase and emberFire data adapter. No errors are being thrown and no data is updated on the firebase backend. I can't seem to figure out what is going on.
Breakpoint your code before the save call and you'll probably see that this.get('session').get('user') has an isDirty property of false. So the save becomes a no-op. You can try to solve that by callingthis.get('session').get('user').notifyPropertyChange('courses').

How to Add Child Record to Existing Parent Record?

I've been googling and scouring Stack Overflow for some sort of hint on this subject but the information is scattered at best.
I'm trying to Create a new Child Record (Comment) and save it to an existing Parent Record (Post). I am using Ember-Model, rather than Ember-Data, but any tips or pointers would be greatly appreciated.
At the moment, I've been successful creating a new, embedded Comment but only when it is created with a new Post record. So:
How do I go about loading/retrieving the currently loaded Post(parent record) in order to apply Comments (child records) to it?
I've been reading up on controller dependencies, using needs: and this.controllerFor and this.modelFor in order to have access to another controller/model's content but have been unable to wire these things together into something meaningful.
Anyway, here is what I've whittled my application code down to, in the hopes I might be able to stumble into the proper way of doing this...
Routes
App.Router.map(function() {
this.resource('post', { path: '/:post_id' }, function() {
this.resource('comments', { path: '/comments'} );
});
});
I removed all the other resources & routes, so I'm left with App.Post, App.PostIndex, and App.Comments. I think my routes are the issue here, I assume I'm not properly implementing the methods to use the loaded Post record in my Comments route.
App.IndexRoute = Ember.Route.extend({
model: function() {
return App.Post.find();
},
setupController: function(controller, model) { // I'm not certain if this
controller.set('content', model); // setupController is needed?
}
});
App.PostRoute = Ember.Route.extend({
model: function(params) {
return App.Post.find(params.post_id);
},
setupcontroller: function( controller, model) { // again, unsure if this
this.controllerFor('post').get('comments'); // is correct.
controller.set('content', comments);
}
});
App.CommentsRoute = Ember.Route.extend({
afterModel: function() {
this.set('post', this.modelFor('post'));
},
setupcontroller: function( controller, model) {
this.controllerFor('post').get('comments');
controller.set('content', comments);
}
});
Controller
App.CommentsController = Ember.ArrayController.extend({
needs: "post",
actions: {
addComment: function() {
var post = App.Post.create({
title: 'static post title'
});
post.get('comments').create({
message: 'static message'
});
post.save();
}
}
});
This is my current Comments Controller, which can create a new Post with an embedded Comment. I've found and been given numerous examples in which to create the Comment, but none seem to work for me. Basically, I'm struggling with defining the var post = ... as the currently loaded record. I've implemented various approaches in an attempt at trial & error. Thus far I have attempted:
var post = App.Post.create();, returns property undefined, as this would create a new record. However, I gave it a shot as every example i saw related to this defined their record as such.
var post = this.get('post');, returns a cannot call 'get' on undefined. I've tried using this method of defining my current post on both the Comments controller and Post controller.
var post = this.get('controllers.post.content);, returns a 'cyclic error' from the backend I'm using.
var post = App.Post.find();, returns a cannot call 'get' on undefined.
var post = App.Post.find(1);, Again, returns a cannot call 'get' on undefined. Figured I'd give it a shot because this is one of those recurring examples people provide. The backend I use applies its own ID to each record, and I'm unsure if I would be able to/how to have the .find() method use a dynamic ID value and retrieve only the model I just loaded.
I'm guessing that I'm not properly setting up my Routes and Controller dependencies?
If anyone has a suggestion, relevant link, or fix I would be very grateful.
This one (seemingly simple) issue/use case has me at wit's end at this point.
Try this (works pre beta 2):
App.CommentsController = Ember.ArrayController.extend({
actions: {
addComment: function() {
this.content.createRecord({
message: 'static message'
});
}
}
});
Ember Data Beta 2 and later:
App.CommentsController = Ember.ArrayController.extend({
needs: ["post"],
actions: {
addComment: function() {
var post = this.get('controllers.post');
var comment = this.get('store').createRecord('comment', {
message: 'static message',
post: post
});
comment.save().then(function() {
post.addObject(comment);
// You may or may not need to save your post, too. In my case my backend handles
// the inverses of relationships (if existing), so there's no need. We still need
// to do this for Ember, though
});
}
}
});

Ember not requesting models by id after first request

SO,
I am working on an Ember app and experiencing a confusing problem. At the index route the app performs a find() and returns an array of dataset and links to a template to show further details about each dataset which are sideloaded when a resquest is made to find by id. (i.e. find(1), where 1 is the id.)
The first request with an id works fine, returning the dataset object and it's sideloaded data, however subsequent requests do not seem to do anything. The server does not see any request if I try to navigate to any other dataset after the first one's details have been loaded. However if I navigate from a specific dataset back to index and then back to any dataset it will send the request again (twice even, am not sure if this a related problem) and work. In other words:
/# works
/#/1 also works (or any other id as long as it is the first one visited)
/#/1 then /#/2 does not work, no request is sent
/#/1 followed by /# then /#/2 does work, maintaining the data at /#/1 & getting the new data for /#/2.
How do I get all of the specific dataset objects to return upon visiting them, without the hacky pitstop at index? Any advice would be greatly appreciated, thank you in advance!
The code:
-app.js
/**************************
* Application
**************************/
var App = Em.Application.create();
App.Router.map(function() {
this.resource('application', {path:'/'}, function() {
this.resource('dataset', {path: '/:dataset_id'}, function() {
});
});
});
App.ApplicationRoute = Em.Route.extend({
model: function() {
return App.Dataset.find();
}
});
App.DatasetRoute = Em.Route.extend({
activate: function() {
this.modelFor('dataset').reload();
}
});
/**************************
* Models
**************************/
App.Store = DS.Store.extend({
adapter: DS.RESTAdapter.create({
url: 'http://***.***.***.***:5000',
namespace: 'api',
serializer: DS.RESTSerializer.extend({
primaryKey: function(type) {
return '_id';
}
})
})
});
App.Dataset = DS.Model.extend({
dataset: DS.attr('string'),
title: DS.attr('string'),
points: DS.hasMany('App.Point')
});
App.Point = DS.Model.extend({
dataset: DS.attr('string'),
dataset_id: DS.attr('string'),
date: DS.attr('date'),
value: DS.attr('string')
});
A route's activate hook is only called when the route is first transitioned to. It is not called again if the route's model changes. So when you transition into App.DatasetRoute either by entering the url directly or by clicking link on index page, the activate hook runs and your dataset is reloaded. When you switch from #/1 to #/2, the route remains active and no hook is called.
If I am understanding your question correctly, you want to reload the dataset whenever a user visits its url. In that case instead of the route's activate hook what you probably want to do is observe changes to the dataset controller's content. Something like this should work:
App.DatasetController = Ember.ObjectController.extend({
refreshOnChange: function() {
var dataset = this.get('content');
if (dataset) {
console.log('reloading dataset ', dataset.toString());
dataset.reload();
}
}.observes('content')
}