Model.set() with new and undefined values in Backbone.js

clayzermk1 picture clayzermk1 · Mar 12, 2013 · Viewed 9.6k times · Source

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!

Answer

Dmitry Minkovsky picture Dmitry Minkovsky · Mar 12, 2013

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.