3

I would like to save calls to my server, so I am currently using Model.save() with the patch option and sending changedAttributes().

I would like to remove an attribute and add a new one. Model.set()/unset() will modify changedAttributes() each time such that I cannot use it with the Model.save()/patch scheme described above.

I think I would like to simply call Model.set() and pass in an object with the values I wish to unset set to undefined along with the values I wish to set.

Is there a way that I can unset() and set() in one go to get the changedAttributes()? Or maybe determine the changedAttributes() for a combined set of operations?

// Currently
var m = new Backbone.Model({ "foo": "bar" });
m.unset("foo");
console.log(m.changedAttributes()); // { "foo": undefined }
m.set("baz", "bar");
console.log(m.changedAttributes()); // { "baz": "bar" }
console.log(m.attributes); // { "baz": "bar" }

// At this point, how do I get the combination of changed attributes? something like: { "foo": undefined, "baz": "bar" }?
// Is that even possible? Am I doing something horribly wrong?

//================================================================

// What (I think) I want is for Model.set() to remove attributes with values of undefined, so I only have to make one call and changedAttributes() will be pristine. Maybe with a option or something?
var w = new Backbone.Model({ "foo": "bar" });
w.set({ "foo": undefined, "baz": "bar" });
console.log(w.changedAttributes()); // { "foo": undefined, "baz": "bar" }
console.log(w.attributes); // I would like it to be { "baz": "bar" }, "foo" having been removed in the set() call.

//================================================================

// I was trying to avoid processing the objects by hand. I realize that I can do something like the following.
var h = new Backbone.Model({ "foo": "bar" });
var changes = { "foo": undefined, "baz": "bar" };
_.each(changes, function(val, key) {
    if (_.isUndefined(val)) {
        h.unset(key, { "silent": true });
    } else {
        h.set(key, val, { "silent": true });
    }
});
h.trigger('change'); // Trigger a change event after all the changes have been done.
console.log(changes); // { "foo": undefined, "baz": "bar" }
console.log(h.attributes); // { "baz": "bar" }

Fiddle of above code in action: http://jsfiddle.net/clayzermk1/AmBfh/

There seems to have been some discussion on this topic about a year ago https://github.com/documentcloud/backbone/pull/879. It seems like the functionality I wanted existed at some point.

EDIT: As @dennis-rongo pointed out, I can obviously do this by hand. To restate my question above: "Does Backbone allow setting/deleting of attributes at once?" and if not, what is the rationale behind that decision? Derick Bailey created Backbone.Memento (https://github.com/derickbailey/backbone.memento) to deal with attribute states, and there are several issues on Backbone about model states closely related to this scenario (https://github.com/documentcloud/backbone/pull/2360, somewhat relevant: https://github.com/documentcloud/backbone/issues/2316, highly relevant: https://github.com/documentcloud/backbone/issues/2301).

EDIT 2: I'm not looking for a hand-rolled solution, I can make it do more or less what I want (see sample code above). I'm looking for a justification of the current design with a clean example for this common scenario - set and unset in one go.

UPDATE: There has been some conversation about this subject in https://github.com/documentcloud/backbone/issues/2301. I have submitted a pull request (https://github.com/documentcloud/backbone/pull/2368) to try and encourage discussion of the current implementation.

Thank you to everyone who posted an answer!

clayzermk1
  • 1,018
  • 1
  • 12
  • 14

3 Answers3

1

There's a lot of ways to skin this one! So, I'll focus on your the part of your question where you ask:

Is there a way that I can unset() and set() in one go to get the changedAttributes()?

because I think that's the way to go here.

Backbone.Model.unset() is just an alias for Backbone.Model.set(). From the source:

unset: function(attr, options) {
  return this.set(attr, void 0, _.extend({}, options, {unset: true}));
}, 

So why not just do m.set({"baz": "bar", "foo": void 0});? See this fiddle I forked from yours: http://jsfiddle.net/dimadima/Q8ZuV/. Pasting from there, the result will be

console.log(m.changedAttributes()); // { "baz": "bar", "foo": undefined }
console.log(m.attributes); // // {foo: undefined, baz: "bar"}, unfortunately "foo"is not deleted

So m.attributes is a bit off because the key you've unset hasn't been deleted, but you can test for that.

Anyway, I recommend skimming the source of Backbone.Model.set() to get a sense of what your other options would be. I could elaborate if you'd like.

Dmitry Minkovsky
  • 36,185
  • 26
  • 116
  • 160
  • Thank you for your answer dimadima. I'd examined the source for `set()` previously and noted the `unset()` alias / `{ unset: true }` flag in `set()`. `void 0` evaluates to `undefined` so functionally, I don't think that is any different than what I'm doing in my "What I want" example. I also need for the attribute to be deleted entirely (having `undefined` values causes issues elsewhere). – clayzermk1 Mar 12 '13 at 01:18
  • So, that being the case, I would define my own `.set()` that's just an `_.wrap`per around `Backbone.Model.set`: calls `Backbone.Model.set` (see "Brief aside on super" at http://backbonejs.org/#Model-extend), but afterward also does something like Dennis suggests. Then you could call your `set()` using the object notation, and it would also remove the undefined values. – Dmitry Minkovsky Mar 12 '13 at 01:22
  • Simply put, if you look at the source for `Backbone.model.set`, you'll see that `this.changed` is reset every time you call `.set`, and `.changedAttributes` relies on `this.changed`. So you need to use the object form of `.set` unless you want to devise some way to keep track of changes yourself as they accumulate between calls. – Dmitry Minkovsky Mar 12 '13 at 01:25
  • Thank you. I had considered overriding the `set()` function, but unfortunately the loop logic is smack in the middle of it. When the function returns, the events have already been triggered, etc. Yes, that is my problem and that is precisely what I am trying to cleanly get around. Cheers! – clayzermk1 Mar 12 '13 at 17:13
0

Something like this should work.

Basically loop through all the attributes and unset properties that are invalid (falsy and non-boolean values).

/** Create a Model and set attributes */
var President = Backbone.Model.extend({});
var m = new President({first: 'Abraham', last: 'Lincoln', age: 90});

/** Blank out an attribute or two */
m.set({age: ''});

/** Loop through each attributes and unset falsy ones.  
    Also pass in silent = true so it doesn't trigger a change event 
       (in case you have listeners).
*/
_.each(m.toJSON(), function(val, col){
   if (typeof val !=='boolean' && !val) {
       m.unset(col, {silent: true});
   }
}, this);

/** Output the new Model */
console.log(m.toJSON());

OR

You can create a new Model that only contains the changed attributes if you'd rather go in that direction.

var President = Backbone.Model.extend({});
var m = new President({
    first: 'Abraham', last: 'Lincoln', age: 90, registered: true});

/** Blank out or change an attribute or two */
m.set({first: null});

/** Pass changed attributes to a new Model */
var t = new President();
if (!_.isEmpty(m.changedAttributes())) {
    _.each(m.changedAttributes(), function(val, col) {
        t.set(col, val);
   }, this);
}
Dennis Rongo
  • 4,611
  • 1
  • 25
  • 25
  • Thank you for answering Dennis. Are you suggesting that what I am looking for is simply not in the design? I'm looking for a set of changes (obviously, the object I pass to `set()` in my example is that very set), and a modified resulting object (which of course I could do by hand as you did). Maybe I've just been staring at this too long. =/ – clayzermk1 Mar 12 '13 at 01:04
  • There's a reason why Backbone sends the entire `Model` to the server since in most scenarios, your server expects those attributes to be present. Unless your server takes care of removing undefined or empty attributes already? Another option aside from what I posted is to override the `sync` function and perform a check similar to what I have posted. – Dennis Rongo Mar 12 '13 at 01:13
  • Thank you. I'm using MongoDB as my server, and while it will accept `undefined` values, it doesn't make a lot of sense to me to store them . I'll take a look at the `sync` override. Cheers! – clayzermk1 Mar 12 '13 at 17:05
  • Interesting, in my specific case (outside the scope of this question) I won't be able to do that because my `Model`s are tied to `Marionette` `*View`s. I'm not looking for a hand-rolled solution though - I can _make_ it work how I want it. I'm looking for a justification of the current design for this common scenario. I will clarify my question further. Thanks again! – clayzermk1 Mar 12 '13 at 18:31
0

The current implementation will allow you to call set() and pass in an object with mixed attributes to set and unset. To effectively unset an attribute, assign it the value of undefined. The key will remain in the attributes hash on the model, but any time the model is serialized to JSON, the undefined values will not be present in the serialization (this is due to the implementation of JSON.stringify()). It will not delete the attributes from the model.

The behavior of JSON.stringify() removing undefined values in serialization is described on MDN - JSON - stringify:

If undefined, a function, or an XML value is encountered during conversion it is either omitted (when it is found in an object) or censored to null (when it is found in an array).

I was not using JSON serialization for my specific case (BSON), so I ended up having to hand-code a solution for myself.

I struck up a discussion on GitHub with a pull request, in the end a decision was made to keep the API as it is. For details see this pull requtest: https://github.com/documentcloud/backbone/pull/2368.

Thank you again to everyone who participated in the discussion, both on SO and GH.

clayzermk1
  • 1,018
  • 1
  • 12
  • 14