Can I save changed attributes of model after several set - backbone.js

I call set method multiple times and change several attributes. Then I want to send the changed data to the server with {patch: true}.
I can use model.save(attrs, {patch: true});, but I do not know attrs. I can't use model.toJSON() (unneeded fields) or model.changedAttributes() (only last set) to obtain attrs.
How can I do this?

According to changedAttributes:
Optionally, an external attributes hash can be passed in, returning the attributes in that hash which differ from the model.
So you could try caching the state of model using toJSON before you start modifying. Once your modifications are done, pass the new state to changedAttributes method to retrieve changed attributes hash and then send a patch request. Something like
var oldAttrs = model.toJSON();
// ...do modifications here
var changedAttrs = model.changedAttributes(oldAttrs);
dataTosend = model.pick(_.keys(changedAttrs));
model.save(dataTosend, {patch: true});

Bind a listener for model
If you are setting value in view, listener should be like (better write it in initialize function)
this.listenTo(this.model, "change", this.onModelValueChange);
And your listener function
onModelValueChange: function(model, args) {
model.save(args.changed, {patch: true});
}

While TJ has the right answer, I have a better way to achieve what he suggest.
Instead of making a raw clone of the attributes, I prefer to keep a master copy of the model at the start of the app or the initialize of the view.
this.master = this.model.clone();
// ... then changes are made to this.model
When ready to save, use the master model to compare the attributes and retrieve the changes directly.
var changes = this.master.changedAttributes(this.model.attributes);
if (changes !== false) this.model.save(changes, { patch: true });
With this, you can skip the dataTosend = model.pick(_.keys(changedAttrs)) altogether since changes already is an object of all the differences with the initial state of the master model.
If it's a view that is re-used after the model save:
var View = Backbone.View.extend({
initialize: function() {
this.updateMaster();
},
saveChanges: function() {
var changes = this.master.changedAttributes(this.model.attributes);
if (changes !== false) {
this.model.save(changes, {
patch: true,
context: true,
success: this.updateMaster
});
}
},
updateMaster: function() {
this.master = this.model.clone();
},
});

Related

Backbone - user case

Say a user is going down a page and checking off and selecting items.
I have a Backbone model object, and each time the user selects something I want to update the object.
I have this in a separate JavaScript file that I source in my HTML:
var app = {};
var newLineup = null;
var team = document.getElementsByName('team');
app.Lineup = Backbone.Model.extend({
defaults: {
team: team,
completed: false
},
idAttribute: "ID",
initialize: function () {
console.log('Book has been intialized');
this.on("invalid", function (model, error) {
console.log("Houston, we have a problem: " + error)
});
},
constructor: function (attributes, options) {
console.log('document',document);
console.log('Book\'s constructor had been called');
Backbone.Model.apply(this, arguments);
},
validate: function (attr) {
if (attr.ID <= 0) {
return "Invalid value for ID supplied."
}
},
urlRoot: 'http://localhost:3000/api/lineups'
});
function createNewLineupInDatabase(){
newLineup = new app.Lineup({team: team, completed: false});
newLineup.save({}, {
success: function (model, respose, options) {
},
error: function (model, xhr, options) {
}
});
}
When the user first accesses the page, I will create a new lineup object by calling the above function. But how do I update that object as the user interacts with the page? Is there a better way to do this other than putting the Backbone model object at the top of my JavaScript file?
The Backbone pattern was designed to answer your question. As other respondents said, wire up a View, which takes your model as a parameter and lets you bind DOM events to the model.
That said, you don't have to use the rest of the framework. I guess you can use all the functionality Backbone provides models by handling the model yourself.
You need to worry about a couple of things.
Give you model a little encapsulation.
Set up a listener (or listeners) for your checkbox items.
Scope the model to your app
Backbone provides neat encapsulation for your model inside a View, but if you can live with it, just use your app variable which is within scope of the JavaScript file you posted.
When you're ready to instantiate your model, make it a property of app:
app.newLineup = new app.Lineup({team: team, completed: false});
It may look weird to have the instance and the constructor in the same object, but there aren't other options until you pull out the rest of Backbone.
The listener
So you have N number of checkboxes you care about. Say you give them a class, say, .options. Your listener will look like
$( ".options" ).change(function() {
if(this.checked) {
//Do stuff with your model
//You can access it from app.newLineup
} else {
}
});
Voila! Now your page is ready to talk to your model.
If there is frontend ui / any user interaction within your code it is extremely useful to create a backbone view which makes use of an events object where you can set up your event handler.
You can also link a view to a model to allow your model / your object to be updated without scope issues.

Is there an equivalent to parse to use before syncing with the server?

In my backbone model, I parse the response from the server:
var MyModel = Backbone.Model.extend({
urlRoot: "/users",
parse: function(response){
var data = {};
data.id = reponse.userDetails.id;
data.name = response.userDetails.firstname + " " + response.userDetails.lastname;
data.description = response.userDetails.description;
return data;
}
});
var myModel = new MyModel({id: 1});
myModel.fetch();
The views that use this model can manipulate it, for example, if the user were to click on the view to "select" it, it would update the model...
myModel.set({selected: true});
...and the view would re-render based on the model's change event and highlight the "selected" user.
When it comes time to save the model to the server, how do I only send the attributes the server wants? and ignore the attributes which were added through user interaction.
OR
Should the data model always reflect what the server returns? If so, is there a better way to store the user interactions (whether the view is "selected")? Should it be a separate model than the actual data model?
Thanks
The model doesn't need to mirror the data on the server if that doesn't make sense for your application.
For the model's attributes, if you don't need to render those attributes in a template, then you can just override model.toJSON() to only serialize the attributes you want sent to the server. Be careful though, in this case if you are rendering your template (or anything else) using this.model.toJSON() then it will also be affected. If that's a problem then you can override model.sync() instead and manipulate the data passed in before sending it to Backbone.sync. For example:
var myModel = Backbone.Model.extend({
sync: function (method, model, options) {
// remove the unwanted attributes. Something like...
options.attrs = _.pick(model.attributes, 'attribute1', 'attribute2', 'attribute3');
return Backbone.sync.call(this, method, model, options);
}
});
Overriding model.toJSON as suggested by mu_is_too_short worked nicely for me.
In the model
function() {
var json = Backbone.Model.prototype.toJSON.call(this);
json.ExtendedFieldData = JSON.stringify(json.ExtendedFieldData);
return json;
},
We use model.attributes for templates.

Is it possible to trigger an event when initializing a model?

In a backbone model, is it possible to trigger an event in the initialize function, for a nested view? I based my current code off this example: https://stackoverflow.com/a/8523075/2345124 and have updated it for backbone 1.0.0. Here is my initialize function, for a Model:
var Edit = Backbone.Model.extend({
initialize: function() {
this.trigger('marquee:add');
this.on('change', function(){
this.trigger('marquee:add');
});
}
...
}
I'm trying to call a method renderMarquee when the model is initialized:
var EditRow = Backbone.View.extend({
initialize: function() {
this.listenTo(this.model, "change", this.render); // works
this.listenTo(this.model, "marquee:add", this.renderMarquee); // only called when changed, but not when initially created
...
}
renderMarquee IS called when the model is changed, but not when it is initialized. 'change' events work as expected (this.render is called). Any thoughts?
Thanks!
I am currently facing a similar problem. I needed to trigger the change event in the initialize method of my model.
I looked into the backbone code which revealed why this is not happening:
var Model = Backbone.Model = function(attributes, options) {
...
this.set(attrs, options);
this.changed = {};
this.initialize.apply(this, arguments);
};
the set is executed before the initialize and this.change is emptied setting the model state to "nothing has changed".
In order to overwrite behavior this I added the following code to my initialize method.
initialize: function(attributes, options) {
...
this.changed = attributes;
this.trigger('change');
for (attr_name in attributes) {
this.trigger('change:' + attr_name);
}
},
I trigger all change events manually, this is important for me since inheriting models may bind to change or change:attrxy. But this is not enough, because if I just trigger the events the changedAttributes() method would return false therefore I also set this.changed to the current attributes.
This doesn't make a lot of sense because you are initializing the model somewhere prior to doing the view.listenTo call. Unfortunately, you don't really have a choice in that matter.
You are probably going to want to move the event handling to a Backbone.Collection which already has built in events you can listen on for adding/removing.

Backbone.js: just one line to set local storage?

demo fiddle (with problem) http://jsfiddle.net/mjmitche/UJ4HN/19/
I have a collection defined like this
var Friends = Backbone.Collection.extend({
model: Friend,
localStorage: new Backbone.LocalStorage("friends-list")
});
As far as I'm aware, that's all I need to do to get local storage to work (in addition to including it below backbone.js)
One thing I wasn't sure about, does the name "friends-list" have to correspond to a DOM element? I'm trying to save the "friends-list" so I called it that in local storage, however, localstorage doesn't seem to require passing a class or an id.
Here's a fiddle where you can see what I'm trying to do http://jsfiddle.net/mjmitche/UJ4HN/19/
On my local site, I'm adding a couple friends, refreshing the page, but the friends are not re-appearing.
Update
I've also done the following in my code on my local site
console.log(Backbone.LocalStorage);
and it's not throwing an error.
My attempt to debug
I tried this code (taken from another SO answer) in the window.AppView but nothing came up in the console.
this.collection.fetch({}, {
success: function (model, response) {
console.log("success");
},
error: function (model, response) {
console.log("error");
}
})
From the fine manual:
Quite simply a localStorage adapter for Backbone. It's a drop-in replacement for Backbone.Sync() to handle saving to a localStorage database.
This LocalStorage plugin is just a replacement for Backbone.Sync so you still have to save your models and fetch your collections.
Since you're not saving anything, you never put anything into your LocalStorage database. You need to save your models:
showPrompt: function() {
var friend_name = prompt("Who is your friend?");
var friend_model = new Friend({
name: friend_name
});
//Add a new friend model to our friend collection
this.collection.add(friend_model);
friend_model.save(); // <------------- This is sort of important.
},
You might want to use the success and error callbacks on that friend_model.save() too.
Since you're not fetching anything, you don't initialize your collection with whatever is in your LocalStorage database. You need to call fetch on your collection and you probably want to bind render to its "reset" event:
initialize: function() {
_.bindAll(this, 'render', 'showPrompt');
this.collection = new Friends();
this.collection.bind('add', this.render);
this.collection.bind('reset', this.render);
this.collection.fetch();
},
You'll also need to update your render to be able to render the whole collection:
render: function() {
var $list = this.$('#friends-list');
$list.empty();
this.collection.each(function(m) {
var newFriend = new FriendView({ model: m });
$list.append(newFriend.render().el);
});
$list.sortable();
return this;
}
You could make this better by moving the "add one model's view" logic to a separate method and bind that method to the collection's "add" event.
And a stripped down and fixed up version of your fiddle: http://jsfiddle.net/ambiguous/haE9K/

Multiple backbone views referencing one collection

I am trying to create my first backbone app and am having some difficulty getting my head around how I am meant to be using views.
What I am trying to do is have a search input that each time its submitted it fetches a collection from the server. I want to have one view control the search input area and listen to events that happen there (a button click in my example) and another view with sub views for displaying the search results. with each new search just prepending the results into the search area.
the individual results will have other methods on them (such as looking up date or time that they where entered etc).
I have a model and collection defined like this:
SearchResult = Backbone.model.extend({
defaults: {
title: null,
text: null
}
});
SearchResults = Backbone.Collection.extend({
model: SearchResult,
initialize: function(query){
this.query = query;
this.fetch();
},
url: function() {
return '/search/' + this.query()
}
});
In my views I have one view that represents the search input are:
var SearchView = Backbone.View.extend({
el: $('#search'),
events: {
'click button': 'doSearch'
},
doSearch: function() {
console.log('starting new search');
var resultSet = new SearchResults($('input[type=text]', this.el).val());
var resultSetView = new ResultView(resultSet);
}
});
var searchView = new SearchView();
var ResultSetView = Backbone.View.extend({
el: $('#search'),
initialize: function(resultSet) {
this.collection = resultSet;
this.render();
},
render: function() {
_(this.collection.models).each(function(result) {
var resultView = new ResultView({model:result});
}, this);
}
});
var ResultView = Backbone.view.extend({
tagName: 'div',
model: SearchResult,
initialize: function() {
this.render();
},
render: function(){
$(this.el).append(this.model.get(title) + '<br>' + this.model.get('text'));
}
});
and my html looks roughly like this:
<body>
<div id="search">
<input type="text">
<button>submit</button>
</div>
<div id="results">
</div>
</body>
In my code it gets as far as console.log('starting new search'); but no ajax calls are made to the server from the initialize method of the ResultSetView collection.
Am I designing this right or is there a better way to do this. I think because the two views bind to different dom elements I should not be instantiating one view from within another. Any advice is appreciated and if I need to state this clearer please let me know and I will do my best to rephrase the question.
Some problems (possibly not the only ones):
Your SearchView isn't bound to the collection reset event; as written it's going to attempt to render immediately, while the collection is still empty.
SearchView instantiates the single view ResultView when presumably it should instantiate the composite view ResultSetView.
You're passing a parameter to the SearchResults collection's constructor, but that's not the correct way to use it. See the documentation on this point.
You haven't told your ResultSetView to listen to any events on the collection. "fetch" is asynchronous. When completed successfully, it will send a "reset" event. Your view needs to listen for that event and then do whatever it needs to do (like render) on that event.
After fixing all the typos in your example code I have a working jsFiddle.
You see like after clicking in the button an AJAX call is done. Of course the response is an error but this is not the point.
So my conclusion is that your problem is in another part of your code.
Among some syntax issues, the most probable problem to me that I see in your code is a race condition. In your views, you're making an assumption that the fetch has already retrieved the data and you're executing your views render methods. For really fast operations, that might be valid, but it gives you no way of truly knowing that the data exists. The way to deal with this is as others have suggested: You need to listen for the collection's reset event; however, you also have to control "when" the fetch occurs, and so it's best to do the fetch only when you need it - calling fetch within the search view. I did a bit of restructuring of your collection and search view:
var SearchResults = Backbone.Collection.extend({
model: SearchResult,
execSearch : function(query) {
this.url = '/search/' + query;
this.fetch();
}
});
var SearchView = Backbone.View.extend({
el: $('#search'),
initialize : function() {
this.collection = new SearchResults();
//listen for the reset
this.collection.on('reset',this.displayResults,this);
},
events: {
'click button': 'doSearch'
},
/**
* Do search executes the search
*/
doSearch: function() {
console.log('starting new search');
//Set the search params and do the fetch.
//Since we're listening to the 'reset' event,
//displayResults will execute.
this.collection.execSearch($('input[type=text]', this.el).val());
},
/**
* displayResults sets up the views. Since we know that the
* data has been fetched, just pass the collection, and parse it
*/
displayResults : function() {
new ResultSetView({
collection : this.collection
});
}
});
Notice that I only created the collection once. That's all you need since you're using the same collection class to execute your searches. Subsequent searches only need to change the url. This is better memory management and a bit cleaner than instantiating a new collection for each search.
I didn't work further on your display views. However, you might consider sticking to the convention of passing hashes to Backbone objects. For instance, in your original code, you passed 'resultSet' as a formal parameter. However, the convention is to pass the collection to a view in the form: new View({collection: resultSet}); I realize that that's a bit nitpicky, but following the conventions improves the readability of your code. Also, you ensure that you're passing things in the way that the Backbone objects expect.

Resources