mirror of
https://github.com/jashkenas/backbone.git
synced 2026-01-20 04:18:11 -05:00
Fixes #959 - Silent changes fire 'change:attr'.
* Silent changes are tracked so `'change:attr'` can be fired next time `change` is called. * Pending changes are tracked to prevent infinite loops and accurately reflect nested changes.
This commit is contained in:
77
backbone.js
77
backbone.js
@@ -176,6 +176,8 @@
|
||||
this.cid = _.uniqueId('c');
|
||||
this.set(attributes, {silent: true});
|
||||
delete this._changed;
|
||||
delete this._silent;
|
||||
delete this._pending;
|
||||
this._previousAttributes = _.clone(this.attributes);
|
||||
this.initialize.apply(this, arguments);
|
||||
};
|
||||
@@ -183,6 +185,17 @@
|
||||
// Attach all inheritable methods to the Model prototype.
|
||||
_.extend(Backbone.Model.prototype, Backbone.Events, {
|
||||
|
||||
// A hash of attributes whose current and previous value differ.
|
||||
_changed: void 0,
|
||||
|
||||
// A hash of attributes that have silently changed since the last time
|
||||
// `change` was called. Will become pending attributes on the next call.
|
||||
_silent: void 0,
|
||||
|
||||
// A hash of attributes that have changed since the last `'change'` event
|
||||
// began.
|
||||
_pending: void 0,
|
||||
|
||||
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
|
||||
// CouchDB users may want to set this to `"_id"`.
|
||||
idAttribute: 'id',
|
||||
@@ -239,33 +252,36 @@
|
||||
// Check for changes of `id`.
|
||||
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
|
||||
|
||||
var changes = {};
|
||||
var now = this.attributes;
|
||||
var escaped = this._escapedAttributes;
|
||||
var prev = this._previousAttributes || {};
|
||||
var alreadySetting = this._setting;
|
||||
this._changed || (this._changed = {});
|
||||
this._setting = true;
|
||||
this._silent || (this._silent = {});
|
||||
this._pending || (this._pending = {});
|
||||
|
||||
// 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;
|
||||
// If the new and current value differ, record the change.
|
||||
if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) {
|
||||
delete escaped[attr];
|
||||
(options.silent ? this._silent : changes)[attr] = true;
|
||||
}
|
||||
delete this._changed[attr];
|
||||
// Update the current value.
|
||||
options.unset ? delete now[attr] : now[attr] = val;
|
||||
// If the new and previous value differ, record the change. If not,
|
||||
// then remove changes for this attribute.
|
||||
if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
|
||||
this._changed[attr] = val;
|
||||
this._changed[attr] = true;
|
||||
if (!options.silent) this._pending[attr] = true;
|
||||
} else {
|
||||
delete this._changed[attr];
|
||||
delete this._pending[attr];
|
||||
}
|
||||
}
|
||||
|
||||
// Fire the `"change"` events, if the model has been changed.
|
||||
if (!alreadySetting) {
|
||||
if (!options.silent && this.hasChanged()) this.change(options);
|
||||
this._setting = false;
|
||||
}
|
||||
// Fire the `"change"` events.
|
||||
if (!options.silent) this.change(_.extend({changes: changes}, options));
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -392,18 +408,26 @@
|
||||
// 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;
|
||||
options || (options = {});
|
||||
var changing = this._changing;
|
||||
this._changing = true;
|
||||
this._moreChanges = true;
|
||||
for (var attr in this._changed) {
|
||||
this.trigger('change:' + attr, this, this._changed[attr], options);
|
||||
// Silent changes become pending changes.
|
||||
this._pending = _.extend(this._pending || {}, this._silent);
|
||||
// Silent changes are triggered.
|
||||
var changes = _.extend({}, options.changes, this._silent);
|
||||
delete this._silent;
|
||||
for (var attr in changes) {
|
||||
this.trigger('change:' + attr, this, this.attributes[attr], options);
|
||||
}
|
||||
while (this._moreChanges) {
|
||||
this._moreChanges = false;
|
||||
if (changing) return this;
|
||||
// Continue firing `"change"` events while there are pending changes.
|
||||
while (!_.isEmpty(this._pending)) {
|
||||
delete this._pending;
|
||||
this.trigger('change', this, options);
|
||||
// Pending and silent changes still remain.
|
||||
this._changed = _.extend({}, this._pending, this._silent);
|
||||
this._previousAttributes = _.clone(this.attributes);
|
||||
}
|
||||
this._previousAttributes = _.clone(this.attributes);
|
||||
delete this._changed;
|
||||
this._changing = false;
|
||||
return this;
|
||||
},
|
||||
@@ -422,7 +446,12 @@
|
||||
// You can also pass an attributes object to diff against the model,
|
||||
// determining if there *would be* a change.
|
||||
changedAttributes: function(diff) {
|
||||
if (!diff) return this.hasChanged() ? _.clone(this._changed) : false;
|
||||
if (!diff) {
|
||||
if (!this.hasChanged()) return false;
|
||||
var changes = {};
|
||||
for (var attr in this._changed) changes[attr] = this.attributes[attr];
|
||||
return changes;
|
||||
}
|
||||
var val, changed = false, old = this._previousAttributes;
|
||||
for (var attr in diff) {
|
||||
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
|
||||
|
||||
103
test/model.js
103
test/model.js
@@ -517,8 +517,8 @@ $(document).ready(function() {
|
||||
}
|
||||
});
|
||||
|
||||
a = new A();
|
||||
b = new B({a: a});
|
||||
var a = new A();
|
||||
var b = new B({a: a});
|
||||
a.set({state: 'hello'});
|
||||
});
|
||||
|
||||
@@ -632,15 +632,21 @@ $(document).ready(function() {
|
||||
equal(changed, 1);
|
||||
});
|
||||
|
||||
test("nested `set` during `'change:attr'`", 1, function() {
|
||||
test("nested `set` during `'change:attr'`", function() {
|
||||
var events = [];
|
||||
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.on('all', function(event) { events.push(event); });
|
||||
model.on('change', function() {
|
||||
model.set({z: true}, {silent:true});
|
||||
});
|
||||
model.set({y: true});
|
||||
model.on('change:x', function() {
|
||||
model.set({y: true});
|
||||
});
|
||||
model.set({x: true});
|
||||
deepEqual(events, ['change:y', 'change:x', 'change']);
|
||||
events = [];
|
||||
model.change();
|
||||
deepEqual(events, ['change:z', 'change']);
|
||||
});
|
||||
|
||||
test("nested `change` only fires once", 1, function() {
|
||||
@@ -658,21 +664,24 @@ $(document).ready(function() {
|
||||
model.change();
|
||||
});
|
||||
|
||||
test("nested `set` suring `'change'`", 3, function() {
|
||||
test("nested `set` during `'change'`", 6, function() {
|
||||
var count = 0;
|
||||
var model = new Backbone.Model();
|
||||
model.on('change', function() {
|
||||
switch(count++) {
|
||||
case 0:
|
||||
deepEqual(this.changedAttributes(), {x: true});
|
||||
equal(model.previous('x'), undefined);
|
||||
model.set({y: true});
|
||||
break;
|
||||
case 1:
|
||||
deepEqual(this.changedAttributes(), {x: true, y: true});
|
||||
deepEqual(this.changedAttributes(), {y: true});
|
||||
equal(model.previous('x'), true);
|
||||
model.set({z: true});
|
||||
break;
|
||||
case 2:
|
||||
deepEqual(this.changedAttributes(), {x: true, y: true, z: true});
|
||||
deepEqual(this.changedAttributes(), {z: true});
|
||||
equal(model.previous('y'), true);
|
||||
break;
|
||||
default:
|
||||
ok(false);
|
||||
@@ -681,6 +690,76 @@ $(document).ready(function() {
|
||||
model.set({x: true});
|
||||
});
|
||||
|
||||
test("nested `'change'` with silent", 3, function() {
|
||||
var count = 0;
|
||||
var model = new Backbone.Model();
|
||||
model.on('change:y', function() { ok(true); });
|
||||
model.on('change', function() {
|
||||
switch(count++) {
|
||||
case 0:
|
||||
deepEqual(this.changedAttributes(), {x: true});
|
||||
model.set({y: true}, {silent: true});
|
||||
break;
|
||||
case 1:
|
||||
deepEqual(this.changedAttributes(), {y: true, z: true});
|
||||
break;
|
||||
default:
|
||||
ok(false);
|
||||
}
|
||||
});
|
||||
model.set({x: true});
|
||||
model.set({z: true});
|
||||
});
|
||||
|
||||
test("nested `'change:attr'` with silent", 1, function() {
|
||||
var model = new Backbone.Model();
|
||||
model.on('change:y', function(){ ok(true); });
|
||||
model.on('change', function() {
|
||||
model.set({y: true}, {silent: true});
|
||||
model.set({z: true});
|
||||
});
|
||||
model.set({x: true});
|
||||
});
|
||||
|
||||
test("multiple nested changes with silent", 1, function() {
|
||||
var model = new Backbone.Model();
|
||||
model.on('change:x', function() {
|
||||
model.set({y: 1}, {silent: true});
|
||||
model.set({y: 2});
|
||||
});
|
||||
model.on('change:y', function(model, val) {
|
||||
equal(val, 2);
|
||||
});
|
||||
model.set({x: true});
|
||||
model.change();
|
||||
});
|
||||
|
||||
test("multiple nested changes with silent", function() {
|
||||
var changes = [];
|
||||
var model = new Backbone.Model();
|
||||
model.on('change:b', function(model, val) { changes.push(val); });
|
||||
model.on('change', function() {
|
||||
model.set({b: 1});
|
||||
model.set({b: 2}, {silent: true});
|
||||
});
|
||||
model.set({b: 0});
|
||||
deepEqual(changes, [0, 1, 1]);
|
||||
model.change();
|
||||
deepEqual(changes, [0, 1, 1, 2, 1]);
|
||||
});
|
||||
|
||||
test("nested set multiple times", 1, function() {
|
||||
var model = new Backbone.Model();
|
||||
model.on('change:b', function() {
|
||||
ok(true);
|
||||
});
|
||||
model.on('change:a', function() {
|
||||
model.set({b: true});
|
||||
model.set({b: true});
|
||||
});
|
||||
model.set({a: true});
|
||||
});
|
||||
|
||||
test("Backbone.wrapError triggers `'error'`", 12, function() {
|
||||
var resp = {};
|
||||
var options = {};
|
||||
|
||||
Reference in New Issue
Block a user