diff --git a/backbone.js b/backbone.js index 7c33cb33..bddb41ff 100644 --- a/backbone.js +++ b/backbone.js @@ -101,7 +101,7 @@ Backbone.Model = function(attributes) { this._attributes = {}; attributes = attributes || {}; - this.set(attributes, true); + this.set(attributes, {silent : true}); this.cid = _.uniqueId('c'); this._formerAttributes = this.attributes(); }; @@ -116,6 +116,26 @@ // Has the item been changed since the last `changed` event? _changed : false, + // Return a copy of the model's `attributes` object. + attributes : function() { + return _.clone(this._attributes); + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url : function() { + var base = this.collection.url(); + if (this.isNew()) return base; + return base + '/' + this.id; + }, + + // String representation of the model. Override this to provide a nice way + // to print models to the console. + toString : function() { + return 'Model ' + this.id; + }, + // Create a new model with identical attributes to this one. clone : function() { return new (this.constructor)(this.attributes()); @@ -132,47 +152,9 @@ return !this.id; }, - // Call this method to fire manually fire a `changed` event for this model. - // Calling this will cause all objects observing the model to update. - changed : function() { - this.trigger('change', this); - this._formerAttributes = this.attributes(); - this._changed = false; - }, - - // Determine if the model has changed since the last `changed` event. - // If you specify an attribute name, determine if that attribute has changed. - hasChanged : function(attr) { - if (attr) return this._formerAttributes[attr] != this._attributes[attr]; - return this._changed; - }, - - // Get the previous value of an attribute, recorded at the time the last - // `changed` event was fired. - formerValue : function(attr) { - if (!attr || !this._formerAttributes) return null; - return this._formerAttributes[attr]; - }, - - // Get all of the attributes of the model at the time of the previous - // `changed` event. - formerAttributes : function() { - return this._formerAttributes; - }, - - // Return an object containing all the attributes that have changed, or false - // if there are no changed attributes. Useful for determining what parts of a - // view need to be updated and/or what attributes need to be persisted to - // the server. - changedAttributes : function(now) { - var old = this.formerAttributes(), now = now || this.attributes(), changed = false; - for (var attr in now) { - if (!_.isEqual(old[attr], now[attr])) { - changed = changed || {}; - changed[attr] = now[attr]; - } - } - return changed; + // Get the value of an attribute. + get : function(attr) { + return this._attributes[attr]; }, // Set a hash of model attributes on the object, firing `changed` unless you @@ -201,40 +183,61 @@ return this; }, - // Get the value of an attribute. - get : function(attr) { - return this._attributes[attr]; - }, - // Remove an attribute from the model, firing `changed` unless you choose to // silence it. unset : function(attr, options) { options || (options = {}); var value = this._attributes[attr]; delete this._attributes[attr]; - if (!options.silent) this.changed(); + if (!options.silent) { + this._changed = true; + this.trigger('change:' + attr); + this.changed(); + } return value; }, - // Return a copy of the model's attributes. - attributes : function() { - return _.clone(this._attributes); + // Call this method to fire manually fire a `changed` event for this model. + // Calling this will cause all objects observing the model to update. + changed : function() { + this.trigger('change', this); + this._formerAttributes = this.attributes(); + this._changed = false; }, - // Bind all methods in the list to the model. - bindAll : function() { - _.bindAll.apply(_, [this].concat(arguments)); + // Determine if the model has changed since the last `changed` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged : function(attr) { + if (attr) return this._formerAttributes[attr] != this._attributes[attr]; + return this._changed; }, - toString : function() { - return 'Model ' + this.id; + // Return an object containing all the attributes that have changed, or false + // if there are no changed attributes. Useful for determining what parts of a + // view need to be updated and/or what attributes need to be persisted to + // the server. + changedAttributes : function(now) { + var old = this.formerAttributes(), now = now || this.attributes(), changed = false; + for (var attr in now) { + if (!_.isEqual(old[attr], now[attr])) { + changed = changed || {}; + changed[attr] = now[attr]; + } + } + return changed; }, - // The URL of the model's representation on the server. - url : function() { - var base = this.collection.url(); - if (this.isNew()) return base; - return base + '/' + this.id; + // Get the previous value of an attribute, recorded at the time the last + // `changed` event was fired. + formerValue : function(attr) { + if (!attr || !this._formerAttributes) return null; + return this._formerAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `changed` event. + formerAttributes : function() { + return this._formerAttributes; }, // Set a hash of model attributes, and sync the model to the server. diff --git a/test/bindable.js b/test/bindable.js index 5f8b79af..2c53699a 100644 --- a/test/bindable.js +++ b/test/bindable.js @@ -2,41 +2,41 @@ $(document).ready(function() { module("Backbone bindable"); - test("bindable: bind and trigger", function() { - var obj = { counter: 0 }; - _.extend(obj,Backbone.Bindable); - obj.bind('event', function() { obj.counter += 1; }); - obj.trigger('event'); - equals(obj.counter,1,'counter should be incremented.'); - obj.trigger('event'); - obj.trigger('event'); - obj.trigger('event'); - obj.trigger('event'); - equals(obj.counter, 5, 'counter should be incremented five times.'); - }); + test("bindable: bind and trigger", function() { + var obj = { counter: 0 }; + _.extend(obj,Backbone.Bindable); + obj.bind('event', function() { obj.counter += 1; }); + obj.trigger('event'); + equals(obj.counter,1,'counter should be incremented.'); + obj.trigger('event'); + obj.trigger('event'); + obj.trigger('event'); + obj.trigger('event'); + equals(obj.counter, 5, 'counter should be incremented five times.'); + }); - test("bindable: bind, then unbind all functions", function() { - var obj = { counter: 0 }; - _.extend(obj,Backbone.Bindable); - var callback = function() { obj.counter += 1; }; - obj.bind('event', callback); - obj.trigger('event'); - obj.unbind('event'); - obj.trigger('event'); - equals(obj.counter, 1, 'counter should have only been incremented once.'); - }); + test("bindable: bind, then unbind all functions", function() { + var obj = { counter: 0 }; + _.extend(obj,Backbone.Bindable); + var callback = function() { obj.counter += 1; }; + obj.bind('event', callback); + obj.trigger('event'); + obj.unbind('event'); + obj.trigger('event'); + equals(obj.counter, 1, 'counter should have only been incremented once.'); + }); - test("bindable: bind two callbacks, unbind only one", function() { - var obj = { counterA: 0, counterB: 0 }; - _.extend(obj,Backbone.Bindable); - var callback = function() { obj.counterA += 1; }; - obj.bind('event', callback); - obj.bind('event', function() { obj.counterB += 1; }); - obj.trigger('event'); - obj.unbind('event', callback); - obj.trigger('event'); - equals(obj.counterA, 1, 'counterA should have only been incremented once.'); - equals(obj.counterB, 2, 'counterB should have been incremented twice.'); - }); + test("bindable: bind two callbacks, unbind only one", function() { + var obj = { counterA: 0, counterB: 0 }; + _.extend(obj,Backbone.Bindable); + var callback = function() { obj.counterA += 1; }; + obj.bind('event', callback); + obj.bind('event', function() { obj.counterB += 1; }); + obj.trigger('event'); + obj.unbind('event', callback); + obj.trigger('event'); + equals(obj.counterA, 1, 'counterA should have only been incremented once.'); + equals(obj.counterB, 2, 'counterB should have been incremented twice.'); + }); }); \ No newline at end of file diff --git a/test/model.js b/test/model.js index eb766127..9d5e2a4a 100644 --- a/test/model.js +++ b/test/model.js @@ -2,51 +2,121 @@ $(document).ready(function() { module("Backbone model"); + // Variable to catch the last request. + var lastRequest = null; + + // Stub out Backbone.request... + Backbone.request = function() { + lastRequest = _.toArray(arguments); + }; + + var attrs = { + id : '1-the-tempest', + title : "The Tempest", + author : "Bill Shakespeare", + length : 123 + }; + + var doc = new Backbone.Model(attrs); + + var klass = Backbone.Collection.extend({ + url : function() { return '/collection'; } + }); + + var collection = new klass(); + collection.add(doc); + + test("model: attributes", function() { + ok(doc.attributes() !== attrs, "Attributes are different objects."); + ok(_.isEqual(doc.attributes(), attrs), "but with identical contents."); + }); + + test("model: url", function() { + equals(doc.url(), '/collection/1-the-tempest'); + }); + + test("model: toString", function() { + equals(doc.toString(), 'Model 1-the-tempest'); + }); + test("model: clone", function() { - attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; - a = new Backbone.Model(attrs); - b = a.clone(); - equals(a.get('foo'), 1); - equals(a.get('bar'), 2); - equals(a.get('baz'), 3); - equals(b.get('foo'), a.get('foo'), "Foo should be the same on the clone."); - equals(b.get('bar'), a.get('bar'), "Bar should be the same on the clone."); - equals(b.get('baz'), a.get('baz'), "Baz should be the same on the clone."); + attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; + a = new Backbone.Model(attrs); + b = a.clone(); + equals(a.get('foo'), 1); + equals(a.get('bar'), 2); + equals(a.get('baz'), 3); + equals(b.get('foo'), a.get('foo'), "Foo should be the same on the clone."); + equals(b.get('bar'), a.get('bar'), "Bar should be the same on the clone."); + equals(b.get('baz'), a.get('baz'), "Baz should be the same on the clone."); + a.set({foo : 100}); + equals(a.get('foo'), 100); + equals(b.get('foo'), 1, "Changing a parent attribute does not change the clone."); }); test("model: isEqual", function() { - attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; - a = new Backbone.Model(attrs); - b = new Backbone.Model(attrs); - ok(a.isEqual(b), "a should equal b"); - c = new Backbone.Model({ 'foo': 1, 'bar': 2, 'baz': 3, 'qux': 4}); - ok(!a.isEqual(c), "a should not equal c"); + attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; + a = new Backbone.Model(attrs); + b = new Backbone.Model(attrs); + ok(a.isEqual(b), "a should equal b"); + c = new Backbone.Model({ 'foo': 1, 'bar': 2, 'baz': 3, 'qux': 4}); + ok(!a.isEqual(c), "a should not equal c"); }); test("model: isNew", function() { - attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; - a = new Backbone.Model(attrs); - ok(a.isNew(), "it should be new"); - attrs = { 'foo': 1, 'bar': 2, 'baz': 3, 'id': -5 }; - ok(a.isNew(), "any defined ID is legal, negative or positive"); + attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; + a = new Backbone.Model(attrs); + ok(a.isNew(), "it should be new"); + attrs = { 'foo': 1, 'bar': 2, 'baz': 3, 'id': -5 }; + ok(a.isNew(), "any defined ID is legal, negative or positive"); }); - test("model: set", function() { - attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; - a = new Backbone.Model(attrs); - var changeCount = 0; - a.bind("change", function() { changeCount += 1; }); - a.set({'foo': 2}); - 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(changeCount == 1, "Change count should NOT have incremented."); + test("model: get", function() { + equals(doc.get('title'), 'The Tempest'); + equals(doc.get('author'), 'Bill Shakespeare'); + }); - a.unset('foo'); - ok(a.get('foo')== null, "Foo should have changed"); - ok(changeCount == 2, "Change count should have incremented for unset."); + test("model: set and unset", function() { + attrs = { '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(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(changeCount == 1, "Change count should NOT have incremented."); + a.unset('foo'); + ok(a.get('foo')== null, "Foo should have changed"); + ok(changeCount == 2, "Change count should have incremented for unset."); + }); + + test("model: changed, hasChanged, changedAttributes, formerValue, formerAttributes", function() { + var model = new Backbone.Model({name : "Tim", age : 10}); + model.bind('change', function() { + ok(model.hasChanged('name'), 'name changed'); + ok(!model.hasChanged('age'), 'age did not'); + ok(_.isEqual(model.changedAttributes(), {name : 'Rob'}), 'changedAttributes returns the changed attrs'); + equals(model.formerValue('name'), 'Tim'); + ok(_.isEqual(model.formerAttributes(), {name : "Tim", age : 10}), 'formerAttributes is correct'); + }); + model.set({name : 'Rob'}, {silent : true}); + model.changed(); + equals(model.get('name'), 'Rob'); + }); + + test("model: save", function() { + doc.save({title : "Henry V"}); + equals(lastRequest[0], 'PUT'); + ok(_.isEqual(lastRequest[1], doc)); + }); + + test("model: destroy", function() { + doc.destroy(); + equals(lastRequest[0], 'DELETE'); + ok(_.isEqual(lastRequest[1], doc)); }); }); \ No newline at end of file