Ember-data: model refresh on DS.Store.createRecord - javascript

Embsters!
I am trying to figure out why my model isn't refreshed after I create a new record and save it to the store.
My route computes the model as follows:
model: function (params) {
var postID = params.post_id,
userID = this.get('session.currentUser.id');
var post = this.store.findRecord('post', postID) ;
var followings = this.store.query('post-following', {
filter: { post: postID }
}) ;
var userFollowing = this.store.queryRecord('post-following', {
filter: { post: postID, user: userID }
}) ;
return new Ember.RSVP.hash({
post: post,
followings: followings,
userFollowing: userFollowing
});
}
My template then renders a list and a button:
{{#each model.followings as |following|}}
...
{{/each}}
{{#if model.userFollowing}}
<button {{action 'follow'}}>Follow</button>
{{else}}
<button {{action 'unFollow'}}>Unfollow</button>
{{/if}}
And my controller creates/deletes the relevant post-following record:
actions: {
follow: function () {
var user = this.get('session.currentUser'),
post = this.get('model.post') ;
this.store.createRecord('post-following', {
user: user,
post: post
}).save();
},
unFollow: function () {
this.get('model.userFollowing').destroyRecord() ;
}
}
When I click the [Follow] button:
a successful POST request is sent
the button is not updated
the list is not updated
When I (refresh the page then) click the [Unfollow] button:
a successful DELETE request is sent
the button is not updated
the list is updated
Do you have any idea of what I'm doing wrong?
Could it be a problem with my payload?
EDIT: Solved!
Well, it sounds like I was expecting too much from ember.
The framework won't automatically update my post-followings array on store.createRecord('post-following', {...}) call.
I then adjusted my controller logic to "manually" update my model:
// in follow action…
userFollowing.save().then( function(){
var followings = store.query('post-following', {
filter: { post: postID }
}).then( function (followings) {
_this.set('model.userFollowing', userFollowing);
_this.set('model.followings', followings);
}) ;
});
// in unFollow action…
userFollowing.destroyRecord().then( function () {
_this.set('model.userFollowing', null);
_this.notifyPropertyChange('model.followings') ;
});
Please note that my backend API design has been criticized by #duizendnegen (see comments). More best practices in this article.
Thanks you for all your help !!!
Brou

For these kind of questions, it really helps to have a smaller, replicated problem (e.g. through Ember Twiddle)
Fundamentally, the new post-following record doesn't match the filter: it is filtered for an attribute { post: 123 } and your post-following object contains something in the lines of { post: { id: 123, name: "" } }. Moreover, your post-following object doesn't contain a property called filter or what it could be - i.e. the query it executes to the server are different than those you want to filter by on the client.
My approach here would be to, as a response to the follow and unfollow actions, update the model, both the userFollowing and followings.

Your issue is that you aren't re-setting the property model to point to the newly created object. You are always accessing the same model property, even after creating a new one.
First thing to be aware of is that, after the model hook in your route, the setupController hook is called that executes:
controller.set('model', resolvedModel)
meaning that the model property on your controller is, by default, set every time the route loads (the model hook resolves). However, this doesn't happen after you create a new record so you must do it explicitly:
let newModel = this.store.createRecord('post-following', {
user: user,
post: post
})
// since model.save() returns a promise
// we wait for a successfull save before
// re-setting the `model` property
newModel.save().then(() => {
this.set('model', newModel);
});
For a more clear design, I would also recommend that you create an alias to the model property that more specifically describes your model or override the default behavior of setupController if you are also doing some initial setup on the controller. So either:
export default Ember.Controller.extend({
// ...
blog: Ember.computed.alias('model') // more descriptive model name
// ...
});
Or:
export default Ember.Route.extend({
// ...
setupController(controller, resolvedModel) {
controller.set('blog', resolvedModel); // more descriptive model name
// do other setup
}
// ...
});

Your model is set when you enter the page. When changes are made, your model doesn't change. The only reason why the list is updated when you destroy the record is because it simply doesn't exist anymore. Reload the model after clicking the follow button or unfollow button, or manually change the values for the list/button.

Related

how to add returned data to the existing template

I am using ember. I intercept one component's button click in controller. The click is to trigger a new report request. When a new report request is made, I want the newly made request to appear on the list of requests that I currently show. How do I make ember refresh the page without obvious flicker?
Here is my sendAction code:
actions: {
sendData: function () {
this.set('showLoading', true);
let data = {
startTime: date.normalizeTimestamp(this.get('startTimestamp')),
endTime: date.normalizeTimestamp(this.get('endTimestamp')),
type: constants.ENTERPRISE.REPORTING_PAYMENT_TYPE
};
api.ajaxPost(`${api.buildV3EnterpriseUrl('reports')}`, data).then(response => {
this.set('showLoading', false);
return response.report;
}).catch(error => {
this.set('showLoading', false);
if (error.status === constants.HTTP_STATUS.GATEWAY_TIMEOUT) {
this.notify.error(this.translate('reports.report_timedout'),
this.translate('reports.report_timedout_desc'));
} else {
this.send('error', error);
}
});
}
There are few think you should consider. Generaly you want to have variable that holds an array which you are render in template in loop. For example: you fetch your initial set of data in route and pass it on as model variable.
// route.js
model() { return []; }
// controller
actions: {
sendData() {
foo().then(payload => {
// important is to use pushObjects method.
// Plain push will work but wont update the template.
this.get('model').pushObjects(payload);
});
}
}
This will automatically update template and add additional items on the list.
Boilerplate for showLoading
You can easily refactor your code and use ember-concurency. Check their docs, afair there is example fitting your usecase.

Content is undefined when trying to work with a record in Ember

I am trying to update a record in the Ember store. When I try to do this, it returns the following error:
Uncaught Error: Assertion Failed: Cannot delegate set('name', test) to the 'content' property of object proxy : its 'content' is undefined.
The controller looks like this:
import Ember from 'ember';
export default Ember.Controller.extend({
model: null,
event: {
name: "test",
id: "adfg8943224xcvsdf"
},
actions: {
editEvent (event) {
var Event = this.store.find('event', event.id);
Event.set('name', event.name);
Event.save()
}
}
});
The route looks like this:
import Ember from 'ember';
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model () {
return {
event: this.store.find('event')
}
},
setupController (controller, model) {
controller.set('model', model);
}
});
The template triggers the action, sending along a object called event, which has properties like name and id. The values of the event object come from the controller and have been set before triggering the editEvent action:
<form {{action 'editEvent' event on="submit"}}>
I believe what is happening is that your model hook is returning a POJO that contains a promise that will resolve. If you want to pass that to your action then you need to do
<form {{action 'editEvent' model.event on="submit"}}>
That being said you should really just return a promise from your model hook so that Ember will wait for your data to load before rendering the template. With the way you have it setup now, if your data takes a long time to load, someone could submit the form before the model is loaded and you'll get an error.
I think you want your route to look like this (no need to override setupController):
import Ember from 'ember';
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model () {
return this.store.find('event');
}
});
Then in your template:
<form {{action 'editEvent' model on="submit"}}>
If you need to load multiple models then you should use Ember.RSVP.hash.
See this answer: EmberJS: How to load multiple models on the same route?
Also, I'm not quite sure what your action is trying to do but you don't need to find the record again. The code you posted for your action doesn't actually do anything. It gets the event and then sets the event's name to its own name.
actions: {
editEvent (event) {
// you already have the event, you passed it in as a parameter
// you don't need to query the store again for it.
var Event = this.store.find('event', event.id);
// This doesn't do anything as it just sets the event.name to itself
Event.set('name', event.name);
Event.save()
}
}
I think you mean to do this:
actions: {
editEvent (event) {
event.set('name', 'updated name');
event.save();
}
}

How to asynchronously load and append data to model on checkbox change of component?

I am currently developing an ember application which has two components.
One component represents a map the other one represents a friendslist.
Both components are placed in the same handlebar template.
What I try to achieve is that a user can check a checkbox in the friendslist component and in the next step his or her posts are loaded asynchronously from facebook (the friend itself was already loaded in the beforeModel hook). Those asynchronously loaded posts should be append to the already existing friend object in the model. Afterwards the map component should be informed about the changes and refresh itself or call a function which will draw points on the map.
At the moment I am trying to set the checked property of a single friend (which would be the same approach as appending the posts but will be easier for now):
// index.hbs
{{map-widget posts=model.posts friends=model.friends}}
{{friends-list checkedFriend='checkedFriend' friends=model.friends}}
// friends-list.hbs (component)
<ul>
{{#each friends as |friend|}}
<li>
{{input type="checkbox" id=friend.facebookID checked=friend.checked change=(action checkedFriend)}} <p>{{friend.name}}</p>
</li>
{{/each}}
</ul>
// friends-list.js (component)
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
checkedFriend: function () {
this.sendAction('checkedFriend');
}
}
});
// index.js (route)
export default Ember.Route.extend(AuthenticatedRouteMixin, {
...
model: function() {
return Ember.RSVP.hash({
posts: this.get('currentUserPosts'),
friends: this.get('friends')
});
},
actions: {
checkedFriend: function () {
// Update just the first friend here to see if the approach works
// Get the friends array from the model
const model = this.controller.get('model');
const friends = model.friends;
// Update the friend
Ember.set(friends[0], 'checked', true);
// Map component receives an update here,
// but "DEPRECATION: You modified (mut model.friends) twice in a single render." warning occurs
this.set('friends', friends);
}
}
})
The current approach works more or less. However, I get a depreciation warning that I modified the model twice in a single render which in my opinion is a sign for a bad design from myside.
What I would like know is how a good approach for my task described above would look like. If I am already on the right way I would be glad if anyone could tell me why this double rendering error appears.
The core problem is how to correctly update the model and how to inform the components especially the component which did not set the action about the changes so that those are refreshed.
Thank you in advance.
You could make a Class - FriendEntry. By calling its constructor you will recieve an instance of FriendEntry. Now you will be modifying instance instead of original record (which indeed is not right).
var FriendEntry = Ember.Object.extend({
init: function() {
this._super(...arguments);
this.set('somethingFriendly', true);
}
});
export default Ember.Controller.extend({
friendsEntries: Ember.computed.map('model.friends', function(friend) {
// Call the constructor
return FriendEntry.create({
friend: friend,
checked: false,
posts: []
})
})
});
Ok so your component would look something like this.
{{friends-list checkedFriend='changeFriendCheckedStatus' entries=friendEntries}}
// friends-list.hbs (component)
<ul>
{{#each entries as |entry|}}
{{input type="checkbox" checked=entry.friend.checked change=(action checkedFriend entry)}} <p>{{entry.friend.name}}</p>
{{/each}}
</ul>
// friends-list.js (component)
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
checkedFriend: function (entry) {
this.sendAction('checkedFriend', entry);
}
}
});
Back to controller
actions: {
changeFriendCheckedStatus(friendEntry) {
ic.ajax.request(API.HOST + '/someUrlForPosts/' + friendEntry.get('id)).then(givenFriendPosts => {
entry.get('posts').pushObjects(givenFriendPosts);
})
}
}
If i understood correctly you have 2 models I friends and posts (DS.belongsTo('friend')). You would need to encapsulate both into friendEntry (friend, posts).
So your map-widget would also look like this {{map-widget friendEntries=friendEntries}}
Instead of querying posts in model you could encapsulate them like this.
friendsEntries: function() {
return DS.PromiseArray.create({
promise: Ember.RSVP.all(this.get('model.friends')).then(friends => {
return friends.map(friend => {
return FriendEntry.create({
friend: friend,
checked: false,
posts: store.query('posts', { friend: friend.get('id') }
});
});
})
});
}.property('model.friends.[]')

What is the proper way to update hierarchical model on changing?

I have the following dialog model:
var DialogModel = Backbone.Model.extend({
user: null,
constructor: function () {
// Initialize user model
this.user = new BW.user.UserModel();
Backbone.Model.apply(this, arguments);
},
parse: function (attributes) {
_.isObject(attributes) || (attributes = {});
// update user's model data
this.user.set(attributes.user);
delete attributes.user;
return attributes;
},
toJSON: fucntion () {
var json = Backbone.Model.prototype.toJSON.call(this);
// serialize user's model data to JSON
_.extend(json, user: this.model.toJSON());
}
});
As you can see from the code above, I store user model into DialogModel and set data on parse and serialize in toJSON.
As some point in time I get dialog:update socket message with the following data:
{
id: 1,
message: 'message',
user: {
<JSON USER DATA>
}
}
To update dialog based on this message data I do the following:
eventBus.on('dialog:update', function (json) {
dialogModel.set(json);
});
But the problem is that user model don't get updated becase parse method does't execute.
So my question is how could I update user model on set method?
One option is to override model.set. Adding something like the following to your DialogModel will do the trick:
set: function (attributes, options) {
this.parse(attributes);
Backbone.Model.prototype.set.apply(this, arguments);
},
Note that I've chosen to call the parse method, since it nicely sets the user data and deletes it from the attributes for us (thus preventing that data from being set on dialogModel).
Click here for a fiddle demonstrating this solution.

live update view in ember

I build application using web-socket and I want dynamically change view
I have code in my model:
model: function () {
var store = this.store;
var socket = new_socket();
socket.on('message', function (message) {
store.push('record',message);
});
return store.find('consultation');
}
And data successfully loading into ember store, but ember doesn't update view.
What I need to do?
For example when I use save all works fine:
var record = this.store.createRecord('record',{});
record.save()
UPDATE:
I found this solution:, but it's also not working
My controller (consultation.js):
export default Ember.ObjectController.extend({
needs: ['application'],
records: function() {
console.log(this.get('store'));
return this.get('store').filter('record', function(record) {
console.log(record);
});
}.property('records'),
Template:
{{#each item in controller.records}}
{{/each}}
Model consultation looks like this (with hasMany field):
export default DS.Model.extend({
records: DS.hasMany('record', { async: true }),
});
Second console.log is not printing,
I solved it like this
var obj = store.push('record', jsonObj.message);
this.controllerFor('consultation').get('records').pushObject(obj);
But it's looks like not good. In which way I can make auto synchronization between model and controller
You have computed properties that are dependent on themselves in your controller. Also, you really should not be pushing anything to the store, conceptually, when you receive something on your socket, your model changed, you want to capture that notion in your code, so you really should be updating the model when you get a socket message. The way I would do this would be to do something like the following:
in the router,
model: function() {
var records = this.store.find('consultation',{});
var socket = new_socket();
socket.on('message', function (message) {
records.pushObject(message);
});
return records;
}
The controller does not need to have any computed properties at all.
and in the template, the code would just be:
{{#each content}}
//do something here
{{/each}}
I'm a little confused about your use of consultation and record in the code.

Categories

Resources