Merge pull request #936 from braddunbar/nested-change

fixes #915 - nested `'change:attr'` events
This commit is contained in:
Jeremy Ashkenas
2012-02-02 08:51:50 -08:00
2 changed files with 66 additions and 16 deletions

View File

@@ -233,15 +233,19 @@
var now = this.attributes;
var escaped = this._escapedAttributes;
var prev = this._previousAttributes || {};
var alreadyChanging = this._changing;
var alreadySetting = this._setting;
this._changed || (this._changed = {});
this._changing = true;
this._setting = true;
// Update attributes.
for (attr in attrs) {
val = attrs[attr];
if (!_.isEqual(now[attr], val)) delete escaped[attr];
options.unset ? delete now[attr] : now[attr] = val;
if (this._changing && !_.isEqual(this._changed[attr], val)) {
this.trigger('change:' + attr, this, val, options);
this._moreChanges = true;
}
delete this._changed[attr];
if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
this._changed[attr] = val;
@@ -249,9 +253,9 @@
}
// Fire the `"change"` events, if the model has been changed.
if (!alreadyChanging) {
if (!alreadySetting) {
if (!options.silent && this.hasChanged()) this.change(options);
this._changing = false;
this._setting = false;
}
return this;
},
@@ -379,12 +383,20 @@
// a `"change:attribute"` event for each changed attribute.
// Calling this will cause all objects observing the model to update.
change: function(options) {
if (this._changing || !this.hasChanged()) return this;
this._changing = true;
this._moreChanges = true;
for (var attr in this._changed) {
this.trigger('change:' + attr, this, this._changed[attr], options);
}
this.trigger('change', this, options);
while (this._moreChanges) {
this._moreChanges = false;
this.trigger('change', this, options);
}
this._previousAttributes = _.clone(this.attributes);
delete this._changed;
this._changing = false;
return this;
},
// Determine if the model has changed since the last `"change"` event.

View File

@@ -506,17 +506,6 @@ $(document).ready(function() {
a.set({state: 'hello'});
});
test("Model: Multiple nested calls to set", function() {
var counter = 0, model = new Backbone.Model({});
model.on('change', function() {
counter++;
model.set({b: 1});
model.set({a: 1});
})
.set({a: 1});
equal(counter, 1, 'change is only triggered once');
});
test("hasChanged/set should use same comparison", function() {
expect(2);
var changed = 0, model = new Backbone.Model({a: null});
@@ -627,4 +616,53 @@ $(document).ready(function() {
equal(changed, 1);
});
test("nested `set` during `'change:attr'`", 1, function() {
var model = new Backbone.Model();
model.on('change:x', function() { ok(true); });
model.on('change:y', function() {
model.set({x: true});
// only fires once
model.set({x: true});
});
model.set({y: true});
});
test("nested `change` only fires once", 1, function() {
var model = new Backbone.Model();
model.on('change', function() {
ok(true);
model.change();
});
model.set({x: true});
});
test("no `'change'` event if no changes", function() {
var model = new Backbone.Model();
model.on('change', function() { ok(false); });
model.change();
});
test("nested `set` suring `'change'`", 3, function() {
var count = 0;
var model = new Backbone.Model();
model.on('change', function() {
switch(count++) {
case 0:
deepEqual(this.changedAttributes(), {x: true});
model.set({y: true});
break;
case 1:
deepEqual(this.changedAttributes(), {x: true, y: true});
model.set({z: true});
break;
case 2:
deepEqual(this.changedAttributes(), {x: true, y: true, z: true});
break;
default:
ok(false);
}
});
model.set({x: true});
});
});