One of the most frustrating things I ran across when I first started using Backbone.js was the way in which models updates were sent to the server. The documentation includes the following:
“The attributes hash (as in set) should contain the attributes you’d like to change — keys that aren’t mentioned won’t be altered — but, a complete representation of the resource will be sent to the server.”
When I read that, my heart sank because POST or PUT operations to our API only allowed certain fields to be passed as many attributes were immutable. I discovered that I wasn’t alone in this particular quandary as a search on doing selective updates with Backbone.js revealed many people wanting to know how to do the same thing.
I tried several things, but most had to do with changing the Backbone.js source code; something that I really didn’t want to do. But taking a deep-dive into Backbone’s source, I discovered a couple of interesting blocks of code in the Backbone.sync object, that ignited an idea. The first block was simply this:
if (!options.data && model && (method == 'create' || method == 'update')) { params.contentType = 'application/json'; params.data = JSON.stringify(model.toJSON()); }
Basically the conditional checks for the existence of options.data. If it existed, the code within the conditional would not execute; meaning that the model would not be copied to params.data. Then I got really excited when I saw the last line of code in Backbone.sync:
return $.ajax(_.extend(params, options));
In that line, options is mixed into params! That meant that I could define my options.data outside of Backbone.sync and pass in only the fields that I wanted to post!
I won’t go into all the niggling details behind coming up with a solution, but suffice it to say that I found that the best thing to do was to make a descendant model of Backbone.Model and override the save method. The following override method will allow you to save only the fields you want to save. It will also handle the case where you do a model.set({fields}) then just all a raw model.save(), as the parent save is invoked via a call at the end of the method. Here’s the override:
save : function(key, value, options) { var attributes={}, opts={}; //Need to use the same conditional that Backbone is using //in its default save so that attributes and options //are properly passed on to the prototype if (_.isObject(key) || key == null) { attributes = key; opts = value; } else { attributes = {}; attributes[key] = value; opts = options; } //Since backbone will post all the fields at once, we //need a way to post only the fields we want. So we can do this //by passing in a JSON in the "key" position of the args. This will //be assigned to opts.data. Backbone.sync will evaluate options.data //and if it exists will use it instead of the entire JSON. if (opts && attributes) { opts.data = JSON.stringify(attributes); opts.contentType = "application/json"; } //Finally, make a call to the default save now that we've //got all the details worked out. return Backbone.Model.prototype.save.call(this, attributes, opts); }
The beauty of this is that it doesn’t require the alteration of any of the Backbone.js source code.