Merge pull request #893 from braddunbar/change

Fire `'change:attr'` from `change`
This commit is contained in:
Jeremy Ashkenas
2012-01-30 06:09:34 -08:00
3 changed files with 62 additions and 49 deletions

View File

@@ -163,10 +163,11 @@
this.attributes = {};
this._escapedAttributes = {};
this.cid = _.uniqueId('c');
this._changed = {};
if (!this.set(attributes, {silent: true})) {
throw new Error("Can't create an invalid model");
}
this._changed = false;
this._changed = {};
this._previousAttributes = _.clone(this.attributes);
this.initialize.apply(this, arguments);
};
@@ -174,9 +175,6 @@
// Attach all inheritable methods to the Model prototype.
_.extend(Backbone.Model.prototype, Backbone.Events, {
// Has the item been changed since the last `"change"` event?
_changed: false,
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
// CouchDB users may want to set this to `"_id"`.
idAttribute: 'id',
@@ -226,7 +224,6 @@
if (!attrs) return this;
if (attrs instanceof Backbone.Model) attrs = attrs.attributes;
if (options.unset) for (var attr in attrs) attrs[attr] = void 0;
var now = this.attributes, escaped = this._escapedAttributes;
// Run validation.
if (this.validate && !this._performValidation(attrs, options)) return false;
@@ -234,30 +231,26 @@
// Check for changes of `id`.
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
// We're about to start triggering change events.
var now = this.attributes;
var escaped = this._escapedAttributes;
var prev = this._previousAttributes || {};
var alreadyChanging = this._changing;
this._changing = true;
// Update attributes.
var changes = {};
for (attr in attrs) {
val = attrs[attr];
if (!_.isEqual(now[attr], val) || (options.unset && (attr in now))) {
delete escaped[attr];
this._changed = true;
changes[attr] = val;
}
if (!_.isEqual(now[attr], val)) delete escaped[attr];
options.unset ? delete now[attr] : now[attr] = val;
delete this._changed[attr];
if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
this._changed[attr] = val;
}
}
// Fire `change:attribute` events.
for (var attr in changes) {
if (!options.silent) this.trigger('change:' + attr, this, changes[attr], options);
}
// Fire the `"change"` event, if the model has been changed.
// Fire the `"change"` events, if the model has been changed.
if (!alreadyChanging) {
if (!options.silent && this._changed) this.change(options);
if (!options.silent && this.hasChanged()) this.change(options);
this._changing = false;
}
return this;
@@ -376,19 +369,23 @@
return this.id == null;
},
// Call this method to manually fire a `change` event for this model.
// Call this method to manually fire a `"change"` event for this model and
// a `"change:attribute"` event for each changed attribute.
// Calling this will cause all objects observing the model to update.
change: function(options) {
for (var attr in this._changed) {
this.trigger('change:' + attr, this, this._changed[attr], options);
}
this.trigger('change', this, options);
this._previousAttributes = _.clone(this.attributes);
this._changed = false;
this._changed = {};
},
// 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 (attr) return !_.isEqual(this._previousAttributes[attr], this.attributes[attr]);
return this._changed;
if (attr) return _.has(this._changed, attr);
return !_.isEmpty(this._changed);
},
// Return an object containing all the attributes that have changed, or
@@ -396,17 +393,8 @@
// parts of a 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 changed = false, old = this._previousAttributes;
for (var attr in now) {
if (_.isEqual(old[attr], now[attr])) continue;
(changed || (changed = {}))[attr] = now[attr];
}
for (var attr in old) {
if (!(attr in now)) (changed || (changed = {}))[attr] = void 0;
}
return changed;
if (!this.hasChanged()) return false;
return _.clone(this._changed);
},
// Get the previous value of an attribute, recorded at the time the last

View File

@@ -382,7 +382,7 @@
<p>
Backbone.js gives structure to your serious JavaScript web applications
by supplying <b>models</b> with key-value binding and custom events,
by supplying <b>models</b> with key-value binding and custom events,
<b>collections</b> with a rich API of enumerable functions,
<b>views</b> with declarative event handling, and connects it all to your
existing API over a RESTful JSON interface.
@@ -392,7 +392,7 @@
The project is <a href="http://github.com/documentcloud/backbone/">hosted on GitHub</a>,
and the <a href="docs/backbone.html">annotated source code</a> is available,
as well as an online <a href="test/test.html">test suite</a>,
an <a href="examples/todos/index.html">example application</a>,
an <a href="examples/todos/index.html">example application</a>,
a <a href="https://github.com/documentcloud/backbone/wiki/Tutorials%2C-blog-posts-and-example-sites">list of tutorials</a>
and a <a href="#examples">long list of real-world projects</a> that use Backbone.
</p>
@@ -1036,8 +1036,9 @@ ActiveRecord::Base.include_root_in_json = false
<p id="Model-change">
<b class="header">change</b><code>model.change()</code>
<br />
Manually trigger the <tt>"change"</tt> event.
If you've been passing <tt>{silent: true}</tt> to the <a href="#Model-set">set</a> function in order to
Manually trigger the <tt>"change"</tt> event and a <tt>"change:attribute"</tt>
event for each attribute that has changed. If you've been passing
<tt>{silent: true}</tt> to the <a href="#Model-set">set</a> function in order to
aggregate rapid changes to a model, you'll want to call <tt>model.change()</tt>
when you're all finished.
</p>
@@ -2964,28 +2965,28 @@ Inbox.messages.add(newMessage);
<p id="FAQ-rails">
<b class="header">Working with Rails</b>
<br />
Backbone.js was originally extracted from
Backbone.js was originally extracted from
<a href="http://www.documentcloud.org">a Rails application</a>; getting
your client-side (Backbone) Models to sync correctly with your server-side
(Rails) Models is painless, but there are still a few things to be aware of.
</p>
<p>
By default, Rails adds an extra layer of wrapping around the JSON representation
of models. You can disable this wrapping by setting:
</p>
<pre>
ActiveRecord::Base.include_root_in_json = false
ActiveRecord::Base.include_root_in_json = false
</pre>
<p>
... in your configuration. Otherwise, override
<a href="#Model-parse">parse</a> to pull model attributes out of the
wrapper. Similarly, Backbone PUTs and POSTs direct JSON representations
of models, where by default Rails expcects namespaced attributes. You can
have your controllers filter attributes directly from <tt>params</tt>, or
you can override <a href="#Model-toJSON">toJSON</a> in Backbone to add
... in your configuration. Otherwise, override
<a href="#Model-parse">parse</a> to pull model attributes out of the
wrapper. Similarly, Backbone PUTs and POSTs direct JSON representations
of models, where by default Rails expcects namespaced attributes. You can
have your controllers filter attributes directly from <tt>params</tt>, or
you can override <a href="#Model-toJSON">toJSON</a> in Backbone to add
the extra wrapping Rails expects.
</p>

View File

@@ -543,7 +543,7 @@ $(document).ready(function() {
test("unset fires change for undefined attributes", 1, function() {
var model = new Backbone.Model({x: undefined});
model.bind('change:x', function(){ ok(true); });
model.on('change:x', function(){ ok(true); });
model.unset('x');
});
@@ -552,4 +552,28 @@ $(document).ready(function() {
ok('x' in model.attributes);
});
test("change fires change:attr", 1, function() {
var model = new Backbone.Model({x: 1});
model.set({x: 2}, {silent: true});
model.on('change:x', function(){ ok(true); });
model.change();
});
test("hasChanged is false after original values are set", function() {
var model = new Backbone.Model({x: 1});
model.on('change:x', function(){ ok(false); });
model.set({x: 2}, {silent: true});
ok(model.hasChanged());
model.set({x: 1}, {silent: true});
ok(!model.hasChanged());
});
test("set/hasChanged object prototype props", function() {
var model = new Backbone.Model();
ok(!model.hasChanged('toString'));
model.set({toString: undefined});
model.unset('toString', {silent: true});
ok(model.hasChanged());
});
});