Backbone.js partial model update

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?

Answers:

Answer

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})
Answer

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).

Answer

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
Answer

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.

Answer

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
Answer

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

Answer

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.

Answer

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);
};
Answer

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);
  },
Answer

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

Answer

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'] });

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.