mirror of
https://github.com/jashkenas/backbone.git
synced 2026-04-30 03:00:06 -04:00
Merge pull request #982 from braddunbar/change
Fixes #959 - Silent changes fire `'change:attr'`.
This commit is contained in:
84
backbone.js
84
backbone.js
@@ -174,8 +174,14 @@
|
||||
this.attributes = {};
|
||||
this._escapedAttributes = {};
|
||||
this.cid = _.uniqueId('c');
|
||||
this.changed = {};
|
||||
this._silent = {};
|
||||
this._pending = {};
|
||||
this.set(attributes, {silent: true});
|
||||
delete this._changed;
|
||||
// Reset change tracking.
|
||||
this.changed = {};
|
||||
this._silent = {};
|
||||
this._pending = {};
|
||||
this._previousAttributes = _.clone(this.attributes);
|
||||
this.initialize.apply(this, arguments);
|
||||
};
|
||||
@@ -183,6 +189,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: null,
|
||||
|
||||
// A hash of attributes that have silently changed since the last time
|
||||
// `change` was called. Will become pending attributes on the next call.
|
||||
_silent: null,
|
||||
|
||||
// A hash of attributes that have changed since the last `'change'` event
|
||||
// began.
|
||||
_pending: null,
|
||||
|
||||
// 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 +256,33 @@
|
||||
// Check for changes of `id`.
|
||||
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
|
||||
|
||||
var changes = options.changes = {};
|
||||
var now = this.attributes;
|
||||
var escaped = this._escapedAttributes;
|
||||
var prev = this._previousAttributes || {};
|
||||
var alreadySetting = this._setting;
|
||||
this._changed || (this._changed = {});
|
||||
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;
|
||||
// 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] = val;
|
||||
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(options);
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -392,18 +409,29 @@
|
||||
// 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.
|
||||
for (var attr in this._silent) this._pending[attr] = true;
|
||||
// Silent changes are triggered.
|
||||
var changes = _.extend({}, options.changes, this._silent);
|
||||
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)) {
|
||||
this._pending = {};
|
||||
this.trigger('change', this, options);
|
||||
// Pending and silent changes still remain.
|
||||
for (var attr in this.changed) {
|
||||
if (this._pending[attr] || this._silent[attr]) continue;
|
||||
delete this.changed[attr];
|
||||
}
|
||||
this._previousAttributes = _.clone(this.attributes);
|
||||
}
|
||||
this._previousAttributes = _.clone(this.attributes);
|
||||
delete this._changed;
|
||||
this._changing = false;
|
||||
return this;
|
||||
},
|
||||
@@ -411,8 +439,8 @@
|
||||
// Determine if the model has changed since the last `"change"` event.
|
||||
// If you specify an attribute name, determine if that attribute has changed.
|
||||
hasChanged: function(attr) {
|
||||
if (!arguments.length) return !_.isEmpty(this._changed);
|
||||
return this._changed && _.has(this._changed, attr);
|
||||
if (!arguments.length) return !_.isEmpty(this.changed);
|
||||
return _.has(this.changed, attr);
|
||||
},
|
||||
|
||||
// Return an object containing all the attributes that have changed, or
|
||||
@@ -422,7 +450,7 @@
|
||||
// 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) return this.hasChanged() ? _.clone(this.changed) : false;
|
||||
var val, changed = false, old = this._previousAttributes;
|
||||
for (var attr in diff) {
|
||||
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
|
||||
|
||||
34
index.html
34
index.html
@@ -238,6 +238,7 @@
|
||||
<li>– <a href="#Model-idAttribute">idAttribute</a></li>
|
||||
<li>– <a href="#Model-cid">cid</a></li>
|
||||
<li>– <a href="#Model-attributes">attributes</a></li>
|
||||
<li>– <a href="#Model-changed">changed</a></li>
|
||||
<li>– <a href="#Model-defaults">defaults</a></li>
|
||||
<li>– <a href="#Model-toJSON">toJSON</a></li>
|
||||
<li>– <a href="#Model-fetch">fetch</a></li>
|
||||
@@ -857,6 +858,17 @@ alert("Cake id: " + cake.id);
|
||||
attributes, use <a href="#Model-toJSON">toJSON</a> instead.
|
||||
</p>
|
||||
|
||||
<p id="Model-changed">
|
||||
<b class="header">changed</b><code>model.changed</code>
|
||||
<br />
|
||||
The <b>changed</b> property is the internal hash containing all the attributes
|
||||
that have changed since the last <tt>"change"</tt> event was triggered.
|
||||
Please do not update <b>changed</b> directly. It's state is maintained internally
|
||||
by <a href="#Model-set">set</a> and <a href="#Model-change">change</a>.
|
||||
A copy of <b>changed</b> can be acquired from
|
||||
<a href="#Model-changedAttributes">changedAttributes</a>.
|
||||
</p>
|
||||
|
||||
<p id="Model-defaults">
|
||||
<b class="header">defaults</b><code>model.defaults or model.defaults()</code>
|
||||
<br />
|
||||
@@ -2392,18 +2404,18 @@ var model = localBackbone.Model.extend(...);
|
||||
<img src="docs/images/wunderkit.png" alt="Wunderkit" class="example_image" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="examples-khan-academy">Khan Academy</h2>
|
||||
|
||||
<p>
|
||||
<a href="http://www.khanacademy.org">Khan Academy</a> is on a mission to
|
||||
provide a free world-class education to anyone anywhere. With thousands of
|
||||
videos, hundreds of JavaScript-driven exercises, and big plans for the
|
||||
future, Khan Academy uses Backbone to keep frontend code modular and organized.
|
||||
<a href="https://khanacademy.kilnhg.com/Code/Website/Group/stable/Files/javascript/profile-package?rev=tip">User profiles</a>
|
||||
and <a href="https://khanacademy.kilnhg.com/Code/Website/Group/stable/File/javascript/shared-package/goals.js?rev=tip">goal setting</a>
|
||||
are implemented with Backbone, jQuery and Handlebars, and most new feature
|
||||
work is being pushed to the client side, greatly increasing the quality of
|
||||
<a href="http://www.khanacademy.org">Khan Academy</a> is on a mission to
|
||||
provide a free world-class education to anyone anywhere. With thousands of
|
||||
videos, hundreds of JavaScript-driven exercises, and big plans for the
|
||||
future, Khan Academy uses Backbone to keep frontend code modular and organized.
|
||||
<a href="https://khanacademy.kilnhg.com/Code/Website/Group/stable/Files/javascript/profile-package?rev=tip">User profiles</a>
|
||||
and <a href="https://khanacademy.kilnhg.com/Code/Website/Group/stable/File/javascript/shared-package/goals.js?rev=tip">goal setting</a>
|
||||
are implemented with Backbone, jQuery and Handlebars, and most new feature
|
||||
work is being pushed to the client side, greatly increasing the quality of
|
||||
<a href="https://github.com/Khan/khan-api/">the API</a>.
|
||||
</p>
|
||||
|
||||
@@ -2835,14 +2847,14 @@ var model = localBackbone.Model.extend(...);
|
||||
<img src="docs/images/ducksboard.png" alt="Ducksboard" class="example_image" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="examples-picklive">Picklive</h2>
|
||||
|
||||
<p>
|
||||
<a href="http://twitter.com/timruffles">Tim Ruffles</a> and <a href="http://twitter.com/timparker">Tim Parker</a>
|
||||
created the game client for <a href="https://free.picklive.com">Picklive</a>, a real-time fantasy-soccer game.
|
||||
The client is written in <a href="http://coffeescript.org">CoffeeScript</a>, organised into
|
||||
modules via <a href="http://requirejs.org">require.js</a>, tested with
|
||||
modules via <a href="http://requirejs.org">require.js</a>, tested with
|
||||
<a href="http://code.google.com/p/js-test-driver">jsTestDriver</a> and uses
|
||||
<a href="http://mustache.github.com">Mustache.js</a> for templating. Backbone's model and sync layer separation
|
||||
manages the complexity of mixed polling and web-sockets based synchronisation.
|
||||
|
||||
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