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

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.

Related

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

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.

What's a proper override for all controls to load saved state and store data

I'm saving control state and store data to a database (only grids for the moment) and I'm wondering what listener or method I could override to put the data back in place when the controls are being rendered/shown to the user.
Here's how I save the state and store data:
SaveControlState: function (controlItemId, controlType, active, successFunction, scope) {
//Save the control state and its data to server...
var controlState = {};
var control = this.getCmp(controlType + '[itemId=' + controlItemId + ']');
controlState.ControlItemId = control.itemId;
controlState.ControlState = Ext.JSON.encode(control.getState()); //save a JSON string to database...
//if the control has this attribute, it means it wants to update its control state/data...
if (typeof control.controlStateId != 'undefined') controlState.ID = control.controlStateId;
controlState.Active = active;
//if control has a store, let's send data over server...
if (control.getStore() != null) {
controlState.ControlStoreData = [];
Ext.each(control.getStore().data.items, function (record) {
controlState.ControlStoreData.push(record.data);
});
controlState.ControlStoreData = Ext.JSON.encode(controlState.ControlStoreData); //same here...
}
control.setLoading({ msg: Strings.Messages.str_Wait });
//initiate request...
Ext.Ajax.request(
{
url: GlobalVars.Urls.portalControlStateCreateApiUrl,
params: { jsonData: Ext.JSON.encode(controlState), action: 'createcontrolstate' },
method: 'POST',
success: function (result, request) {
//hide spinner...
this.getCmp(controlType + '[itemId=' + controlItemId + ']').setLoading(false);
//if we had a success handler provided, call it...
if (typeof successFunction != 'undefined') successFunction(scope);
},
failure: function (result, request) {
var control = this.getCmp(controlType + '[itemId=' + controlItemId + ']');
control.setLoading(false);
Ext.Msg.show({
title: Strings.UI.str_Error,
msg: Strings.format(Strings.UI.Messages.str_ErrorControlState, control.id, result.responseText),
buttons: Ext.MessageBox.OK,
icon: Ext.MessageBox.ERROR
});
},
scope: this
});
}
And I'm retrieving all the active control state entries when a user logs in:
Ext.create('App.store.ControlState', {
autoLoad: true,
scope: this,
storeId: 'controlStateStore',
proxy: {
type: 'ajax',
extraParams: { activeOnly: true },
url: GlobalVars.Urls.portalControlStateGetAllApiUrl,
headers: { 'Content-type': 'application/json' },
reader: { type: 'json', root: 'ControlStates' }
},
listeners: { load: function (store, records, success, opts) {
//one this is done, I show the rest of the UI....
}
}
});
Now what I need is an override that allows me to peer into the store above and find a record, if I have a match (using the control's itemId attribute) then apply the state and load the data in the store, if the control that's being overriden indeed has a store.
Any ideas what I could use ? Thanks.
Controls implementing Stateful already can use the functions applyState() and getState() exactly for the purpose of saving/restoring state. So these functions are called whenever a control state has to be changed/applied. I think that if you reuse Stateful, ExtJS will handle everything else for you (reloading stores if grid's filters have changed etc.)
But I don't think that you can do a single override to make states work in EVERY component, since each component type would need a special list of what's part of a state. Such a list is not provided by ExtJS; not even for their standard components.
So I fear that you would have to do one override per component type.
If I were you, I would make a store with ID controlStateStore and a model of three and a half fields:
itemId // type string
propertyName // type string
propertyValue // no type(!)
id // auto-created using `convert` to concatenate itemId, a comma and propertyName.
This store would get two added-value functions to load/store a Stateful state object into the model:
getState:function(itemId) {
var records = store.Query("itemId",itemId);
var state = {};
records.each(function rec() {
state[rec.get("propertyName")]=rec.get("propertyValue");
})
return state;
}
setState:function(itemId,state) {
// add one record to the store per property
for(k in state) {
store.add({itemId:itemId,propertyName:k,propertyValue:state[k]});
}
// idProperty id will take care of replacing duplicates
}
These are the functions on the store. Now, on each component type, you would need an override that implements Stateful. So let's have a look at the "recommended" getState/applyState functions, which may look similar to this:
xtype:button,
stateful:true,
stateid:'bla',
getState:function() {
// required if you want to save state
// by default, getState is empty function, so nothing is saved
return {pressed:this.pressed}; // here, a bigger object would be returned
// depending on the component type
},
applyState: function(state) {
// not required, since this function already exists and is the same for alle component types.
if (state) {
Ext.apply(this, state);
}
},
Here, I would replace them both with sth. like:
getState:function() {
if(this.itemId)
Ext.getStore("controlStateStore").setState(this.itemId,{pressed:this.pressed})
},
applyState: function() {
if(this.itemId) {
var state = Ext.getStore("controlStateStore").getState(this.itemId);
Ext.apply(this, state);
}
},
and either add a store.sync() to both, or, the better way, use autoSync on the store.
Although I would opt to keep it compatible by using stateId, not itemId. Since you can reuse ItemIds, but you don't want components take the state of a different component (with same itemId), you should consider to use id or stateId, not itemId.
Disclaimer: Didn't test any of this code, so let's hope it's not too far off the shot...

Ember sending data from view causes error

I sent action from view to currents route controller, then to another controller, in order to write code once.
this.get('controller.controllers.study/study').send('processPersonData', data);
**DEPRECATION: Action handlers implemented directly on controllers are deprecated in favor of action handlers on an actions object ( action: processPersonData on )
at Ember.ControllerMixin.Ember.Mixin.create.deprecatedSend
What is the right way to implement this send action?
FYI: the send action works correctly.
This message indicates that the method handling the action should be under an 'actions' hash on the object, like so:
App.SomeController = Ember.ObjectController.extend({
someVariable: null,
actions: {
processPersonData: function(context) {
//implementation
},
otherAction: function(context) {
//implementation
}
}
});
It is just new semantics for action handling.
If you are trying to call an action in your controller from your view, you should use the Em.ViewTargetActionSupport mixin as follows:
App.DashboardView = Em.View.extend(
Ember.ViewTargetActionSupport, { // Mixin here
functionToTriggerAction: function() {
var data = this.get('data'); // Or wherever/whatever the data object is
this.triggerAction({
action: 'processPersonData',
actionContext: data,
target: this.get('controller') // Or wherever the action is
});
},
});
App.DashboardController = Em.ObjectController.extend(
// The actions go in a hash, as buuda mentioned
actions:
processPersonData: function(data) {
// The logic you want to do on the data object goes here
},
},
});

What is the best way to add server variables (PHP) in to the Backbone.model using require.js?

I'm not sure what is the elegant way to pass server variables in to my Model.
For example, i have an id of user that has to be implemented on my Model. But seems like Backbone with require are not able to do that.
My two options are:
Get a json file with Ajax.
Add the variable on my index.php as a global.
Someone know if exists a other way. Native on the clases?
Trying to make work the example of backbonetutorials. I am not able to throw a callback when the method fetch().
$(document).ready(function() {
var Timer = Backbone.Model.extend({
urlRoot : 'timeserver/',
defaults: {
name: '',
email: ''
}
});
var timer = new Timer({id:1});
timer.fetch({
success: function(data) {
alert('success')
},
fail: function(model, response) {
alert('fail');
},
sync: function(data) {
alert('sync')
}
});
});
The ajax request it has been threw. But does not work at all. Because any alert its dispatched.
var UserModel = Backbone.Model.extend({
urlRoot: '/user',
defaults: {
name: '',
email: ''
}
});
// Here we have set the `id` of the model
var user = new Usermodel({id: 1});
// The fetch below will perform GET /user/1
// The server should return the id, name and email from the database
user.fetch({
success: function (user) {
console.log(user);
}
})
The server will reply with a json object then you can leave the rendering part for your backbone. Based on a template for the user.
You may also want to check these out: http://backbonetutorials.com/

Backbone.js and local storage . A "url" property or function must be specified

I'm improving my knowledge about Backbone.js and have this code sample taken from a tutorial. (http://bardevblog.wordpress.com/2012/01/16/understanding-backbone-js-simple-example/)
This example will not access the server for now, so to simulate the retrieval of data from the server I have a file name movies.json.
What I am trying to do:
Add json data in local storage (using localStorage adapter)
For this I am using Backbone.ajaxSync, Which Is Given to the alias Backbone.sync by the localStorage adapter: I created the method refreshFromServer () to do this
The reason for doing this is that I'm trying to implement a way to get data only one time (and only refresh when i need to)
My issues:
  I'm having an error "Uncaught Error: 'url' property or function must be specified" when I call refreshFromServer ().
I do not understand why because I set the url collection. (url : "scripts/data/movies.json" )
Sample code
var Theater = {
Models : {},
Collections : {},
Views : {},
Templates : {}
}
Theater.Models.Movie = Backbone.Model.extend({})
Theater.Collections.Movies = Backbone.Collection.extend({
model : Theater.Models.Movie,
localStorage : new Backbone.LocalStorage("MovieStore"), // Unique name within your app.
url : "scripts/data/movies.json",
refreshFromServer : function() {
return Backbone.ajaxSync.apply(this, arguments);
},
initialize : function() {
console.log("Movies initialize")
}
});
Theater.Templates.movies = _.template($("#tmplt-Movies").html())
Theater.Views.Movies = Backbone.View.extend({
el : $("#mainContainer"),
template : Theater.Templates.movies,
initialize : function() {
this.collection.bind("reset", this.render, this);
},
render : function() {
console.log("render")
console.log(this.collection.length);
}
})
Theater.Router = Backbone.Router.extend({
routes : {
"" : "defaultRoute"
},
defaultRoute : function() {
console.log("defaultRoute");
Theater.movies = new Theater.Collections.Movies()
new Theater.Views.Movies({
collection : Theater.movies
});
Theater.movies.refreshFromServer();
//Theater.movies.fetch();
console.log(Theater.movies.length)
}
})
var appRouter = new Theater.Router();
Backbone.history.start();
Notes:
If a comment localStorage property in the collection
Theater.Models.Movie = Backbone.Model.extend({})
Theater.Collections.Movies = Backbone.Collection.extend({
model : Theater.Models.Movie,
//localStorage : new Backbone.LocalStorage("MovieStore")
...
});
and then in router call normal fetch method
Theater.Router = Backbone.Router.extend({
routes : {
"" : "defaultRoute"
},
defaultRoute : function() {
Theater.movies = new Theater.Collections.Movies()
new Theater.Views.Movies({
collection : Theater.movies
});
//Theater.movies.refreshFromServer();
Theater.movies.fetch();
}
})
I can see the json list correctly in my view
If I use the localStorage property in the collection and then call the standard fetch () method, I see only an empty list (I think it is normal as it is read from the local storage and is empty)
The error only occurs when using the method refreshFromServer () witch use Backbone.ajaxSync (alias for backbone.sync)
Err... my bad. The refreshFromServer implementation is from my answer to your earlier question., and it's completely, uselessly wrong.
Backbone.sync expects arguments (method, model, options), but as it stands, it doesn't get what it needs from refreshFromServer because the refresh method simply sends forward whatever arguments it gets. Sorry for the mistake.
The correct, working implementation would be:
refreshFromServer : function(options) {
return Backbone.ajaxSync('read', this, options);
}
It can be used either via success / error callbacks passed to the options hash:
this.collection.refreshFromServer({ success: function() { /* refreshed... */ });
Or via the jqXHR Promise API:
this.collection.refreshFromServer().done(function() { /* refreshed... */ })
Or not signing up for callbacks and waiting for the collection reset event like in your example:
this.collection.bind("reset", this.render, this);
this.collection.refreshFromServer();
This should work. Please let me know if it doesn't. I fixed my answer in the previous question too, in case someone stumbles onto it.
Edit: To save the data to local storage after refreshing you need to manually save each of the models:
var collection = this.collection;
collection.refreshFromServer({success: function(freshData) {
collection.reset(freshData);
collection.each(function(model) {
model.save();
});
}});

Categories

Resources