34

Is it possible to send just the modified properties of a model when saving the changes?

BTW, Are there any "official" Backbone.js group/mailing list to ask this kind of questions?

ggarber
  • 8,300
  • 5
  • 27
  • 32
  • 5
    This is now supported as of version 0.9.9, If you'd only like the changed attributes to be sent to the server, call model.save(attrs, {patch: true}). You'll get an HTTP PATCH request to the server with just the passed-in attributes. http://backbonejs.org/#Model-save – Ben Jan 06 '13 at 19:18

11 Answers11

46

Backbone does not support this out of the box, but you have all the tools to make that happen. If you look at Backbone.sync you will see that it calls toJSON on your model to get the actual data to send. Now you might have to tweak this out, but here is the gist of it:

initialize: function(){
  this.dirtyAttributes = {}
},
set: function(attrs, options){
  Backbone.Model.prototype.set.call(this, attrs, options);
  _.extend(this.dirtyAttributes, attrs);
},
toJSON : function(){
  json = this.dirtyAttributes;
  this.dirtyAttributes = {};
  return json;
}

If you want a complete solution you need to apply the same logic to unset, clear, save, etc. But I guess you get how to do this. I put the reset of the dirty attributes in the toJSON function, but it should really be in the success callback (when calling save).

Julien
  • 9,216
  • 4
  • 37
  • 38
  • Thank you for your answer. I accepted the other one because he was faster :-). – ggarber Mar 11 '11 at 16:08
  • I'd go even further and only mark as dirty attributes that have actually changed. Setting an attribute's value to it's original value (as loaded from server) should mark it as clean (no longer dirty). Would this make sense in the lib? Should I submit a patch? Note that the current behavior of `{ patch: true }` doesn't do this. – Mihai Alexandru Bîrsan Apr 30 '13 at 13:01
  • Wouldn't clearing the dirtyAttributes cause data loss if the save failed? Shouldn't the attributes be cleared only after the ajax call completed successfully? – geon Oct 25 '13 at 09:12
  • I'm pretty sure that `.set()` is called internally when the model gets data from the server, after a `fetch` and after a `save` to update properties that the server sends back. I'm pretty sure this solution would just mark all fields as "dirty" right away after a `.fetch()` – CodingWithSpike Mar 15 '16 at 20:47
33

Currently backbone does not support sending part of the model to the server. It would be an interesting addition though.

If you browse the source you can see that Backbone.sync (the part of backbone that is responsible for communicating with the data store) is one of the simplest components in backbone and simply wraps the ajax support in jQuery or Zepto.


UPDATE

starting backbone version 0.9.10, partial model update is supported natively via

model.save(attrs, {patch: true})
Jay Kumar
  • 1,514
  • 1
  • 14
  • 17
Andrew Hare
  • 344,730
  • 71
  • 640
  • 635
  • Thank you. This feature is supported in JavascriptMVC for example and I agree that it would be a great addition. – ggarber Mar 11 '11 at 16:09
  • 3
    Patch is not the good idea for `create` a new object and `update` a object,it's not a restful well design.I recommend override `sync` method – sjbwylbs Apr 21 '13 at 15:25
  • 4
    Looks like the easiest way to send only the changed fields is `model.save(model.changedAttributes(), {patch: true});`. That's at Backbone 1.0.0. – Harry Pehkonen Aug 02 '13 at 17:03
  • 2
    @HarryPehkonen changedAttributes does not keep track of dirty states between syncs, only between sets and is generally only used in a change callback. It would not work for this case – Johnathon Sanders Mar 07 '14 at 13:08
6

UPDATE: starting backbone version 0.9.10, partial update is supported natively via

model.save(attrs, {patch: true})


Until 0.9.9 An approach without directly editing the backbone.js library file. Just the add the following code in application js file and load it after backbone.js gets loaded.

//override the Backbone.sync to send only the changed fields for update (PUT) request
var Original_BackboneSync = Backbone.sync;

Backbone.sync = function(method, model, options) {
    /* just handle the data picking logic for update method */
    if (!options.data && model && method == 'update') {
        options.contentType = 'application/json';
        options.data = JSON.stringify(model.changedAttributes() || {});
    }

    //invoke the original backbone sync method
    return Original_BackboneSync.apply(this, arguments);
};

//Tested in Backbone.js 0.9.1
Jay Kumar
  • 1,514
  • 1
  • 14
  • 17
  • `changedAttributes` only contains the changes from the last call to `.set()` so this wouldn't work if you did something like `m.set('p1', 1); m.set('p2', 2); m.save();` it would only send `{ p2: 2 }` not all updated fields. – CodingWithSpike Mar 15 '16 at 20:50
2

Instead of overwriting Backbone.sync you could just do it inside the Model.sync method. Since you can't access model.changedAttributes() there, be sure to always return false inside this method.

sync: (method, model, options) ->
  if method is "update"
    options.contentType = 'application/json'
    changedData = {}
    for attr in _.keys(options.changes)
      changedData[attr] = model.get(attr)
    options.data = JSON.stringify changedData

  Backbone.sync method, model, options
mateusmaso
  • 7,843
  • 6
  • 41
  • 54
2

I tried several of the techniques that were suggested here, but in the end, decided to modify Backbone.Model and Backbone.sync directly. What I wanted to provide was a minimally invasive method for providing this functionality that didn't require instructing developers on my team on overriding backbone methods; just too prone to error. My solution involves only passing an option to the model's "save" method. For example:

//Note that this could be from your view or anywhere you're invoking model.save
saveToModel : function() {
    this.model.save({
        field1 : field1Value,
        field2 : field2Value,
        field3 : field3Value
    }, {partialUpdate : true}
}

Now, to enable this functionality, I made some very minor modifications to Backbone.Model.save and Backbone.sync. Here's the change to Backbone.Model.save:

//If a partialUpdate is required, create a member on the options
//hash called updateAttrs and set it to attrs
if (options.partialUpdate != "undefined" && options.partialUpdate) {
    options.updateAttrs = attrs;
}
//--->>>Put the block above right above the return line
return (this.sync || Backbone.sync).call(this, method, this, options);

What happens here is that if partialUpdate is passed as an option, then a new member called updateAttrs is created on the options hash. The options hash is automatically passed to Backbone.sync.

For Backbone.sync, I changed the following conditional:

// Ensure that we have the appropriate request data.
if (!params.data && model && (method == 'create' || method == 'update')) {
    params.contentType = 'application/json';
    params.data = JSON.stringify(model.toJSON());
}

to...

// Ensure that we have the appropriate request data.
if (!params.data && model && (method == 'create' || method == 'update')) {
    params.contentType = 'application/json';

    //If doing a partial model update, then grab the updateAttrs member
    //from options. Will not interfere with line directly below as params.data
    //will have been set.
    params.data = (options.partialUpdate != "undefined" && options.partialUpdate)
                ? params.data = JSON.stringify(options.updateAttrs)
                : params.data = JSON.stringify(model.toJSON());
}

Adding the extra conditional checks to see if partialUpdate was set, then if it is, set params.data to options.updateAttrs. This will then be passed to the jquery Ajax method.

Brendan Delumpa
  • 1,155
  • 1
  • 6
  • 11
1

Most of the answers here are either direct or indirectly modify the sync function. Here is my little trick to overcome this:

When you call your model.save, you can actually pass in the second parameter that will be passed along with the $.ajax when Backbone is trying to call the sync. I did the partial update like this, more of explicitly specifying which fields to submit:

/**
 * On user clicking on "mark important"
 */
onMarkImportantBtnClick: function() {
    var marked = this.model.get('UserFeed.marked_important'),
        data = {
            UserFeed: {
                marked_important: !marked
            }
        };
    this.model.save(data, {data: JSON.stringify(data), contentType: 'application/json'});
}

This action updated my model attributes correctly, plus sending to the server only the data that mentioned in the JSON.stringify. contentType is required here, better

This is because Backbone.sync has these lines, and we are negating it by passing data attribute:

if (!options.data && model && (method == 'create' || method == 'update')) {
    params.contentType = 'application/json';
    params.data = JSON.stringify(model.toJSON());
}

Credit to this page: https://plus.google.com/103858073822961240171/posts/1gTcu6avmWQ

Note: This model inherited from powmedia's DeepModel to support nested model attributes


Edit

Since Backbone 0.9.9, patch option has been added so this trick is only applicable to previous version.

To submit only the dirty data back to your server, supply {patch: true} in your save, such that

this.model.save(modifiedData, {patch: true});

Thanks @Lincoln B for pointing it out.

Lionel Chan
  • 7,894
  • 5
  • 40
  • 69
  • 1
    Best answer here, thanks for lookinga at the source. Backbone 0.9.9 added {path: true} to save, but if you cannot upgrade right now this is the ideal thing to do. I personally defined a seperate patch like patch: (attributes, options={}) -> this.save(attributes, { data: JSON.stringify(attributes)} ) – Lincoln B Jan 17 '13 at 23:41
  • Yes. This solution is only applicable to older Backbone version. The new `patch` is cool! – Lionel Chan Jan 18 '13 at 01:41
1

If you need to send update request to a server with just a specific attributes, you can do something similar:

saveAttributes: (attributes, options={}) ->
  data = {}
  _(attributes).each (attribute) =>
    data[attribute] = @get(attribute)

  params =
    data: $.param(data)

  _.extend(params, options)

  Backbone.sync('update', null, params)

More info on that: https://github.com/documentcloud/backbone/pull/573

You can extend it with _.extend Backbone.Model.prototype

Dmitry Polushkin
  • 3,283
  • 1
  • 38
  • 44
  • I get `A 'url' property or function must be specified. Error: A 'url' property or function must be specified backbone_rails_sync.js:15` – lulalala Aug 27 '12 at 08:21
0

Using the (very good) answer of Jayyy V, i rewrote it a little bit to make the sync function take a whitelist so you can give it an array of keys that get saved.

var Original_BackboneSync = Backbone.sync;
Backbone.sync = function(method, model, options) {
    /* check if a whitelist was in options */
    if (options.whitelist) {
      options.contentType = 'application/json';
      /* use underscore method for picking only whitelisted attributes to save */
      options.data = JSON.stringify(_.pick(model.attributes, options.whitelist));
    }

    //invoke the original backbone sync method
    return Original_BackboneSync.apply(this, arguments);
};
0

Building on @Julien's post: You can add this to your model, and it will only send the attributes that you pass in as opposed to the entire model. You can still use save for the default behavior, and you can use partialSave when you want to just send those attributes that you pass in as a parameter. I have tested this, and it worked for me.

  partialSave: function(attr, options) { //use this method instead of save()
    this.dirtyAttributes = attr;
    this.save(attr, options);
  },

  toJSON: function() { //overrides Backbone.Model.prototype.toJSON
    if (this.dirtyAttributes) {
      var attr = this.dirtyAttributes;
      this.dirtyAttributes = null;
      return attr;
    }
    return Backbone.Model.prototype.toJSON.apply(this, arguments);
  },
Daniel
  • 1,789
  • 17
  • 15
0

In fact there is a much simpler way of achieving this

if you look at backbone.js line 1145 you will see that

// Ensure that we have the appropriate request data.
    if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
      params.contentType = 'application/json';
      params.data = JSON.stringify(options.attrs || model.toJSON(options));
    }

Which means that you may override the data part of the xhr by putting data in your options

Since backbone save requires model.save([attributes], [options])

But remember that attributes like id might be essential to proper saving

Example

model.save( {}, { data: JSON.stringify(data) } ) ; 

So you should be doing something like this

var data = { id : model.id , otherAttributes : 'value' }  ;  

or

var data = model.toJSON () ;
remove data.tempData ; 

Finally

model.save( {}, { data : JSON.stringify(data) } );

This do the trick quite well for me and could be used with any backbone with xhr such as fetch, save, delete, ...

Messing with save, sync or toJSON look so wrong

brasofilo
  • 25,496
  • 15
  • 91
  • 179
Pascal
  • 2,377
  • 3
  • 25
  • 40
0

I created extended Model.

var CModel = Backbone.Model.extend({
    save: function(attributes, options) {
        if(_.isUndefined(options)) {
            options = {};
        }

        var isNeedAttrsRefresh = false,
            basicAttributes = null;

        if(!_.isUndefined(options.fields)) {
            basicAttributes = _.clone(this.attributes);
            var newAttributes = {};
            _.each(this.attributes, function(value, name) {
                if(options.fields.indexOf(name) > -1) {
                    newAttributes[name] = value;
                }
            });
            this.attributes = newAttributes;
            isNeedAttrsRefresh = true;
        }

        this.isSaving = true;
        var result = Backbone.Model.prototype.save.apply(this, arguments);
        this.isSaving = false;

        if(isNeedAttrsRefresh) {
            this.attributes = basicAttributes;
        }

        return result;
    }
});

Example of usage:

var CommentModel = CModel.extend({ ... }

And allowed fields to save:

comment.save(null, {    fields: ['message', 'entry_id', 'module_id', 'parent_id'] });
Oleksandr Knyga
  • 625
  • 9
  • 9