diff --git a/backbone.js b/backbone.js index 8485dac6..32ef58bd 100644 --- a/backbone.js +++ b/backbone.js @@ -187,6 +187,7 @@ options || (options = {}); if (!attrs) return this; if (attrs.attributes) attrs = attrs.attributes; + if (options.unset) for (var attr in attrs) attrs[attr] = void 0; var now = this.attributes, escaped = this._escapedAttributes; // Run validation. @@ -202,8 +203,8 @@ // Update attributes. for (var attr in attrs) { var val = attrs[attr]; - if (!_.isEqual(now[attr], val)) { - now[attr] = val; + if (!_.isEqual(now[attr], val) || options.unset && (attr in now)) { + options.unset ? delete now[attr] : now[attr] = val; delete escaped[attr]; this._changed = true; if (!options.silent) this.trigger('change:' + attr, this, val, options); @@ -220,53 +221,20 @@ // Remove an attribute from the model, firing `"change"` unless you choose // to silence it. `unset` is a noop if the attribute doesn't exist. - unset : function(attr, options) { - if (!(attr in this.attributes)) return this; - options || (options = {}); - var value = this.attributes[attr]; - - // Run validation. - var validObj = {}; - validObj[attr] = void 0; - if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; - - // changedAttributes needs to know if an attribute has been unset. - (this._unsetAttributes || (this._unsetAttributes = [])).push(attr); - - // Remove the attribute. - delete this.attributes[attr]; - delete this._escapedAttributes[attr]; - if (attr == this.idAttribute) delete this.id; - this._changed = true; - if (!options.silent) { - this.trigger('change:' + attr, this, void 0, options); - this.change(options); + unset : function(attrs, options) { + if (_.isString(attrs)) { + var args = _.toArray(arguments), attrs = {}; + while (_.isString(options = args.shift())) attrs[options] = void 0; } - return this; + (options || (options = {})).unset = true; + return this.set(attrs, options); }, // Clear all attributes on the model, firing `"change"` unless you choose // to silence it. clear : function(options) { - options || (options = {}); - var attr; - var old = this.attributes; - - // Run validation. - var validObj = {}; - for (attr in old) validObj[attr] = void 0; - if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; - - this.attributes = {}; - this._escapedAttributes = {}; - this._changed = true; - if (!options.silent) { - for (attr in old) { - this.trigger('change:' + attr, this, void 0, options); - } - this.change(options); - } - return this; + var keys = _.without(_.keys(this.attributes), 'id'); + return this.unset.apply(this, keys.concat([options])); }, // Fetch the model from the server. If the server's representation of the @@ -346,7 +314,6 @@ change : function(options) { this.trigger('change', this, options); this._previousAttributes = _.clone(this.attributes); - this._unsetAttributes = null; this._changed = false; }, @@ -362,23 +329,16 @@ // view need to be updated and/or what attributes need to be persisted to // the server. Unset attributes will be set to undefined. changedAttributes : function(now) { + if (!this._changed) return false; now || (now = this.attributes); - var old = this._previousAttributes, unset = this._unsetAttributes; - - var changed = false; + var changed = false, old = this._previousAttributes; for (var attr in now) { - if (!_.isEqual(old[attr], now[attr])) { - changed || (changed = {}); - changed[attr] = now[attr]; - } + if (_.isEqual(old[attr], now[attr])) continue; + (changed || (changed = {}))[attr] = now[attr]; } - - if (unset) { - changed || (changed = {}); - var len = unset.length; - while (len--) changed[unset[len]] = void 0; + for (var attr in old) { + if (!(attr in now)) (changed || (changed = {}))[attr] = void 0; } - return changed; }, diff --git a/test/model.js b/test/model.js index 4963248b..ac1ab0a9 100644 --- a/test/model.js +++ b/test/model.js @@ -140,19 +140,24 @@ $(document).ready(function() { }); test("Model: set and unset", function() { + expect(8); attrs = {id: 'id', foo: 1, bar: 2, baz: 3}; a = new Backbone.Model(attrs); var changeCount = 0; a.bind("change:foo", function() { changeCount += 1; }); a.set({'foo': 2}); - ok(a.get('foo')== 2, "Foo should have changed."); + ok(a.get('foo') == 2, "Foo should have changed."); ok(changeCount == 1, "Change count should have incremented."); a.set({'foo': 2}); // set with value that is not new shouldn't fire change event - ok(a.get('foo')== 2, "Foo should NOT have changed, still 2"); + ok(a.get('foo') == 2, "Foo should NOT have changed, still 2"); ok(changeCount == 1, "Change count should NOT have incremented."); - a.unset('foo'); - ok(a.get('foo')== null, "Foo should have changed"); + a.validate = function(attrs) { + ok(attrs.foo === void 0, 'ignore values when unsetting'); + }; + a.unset({foo: 1}); + ok(a.get('foo') == null, "Foo should have changed"); + delete a.validate; ok(changeCount == 2, "Change count should have incremented for unset."); a.unset('id'); @@ -199,11 +204,18 @@ $(document).ready(function() { test("Model: clear", function() { var changed; - var model = new Backbone.Model({name : "Model"}); + var model = new Backbone.Model({id: 1, name : "Model"}); model.bind("change:name", function(){ changed = true; }); + model.bind("change", function() { + var changedAttrs = model.changedAttributes(); + ok('name' in changedAttrs); + ok(!('id' in changedAttrs)); + }); model.clear(); equals(changed, true); equals(model.get('name'), undefined); + equals(model.id, 1); + equals(model.get('id'), 1); }); test("Model: defaults", function() { @@ -450,12 +462,14 @@ $(document).ready(function() { }); test("Model: Multiple nested calls to set", function() { - var model = new Backbone.Model({}); + var counter = 0, model = new Backbone.Model({}); model.bind('change', function() { + counter++; model.set({b: 1}); model.set({a: 1}); }) .set({a: 1}); + equal(counter, 1, 'change is only triggered once'); }); });