43

Is there a way to exclude certain property from my model when I sync?

For example, I keep in my model information about some view state. Let's say I have a picker module and this module just toggle a selected attributes on my model. Later, when I call .save() on my collection, I'd want to ignore the value of selected and exclude it from the sync to the server.

Is there a clean way of doing so?

(Let me know if you'd like more details)

Simon Boudrias
  • 42,953
  • 16
  • 99
  • 134

11 Answers11

46

This seems like the best solution (based on @nikoshr referenced question)

Backbone.Model.extend({

    // Overwrite save function
    save: function(attrs, options) {
        options || (options = {});
        attrs || (attrs = _.clone(this.attributes));

        // Filter the data to send to the server
        delete attrs.selected;
        delete attrs.dontSync;

        options.data = JSON.stringify(attrs);

        // Proxy the call to the original save function
        return Backbone.Model.prototype.save.call(this, attrs, options);
    }
});

So we overwrite save function on the model instance, but we just filter out the data we don't need, and then we proxy that to the parent prototype function.

Cymen
  • 14,079
  • 4
  • 52
  • 72
Simon Boudrias
  • 42,953
  • 16
  • 99
  • 134
  • 3
    attrs might be null so do JSON.stringify(attrs || this.attributes) instead .. or JSON.stringify(_.pick(attrs || this.attributes, "f1", "f2", ...)) for a whitelist – David Tinker Jul 25 '13 at 12:46
  • Pretty sure this example isn't quite right. You're stringifying attrs and then trying to delete variables from it like it's an object... but it's a string. – Will Jul 30 '13 at 23:43
  • Updated - although I'm unsure in which case the attrs would be passed empty. – Simon Boudrias Aug 26 '14 at 19:11
  • 1
    I think this solution will cause strange errors. If you pass in null for attrs then it will use the model's attributes and delete a couple (attrs.selected and attrs.dontSync) so everytime you call save you'll be losing attributes from your model. Unless I'm reading that wrong? – Evan Hobbs Oct 28 '14 at 14:56
  • @EvanHobbs Looks correct, we should clone the object. – Simon Boudrias Oct 28 '14 at 20:48
  • `save` should also return the result of calling the `Backbone.Model.prototype.save` as it returns the XHR request object. – Cymen May 19 '15 at 22:46
  • I edited that in -- commenting in case I missed a reason not to do that. – Cymen May 19 '15 at 22:47
  • Also mind that this method won't properly `set` blacklisted properties if they're passed as parameter to the `save`. – Annarfych Dec 10 '16 at 03:01
35

In Underscore 1.3.3 they added pick and in 1.4.0 they added omit which can be used very simply to override your model's toJSON function to whitelist attributes with _.pick or blacklist attributes with _.omit.

And since toJSON is used by the sync command for passing the data to the server I think this is a good solution as long as you do not want these fields wherever else you use toJSON.

Backbone.Model.extend({
    blacklist: ['selected',],
    toJSON: function(options) {
        return _.omit(this.attributes, this.blacklist);
    },
});
byoungb
  • 1,771
  • 20
  • 31
  • 18
    `_.pick` and `_.omit` are great, I use them, but in `save`, not `toJSON`. The problem with `toJSON` approach is that it filters out data you need for the view as well as, not just for saving to server. – Azder Jun 20 '13 at 08:48
  • That is true, but I would also say that for me I do not use the toJSON for view/template rendering already because it is not very forgiving on undefined variables, so I instead pass the entire model to the template, and then I can user model.has("field") in case it is not set yet. (for instance the ID on a newly created field) – byoungb Mar 17 '14 at 17:04
  • "not forgiving" - I haven't had that problem with my templates, toJSON returned all I need: all I did was set the field defaults: { id: null }, and voila, id is set :) – Azder Mar 18 '14 at 21:48
  • This is great, however, I have a problem that the non-synced attributes are overwritten on a collection fetch. Is it possible to preserve them? – Anton Abilov Jun 01 '14 at 21:09
  • Yeah you could just override your model's `parse` function and strip out the attributes that you do not what overwritten (with `_.omit`!), but this would cause it to not be set it initially. But you could code around that. – byoungb Jun 02 '14 at 13:55
  • @azder to clarify what I meant by "not forgiving". If you don't have defaults for everything and assume that is available in the _.template it will fail. Also I like to use custom model methods inside my templates from time to time so I find it just easier to pass the entire model to the template. `<%= model.get('selected') %>` or `<%= model.custom_method() %>` – byoungb Aug 05 '15 at 19:33
11

my solution combine all the above. just use white list instead of black one .. this is good rule in general

define

          attrWhiteList:['id','biography','status'],

and then overwrite the save

  save: function(attrs, options) {
    options || (options = {});

 //here is whitelist or all
    if (this.attrWhiteList != null )
          // Filter the data to send to the server
             whitelisted =  _.pick(this.attributes, this.attrWhiteList);
    else  
        whitelisted =this.attributes;
    /* it seems that if you override save you lose some headers and the ajax call changes*/
    // get data
    options.data = JSON.stringify(whitelisted);

    if ((this.get('id') == 0) || (this.get('id') == null)) 
        options.type = "POST"
    else
        options.type = "PUT";


    options.contentType = "application/json";
     //        options.headers =  { 
     //            'Accept': 'application/json',
     //            'Content-Type': 'application/json' 
     //        },

    // Proxy the call to the original save function
   return  Backbone.Model.prototype.save.call(this, attrs, options);
},
oak
  • 2,898
  • 2
  • 32
  • 65
  • yeah whitelist feels better. And I'd pass it as an option in case I need different things to be saved in different situations. – msanjay Mar 07 '14 at 09:11
  • Btw what about the attrs parameter? It should be included in the toJson – msanjay Mar 07 '14 at 09:42
  • hey @msanjay sorry for the delay but what do you mean by "included in the toJson"? – oak Mar 26 '14 at 15:08
  • Oops by toJson I meant stringify, I don't remember why exactly now, but for some reason I got it working with: options.data = JSON.stringify(_.extend(whitelisted, attrs)); – msanjay Apr 01 '14 at 06:40
  • basicly you don't need to _.extend whitelisted as long as you have attrWhiteList in your object. why? because the _.pick will get you the right attr. if you want to use the passed attr, you can do `_.pick(attrs,this.attrWhiteList)` instead of `this.attributes` – oak Apr 01 '14 at 07:20
6

In fact there is a much simpler way of achieving this without messing with backbone save or sync function since you would no be expecting this behaviour to be permanent

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' }  ;  
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, ...

brasofilo
  • 25,496
  • 15
  • 91
  • 179
Pascal
  • 2,377
  • 3
  • 25
  • 40
  • 2
    That works well for a case by case need. Not to filter out each time display related property. But it is a valid solution and way easier than a lot proposed here for "case by case" solution. – Simon Boudrias Aug 04 '14 at 19:28
3

Based on several of the answers, this accounts for cases of null objects and a conditional in Backbone that doesn't sent the contentType if options.data is already set:

EDITABLE_ATTRIBUTES = ["name", "birthdate", "favoriteFood"];

...

save: function(attrs, options) {
  // `options` is an optional argument but is always needed here
  options || (options = {});

  var allAttrs = _.extend({}, this.attributes, attrs);
  var allowedAttrs = _.pick(allAttrs, EDITABLE_ATTRIBUTES);

  // If `options.data` is set, Backbone does not attempt to infer the content
  // type and leaves it null. Set it explicitly as `application/json`.
  options.contentType = "application/json";
  options.data = JSON.stringify(allowedAttrs);

  return Backbone.Model.prototype.save.call(
    this, allowedAttrs, options);
},
Ross Allen
  • 43,772
  • 14
  • 97
  • 95
3

I found some problems with the accepted solution, as options.data modifies the way Backbone makes the calls. Better using options.attrs as this:

Backbone.Model.extend({
    save: function (attrs, options) {
        options = options || {};
        attrs = _.extend({}, _.clone(this.attributes), attrs);

        // Filter the data to send to the server
        delete attrs.selected;
        options.attrs = attrs;
        // Proxy the call to the original save function
        return Backbone.Model.prototype.save.call(this, attrs, options);
    }
});
Nacho
  • 81
  • 3
  • I think that this is now the correct solution---it requires no modification or complex assumptions about `Backbone.sync` or `Model.save`, and it allows you to pass whatever attributes you want via XHR, without stepping on any of the MANY confusing things that `Model.save()` does. – mikebridge Jun 09 '15 at 21:42
  • This works as advertised and I agree looks simpler than the current accepted answer. – Shyam Habarakada Jun 12 '19 at 21:33
2

Since save uses toJSON we override it:

    toJSON: function(options) {
        var attr = _.clone(this.attributes);
        delete attr.selected;
        return attr;
    },

But it may not work if you're using toJSON and need selected in views for example. Otherwise you probably need to override save method.

rinat.io
  • 3,168
  • 2
  • 21
  • 25
  • 3
    the problem with toJSON approach is that it filters out data you need for the view as well as, not just for saving to server – Azder Jun 20 '13 at 08:47
  • Note that you can pass options to `toJSON` making it possible to bypass your modification when needing it for a view. – Emile Bergeron Jun 21 '16 at 19:35
2

Set options.attrs will allow you customise api param:

var model = new Backbone.Model();
model.save(null, {
  wait: true,
  success: function() {
  },
  attrs: _.omit(model.attributes, 'selected')
});
John Xiao
  • 1,680
  • 2
  • 18
  • 24
  • Good solution if you need custom per action attributes. But it won't scale if you want to always ignore certain properties when saving. – Simon Boudrias Jul 10 '14 at 18:23
  • @SimonBoudrias If that, you can consider this solution: https://gist.github.com/bammoo/d8b09252e4cfa081d0e6 – John Xiao Jul 12 '14 at 12:53
0

If it is a one-off occasion, you could use mode.unset('selected', { silent:true }) (silent is set only to avoid firing the change event), to remove the attribute... This has the not so nice counter-effect of having to re-set it after saving though.

This said, I totally endorse one of the solutions above. Moreover if this is something you need on a more regular basis.

Tallmaris
  • 7,605
  • 3
  • 28
  • 58
0

To set only desired values, use HTTP PATCH insead of HTTP POST. On the backbone side, just add a patch attribute to the save method:

entity.save(data,{patch:true})

Using save with this attribute, only fields passed as data are sent to the server.

Pavel Sedek
  • 519
  • 5
  • 13
0

Having this same issue, I decided to create a small module that can help : https://github.com/lupugabriel1/backbone-model-save

This is how you can use it in your models:

var myModel = new Backbone.ModelSave.extend({
    notSync: ['name', 'age']
});
Gabriel Lupu
  • 1,599
  • 1
  • 12
  • 16