From 573d491f8d9b449d4a0ed0b61a3e2dc55bfed22f Mon Sep 17 00:00:00 2001 From: Joe Germuska Date: Sat, 2 Oct 2010 17:16:17 -0500 Subject: [PATCH 01/29] fix bug in 'add' and 'refresh' --- backbone.js | 18 ++++++++++++------ test/collection.js | 19 +++++++++++++++++++ test/model.js | 11 ++++++++++- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/backbone.js b/backbone.js index 437f6373..68275786 100644 --- a/backbone.js +++ b/backbone.js @@ -284,9 +284,16 @@ // Provides a standard collection class for our sets of models, ordered // or unordered. If a `comparator` is specified, the Collection will maintain // its models in sort order, as they're added and removed. - Backbone.Collection = function(options) { + Backbone.Collection = function(models,options) { + if (options && options.comparator) { + this.comparator = options.comparator; + delete options.comparator; + } this._boundOnModelEvent = _.bind(this._onModelEvent, this); this._initialize(); + if (models) { + this.refresh(models,true); + } }; // Define the Collection's inheritable methods. @@ -340,7 +347,7 @@ if (already) throw new Error(["Can't add the same model to a set twice", already.id]); this._byId[model.id] = model; this._byCid[model.cid] = model; - var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length - 1; + var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length; this.models.splice(index, 0, model); model.bind('all', this._boundOnModelEvent); this.length++; @@ -375,10 +382,9 @@ refresh : function(models, silent) { models = models || []; if (models[0] && !(models[0] instanceof Backbone.Model)) { - for (var i = 0, l = models.length; i < l; i++) { - models[i].collection = this; - models[i] = new this.model(models[i]); - } + _.each(models, _.bind(function(model,i) { + model.collection = this; + }, this)); } this._initialize(); this.add(models, true); diff --git a/test/collection.js b/test/collection.js index e69de29b..bc97695b 100644 --- a/test/collection.js +++ b/test/collection.js @@ -0,0 +1,19 @@ +$(document).ready(function() { + + module("Backbone collections"); + + test("collections: simple", function() { + a = new Backbone.Model({label: 'a'}); + b = new Backbone.Model({label: 'b'}); + c = new Backbone.Model({label: 'c'}); + d = new Backbone.Model({label: 'd'}); + col = new Backbone.Collection([a,b,c,d]); + equals(col.first(),a, "a should be first"); + equals(col.last(),d, "d should be last"); + }); + + test("collections: sorted", function() { + + }); + +}); \ No newline at end of file diff --git a/test/model.js b/test/model.js index a836bc95..2804a5c8 100644 --- a/test/model.js +++ b/test/model.js @@ -27,7 +27,9 @@ $(document).ready(function() { test("model: isNew", function() { attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; a = new Backbone.Model(attrs); - ok(a.isNew()); + 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() { @@ -38,6 +40,13 @@ $(document).ready(function() { 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."); }); From a6293310fda959cc6e8a152297b0281de17cfe4f Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Mon, 4 Oct 2010 15:20:15 -0400 Subject: [PATCH 02/29] jslint --- backbone.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backbone.js b/backbone.js index 2ac3f1e0..0c05cde6 100644 --- a/backbone.js +++ b/backbone.js @@ -516,8 +516,8 @@ return child; }; + // `Backbone.request`... Backbone.request = function(type, model, success, error) { - $.ajax({ url : model.url(), type : type, @@ -526,6 +526,6 @@ success : success, error : error }); - } + }; })(); \ No newline at end of file From ddd87c0e436fc6a1159df0d332b93229ce2cc946 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Mon, 4 Oct 2010 15:43:47 -0400 Subject: [PATCH 03/29] docs --- backbone.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/backbone.js b/backbone.js index 44fd0ca1..f1ea2ca3 100644 --- a/backbone.js +++ b/backbone.js @@ -1,8 +1,7 @@ -// Backbone.js -// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. -// Backbone may be freely distributed under the terms of the MIT license. -// For all details and documentation: -// http://documentcloud.github.com/backbone +// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the terms of the MIT license. +// For all details and documentation: +// http://documentcloud.github.com/backbone (function(){ @@ -38,7 +37,13 @@ // ----------------- // A module that can be mixed in to any object in order to provide it with - // custom events. + // custom events. You may `bind` or `unbind` a callback function to an event; + // `trigger`-ing an event fires all callbacks in succession. + // + // _.extend(object, Backbone.Bindable); + // object.bind('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // Backbone.Bindable = { // Bind an event, specified by a string name, `ev`, to a `callback` function. @@ -73,7 +78,7 @@ return this; }, - // Trigger an event, firing all bound callbacks + // Trigger an event, firing all bound callbacks. trigger : function(ev) { var calls = this._callbacks; for (var i = 0; i < 2; i++) { From a29dd5c5114b09e6704e1db2536330fb97afacf9 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Mon, 4 Oct 2010 16:02:39 -0400 Subject: [PATCH 04/29] fixing refresh for attributes only --- backbone.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/backbone.js b/backbone.js index f1ea2ca3..a4484f22 100644 --- a/backbone.js +++ b/backbone.js @@ -18,12 +18,11 @@ (typeof exports !== 'undefined' ? exports : this).Backbone = Backbone; // Helper function to correctly set up the prototype chain, for subclasses. + // Similar to `goog.inherits`, but uses a hash of prototype properties and + // static properties to be extended. var inherits = function(parent, protoProps, classProps) { - if (protoProps.hasOwnProperty('constructor')) { - child = protoProps.constructor; - } else { - child = function(){ return parent.apply(this, arguments); }; - } + var child = protoProps.hasOwnProperty('constructor') ? protoProps.constructor : + function(){ return parent.apply(this, arguments); }; var ctor = function(){}; ctor.prototype = parent.prototype; child.prototype = new ctor(); @@ -379,10 +378,13 @@ // any `added` or `removed` events. Fires `refreshed` when finished. refresh : function(models, silent) { models = models || []; + var collection = this; if (models[0] && !(models[0] instanceof Backbone.Model)) { - _.each(models, _.bind(function(model,i) { + models = _.map(models, function(attrs, i) { + var model = new collection.model(attrs); model.collection = this; - }, this)); + return model; + }); } this._initialize(); this.add(models, true); From 8eea2caab2ce6b3f2c0fc2e79dec494400794ea3 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 5 Oct 2010 10:59:00 -0400 Subject: [PATCH 05/29] first working version of create --- backbone.js | 56 +++++++++++++++++++++++++---------------------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/backbone.js b/backbone.js index a4484f22..f7419718 100644 --- a/backbone.js +++ b/backbone.js @@ -105,28 +105,6 @@ this._formerAttributes = this.attributes(); }; - // Create a model on the server and add it to the set. - // When the server returns a JSON representation of the model, we update it - // on the client. - Backbone.Model.create = function(attributes, options) { - options || (options = {}); - var model = new this(attributes); - $.ajax({ - url : model.set.resource, - type : 'POST', - data : {model : JSON.stringify(model.attributes())}, - dataType : 'json', - success : function(resp) { - model.set(resp.model); - if (options.success) return options.success(model, resp); - }, - error : function(resp) { - if (options.error) options.error(model, resp); - } - }); - return model; - }; - // Attach all inheritable methods to the Model prototype. _.extend(Backbone.Model.prototype, Backbone.Bindable, { @@ -211,8 +189,11 @@ var val = attrs[attr]; if (val === '') val = null; if (!_.isEqual(now[attr], val)) { - if (!options.silent) this._changed = true; now[attr] = val; + if (!options.silent) { + this._changed = true; + this.trigger('change:' + attr); + } } } if (!options.silent && this._changed) this.changed(); @@ -248,14 +229,16 @@ return 'Model ' + this.id; }, - // Return the URL used to {save,delete} + // The URL of the model's representation on the server. url : function() { - if (!this.id) throw new Error(this.toString() + " has no id."); - return this.collection.url() + '/' + this.id; + var base = this.collection.url(); + if (this.isNew()) return base; + return base + '/' + this.id; }, // Set a hash of model attributes, and sync the model to the server. save : function(attrs, options) { + attrs || (attrs = {}); options || (options = {}); this.set(attrs, options); var model = this; @@ -263,7 +246,8 @@ model.set(resp.model); if (options.success) options.success(model, resp); }; - Backbone.request('PUT', this, success, options.error); + var method = this.isNew() ? 'POST' : 'PUT'; + Backbone.request(method, this, success, options.error); return this; }, @@ -344,6 +328,7 @@ if (already) throw new Error(["Can't add the same model to a set twice", already.id]); this._byId[model.id] = model; this._byCid[model.cid] = model; + model.collection = this; var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length; this.models.splice(index, 0, model); model.bind('all', this._boundOnModelEvent); @@ -366,6 +351,7 @@ if (!model) return null; delete this._byId[model.id]; delete this._byCid[model.cid]; + delete model.collection; this.models.splice(this.indexOf(model), 1); model.unbind('all', this._boundOnModelEvent); this.length--; @@ -381,9 +367,7 @@ var collection = this; if (models[0] && !(models[0] instanceof Backbone.Model)) { models = _.map(models, function(attrs, i) { - var model = new collection.model(attrs); - model.collection = this; - return model; + return new collection.model(attrs); }); } this._initialize(); @@ -391,6 +375,18 @@ if (!silent) this.trigger('refresh'); }, + // Create a new instance of a model in this collection. + create : function(model, options) { + options || (options = {}); + if (!(model instanceof Backbone.Model)) model = new this.model(model); + model.collection = this; + var success = function(model, resp) { + model.collection.add(model); + if (options.success) options.success(model, resp); + }; + model.save(null, {success : success, error : options.error}); + }, + // Force the set to re-sort itself. You don't need to call this under normal // circumstances, as the set will maintain sort order as each item is added. sort : function(silent) { From 1074f247cd0ac301fdd29729e1ae4f038765839c Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 5 Oct 2010 11:58:29 -0400 Subject: [PATCH 06/29] Adding Backbone.Collection#fetch --- backbone.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/backbone.js b/backbone.js index f7419718..8db4e171 100644 --- a/backbone.js +++ b/backbone.js @@ -66,6 +66,7 @@ calls[ev] = []; } else { var list = calls[ev]; + if (!list) return this; for (var i = 0, l = list.length; i < l; i++) { if (callback === list[i]) { list.splice(i, 1); @@ -265,16 +266,14 @@ // Provides a standard collection class for our sets of models, ordered // or unordered. If a `comparator` is specified, the Collection will maintain // its models in sort order, as they're added and removed. - Backbone.Collection = function(models,options) { + Backbone.Collection = function(models, options) { if (options && options.comparator) { this.comparator = options.comparator; delete options.comparator; } this._boundOnModelEvent = _.bind(this._onModelEvent, this); this._initialize(); - if (models) { - this.refresh(models,true); - } + if (models) this.refresh(models,true); }; // Define the Collection's inheritable methods. @@ -375,6 +374,18 @@ if (!silent) this.trigger('refresh'); }, + // Fetch the default set of models for this collection, refreshing the + // collection. + fetch : function(options) { + options || (options = {}); + var collection = this; + var success = function(resp) { + collection.refresh(resp.models); + if (options.success) options.success(collection, resp); + }; + Backbone.request('GET', this, success, options.error); + }, + // Create a new instance of a model in this collection. create : function(model, options) { options || (options = {}); @@ -527,10 +538,11 @@ // `Backbone.request`... Backbone.request = function(type, model, success, error) { + var data = model.attributes ? {model : JSON.stringify(model.attributes())} : {}; $.ajax({ url : model.url(), type : type, - data : {model : JSON.stringify(model.attributes())}, + data : data, dataType : 'json', success : success, error : error From 2ca43eaa5f9a8b9c7fb0fa51053c3c49c8be254d Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 5 Oct 2010 15:51:23 -0400 Subject: [PATCH 07/29] fleshing out destroy --- backbone.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index 8db4e171..990eca32 100644 --- a/backbone.js +++ b/backbone.js @@ -254,7 +254,13 @@ // Destroy this model on the server. destroy : function(options) { - Backbone.request('DELETE', this, options.success, options.error); + options || (options = {}); + var model = this; + var success = function(resp) { + if (model.collection) model.collection.remove(model); + if (options.success) options.success(model, resp); + }; + Backbone.request('DELETE', this, success, options.error); return this; } From 3190a5a7f49b177b0177aacdc6fabc2525eef4da Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 5 Oct 2010 16:24:25 -0400 Subject: [PATCH 08/29] Backbone.View option handling tweak. --- backbone.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index 990eca32..148b1ec1 100644 --- a/backbone.js +++ b/backbone.js @@ -449,7 +449,7 @@ Backbone.View = function(options) { this.modes = {}; - this.configure(options || {}); + this.configure(options); if (this.options.el) { this.el = this.options.el; } else { @@ -474,6 +474,7 @@ tagName : 'div', configure : function(options) { + options || (options = {}); if (this.options) options = _.extend({}, this.options, options); if (options.model) this.model = options.model; if (options.collection) this.collection = options.collection; From c63f01ee129c14afaf31deec322209f05195732c Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 6 Oct 2010 11:28:43 -0400 Subject: [PATCH 09/29] creating a model should set its attribute --- backbone.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backbone.js b/backbone.js index 148b1ec1..7c33cb33 100644 --- a/backbone.js +++ b/backbone.js @@ -398,6 +398,7 @@ if (!(model instanceof Backbone.Model)) model = new this.model(model); model.collection = this; var success = function(model, resp) { + model.set(resp.model); model.collection.add(model); if (options.success) options.success(model, resp); }; From 03c1c70f3fbd8d0f2b5bc5befa299d60733db2da Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 6 Oct 2010 11:33:31 -0400 Subject: [PATCH 10/29] linting test/bindable --- test/bindable.js | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/test/bindable.js b/test/bindable.js index 465a6767..5f8b79af 100644 --- a/test/bindable.js +++ b/test/bindable.js @@ -3,40 +3,40 @@ $(document).ready(function() { module("Backbone bindable"); test("bindable: bind and trigger", function() { - var obj = { counter: 0 } + var obj = { counter: 0 }; _.extend(obj,Backbone.Bindable); - obj.bind('foo',function() { obj.counter += 1; }); - obj.trigger('foo'); + obj.bind('event', function() { obj.counter += 1; }); + obj.trigger('event'); equals(obj.counter,1,'counter should be incremented.'); - obj.trigger('foo'); - obj.trigger('foo'); - obj.trigger('foo'); - obj.trigger('foo'); - equals(obj.counter,5,'counter should be incremented five times.'); + 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 } + var obj = { counter: 0 }; _.extend(obj,Backbone.Bindable); - var callback = function() { obj.counter += 1; } - obj.bind('foo', callback); - obj.trigger('foo'); - obj.unbind('foo'); - obj.trigger('foo'); - equals(obj.counter,1,'counter should have only been incremented once.') + 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 } + var obj = { counterA: 0, counterB: 0 }; _.extend(obj,Backbone.Bindable); var callback = function() { obj.counterA += 1; }; - obj.bind('foo', callback); - obj.bind('foo', function() { obj.counterB += 1 }); - obj.trigger('foo'); - obj.unbind('foo', callback); - obj.trigger('foo'); - equals(obj.counterA,1,'counterA should have only been incremented once.') - equals(obj.counterB,2,'counterB should have been incremented twice.') + 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 From 71969d367b916465df4566ba1bd76fb76c959840 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 6 Oct 2010 11:35:14 -0400 Subject: [PATCH 11/29] linting the remainder of the tests. --- test/collection.js | 6 +++--- test/model.js | 33 ++++++++++++++++----------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/test/collection.js b/test/collection.js index bc97695b..ccb91595 100644 --- a/test/collection.js +++ b/test/collection.js @@ -8,12 +8,12 @@ $(document).ready(function() { c = new Backbone.Model({label: 'c'}); d = new Backbone.Model({label: 'd'}); col = new Backbone.Collection([a,b,c,d]); - equals(col.first(),a, "a should be first"); - equals(col.last(),d, "d should be last"); + equals(col.first(), a, "a should be first"); + equals(col.last(), d, "d should be last"); }); test("collections: sorted", function() { - + }); }); \ No newline at end of file diff --git a/test/model.js b/test/model.js index 2804a5c8..eb766127 100644 --- a/test/model.js +++ b/test/model.js @@ -6,23 +6,22 @@ $(document).ready(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."); + 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."); }); 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"); + 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"); - - }) + ok(!a.isEqual(c), "a should not equal c"); + }); test("model: isNew", function() { attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; @@ -30,24 +29,24 @@ $(document).ready(function() { 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.bind("change", function() { changeCount += 1; }); a.set({'foo': 2}); - ok(a.get('foo')==2, "Foo should have changed."); + 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(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(a.get('foo')== null, "Foo should have changed"); ok(changeCount == 2, "Change count should have incremented for unset."); - + }); }); \ No newline at end of file From 16149c7c376eca25fc85db638e2f81de32f71124 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 6 Oct 2010 12:23:22 -0400 Subject: [PATCH 12/29] Wrapped up the model tests. --- backbone.js | 125 +++++++++++++++++++++--------------------- test/bindable.js | 68 +++++++++++------------ test/model.js | 138 +++++++++++++++++++++++++++++++++++------------ 3 files changed, 202 insertions(+), 129 deletions(-) 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 From 6009b8d1ab3673cd25db6c988960d6bd6fc90ded Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 6 Oct 2010 13:18:32 -0400 Subject: [PATCH 13/29] more tests for collection, 0 is a valid id. --- backbone.js | 22 ++++++++++------------ test/collection.js | 41 ++++++++++++++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/backbone.js b/backbone.js index bddb41ff..c0fa4774 100644 --- a/backbone.js +++ b/backbone.js @@ -85,7 +85,7 @@ var list = calls && calls[i ? 'all' : ev]; if (!list) continue; for (var j = 0, l = list.length; j < l; j++) { - list[j].apply(this, arguments); + list[j].apply(this, _.rest(arguments)); } } return this; @@ -164,7 +164,7 @@ if (!attrs) return this; attrs = attrs._attributes || attrs; var now = this._attributes; - if (attrs.id) { + if ('id' in attrs) { this.id = attrs.id; if (this.collection) this.resource = this.collection.resource + '/' + this.id; } @@ -280,7 +280,7 @@ this.comparator = options.comparator; delete options.comparator; } - this._boundOnModelEvent = _.bind(this._onModelEvent, this); + this._boundOnModelChange = _.bind(this._onModelChange, this); this._initialize(); if (models) this.refresh(models,true); }; @@ -339,7 +339,7 @@ model.collection = this; var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length; this.models.splice(index, 0, model); - model.bind('all', this._boundOnModelEvent); + model.bind('change', this._boundOnModelChange); this.length++; if (!silent) this.trigger('add', model); return model; @@ -361,7 +361,7 @@ delete this._byCid[model.cid]; delete model.collection; this.models.splice(this.indexOf(model), 1); - model.unbind('all', this._boundOnModelEvent); + model.unbind('change', this._boundOnModelChange); this.length--; if (!silent) this.trigger('remove', model); return model; @@ -418,14 +418,12 @@ // Internal method called every time a model in the set fires an event. // Sets need to update their indexes when models change ids. - _onModelEvent : function(ev, model) { - if (ev == 'change') { - if (model.hasChanged('id')) { - delete this._byId[model.formerValue('id')]; - this._byId[model.id] = model; - } - this.trigger('change', model); + _onModelChange : function(model) { + if (model.hasChanged('id')) { + delete this._byId[model.formerValue('id')]; + this._byId[model.id] = model; } + this.trigger('change', model); }, // Inspect. diff --git a/test/collection.js b/test/collection.js index ccb91595..2502b5e9 100644 --- a/test/collection.js +++ b/test/collection.js @@ -2,18 +2,45 @@ $(document).ready(function() { module("Backbone collections"); - test("collections: simple", function() { - a = new Backbone.Model({label: 'a'}); - b = new Backbone.Model({label: 'b'}); - c = new Backbone.Model({label: 'c'}); - d = new Backbone.Model({label: 'd'}); - col = new Backbone.Collection([a,b,c,d]); + var a = new Backbone.Model({id: 4, label: 'a'}); + var b = new Backbone.Model({id: 3, label: 'b'}); + var c = new Backbone.Model({id: 2, label: 'c'}); + var d = new Backbone.Model({id: 1, label: 'd'}); + var col = new Backbone.Collection([a,b,c,d]); + + test("collections: simple and sorted", function() { equals(col.first(), a, "a should be first"); equals(col.last(), d, "d should be last"); + col.comparator = function(model) { return model.id; }; + col.sort(); + equals(col.first(), d, "d should be first"); + equals(col.last(), a, "a should be last"); + equals(col.length, 4); }); - test("collections: sorted", function() { + test("collections: get, getByCid", function() { + equals(col.get(1), d); + equals(col.get(3), b); + equals(col.getByCid(col.first().cid), col.first()); + }); + test("collections: getIds, getCids", function() { + equals(col.getIds().sort().join(' '), '1 2 3 4'); + equals(col.getCids().sort().join(' '), 'c1 c2 c3 c4'); + }); + + test("collections: at", function() { + equals(col.at(2), b); + }); + + test("collections: add", function() { + var added = null; + col.bind('add', function(model){ added = model.get('label'); }); + var e = new Backbone.Model({id: 0, label : 'e'}); + col.add(e); + equals(added, 'e'); + equals(col.length, 5); + equals(col.first(), e); }); }); \ No newline at end of file From 463ce3e62d4c80e20cf7ee15734dfa2e2bb1e10b Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 6 Oct 2010 13:29:36 -0400 Subject: [PATCH 14/29] Making zero numeric ids more possible --- backbone.js | 38 +++++++++++++++++++++----------------- test/collection.js | 16 +++++++++++++--- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/backbone.js b/backbone.js index c0fa4774..91caaeb1 100644 --- a/backbone.js +++ b/backbone.js @@ -299,7 +299,7 @@ // Get a model from the set by id. get : function(id) { - return id && this._byId[id.id || id]; + return id && this._byId[id.id != null ? id.id : id]; }, // Get a model from the set by client id. @@ -322,16 +322,17 @@ return this.models[index]; }, - // Add a model, or list of models to the set. Pass silent to avoid firing - // the `added` event for every new model. - add : function(models, silent) { - if (!_.isArray(models)) return this._add(models, silent); - for (var i=0; i Date: Wed, 6 Oct 2010 13:36:28 -0400 Subject: [PATCH 15/29] more tests, returning 'this', when performing side effects on collections --- backbone.js | 7 +++++-- test/collection.js | 26 ++++++++++++++++++++++++++ test/model.js | 2 +- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/backbone.js b/backbone.js index 91caaeb1..c775e4e9 100644 --- a/backbone.js +++ b/backbone.js @@ -382,8 +382,9 @@ }); } this._initialize(); - this.add(models, true); + this.add(models, {silent: true}); if (!options.silent) this.trigger('refresh'); + return this; }, // Fetch the default set of models for this collection, refreshing the @@ -396,6 +397,7 @@ if (options.success) options.success(collection, resp); }; Backbone.request('GET', this, success, options.error); + return this; }, // Create a new instance of a model in this collection. @@ -408,7 +410,7 @@ model.collection.add(model); if (options.success) options.success(model, resp); }; - model.save(null, {success : success, error : options.error}); + return model.save(null, {success : success, error : options.error}); }, // Force the set to re-sort itself. You don't need to call this under normal @@ -418,6 +420,7 @@ if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); this.models = this.sortBy(this.comparator); if (!options.silent) this.trigger('refresh'); + return this; }, // Internal method called every time a model in the set fires an event. diff --git a/test/collection.js b/test/collection.js index 5e684728..96ddd044 100644 --- a/test/collection.js +++ b/test/collection.js @@ -2,6 +2,12 @@ $(document).ready(function() { module("Backbone collections"); + window.lastRequest = null; + + Backbone.request = function() { + lastRequest = _.toArray(arguments); + }; + var a = new Backbone.Model({id: 4, label: 'a'}); var b = new Backbone.Model({id: 3, label: 'b'}); var c = new Backbone.Model({id: 2, label: 'c'}); @@ -53,4 +59,24 @@ $(document).ready(function() { equals(col.first(), d); }); + test("collections: refresh", function() { + var refreshed = 0; + var models = col.models; + col.bind('refresh', function() { refreshed += 1; }); + col.refresh([]); + equals(refreshed, 1); + equals(col.length, 0); + equals(col.last(), null); + col.refresh(models); + equals(refreshed, 2); + equals(col.length, 4); + equals(col.last(), a); + }); + + test("collections: fetch", function() { + col.fetch(); + equals(lastRequest[0], 'GET'); + equals(lastRequest[1], col); + }); + }); diff --git a/test/model.js b/test/model.js index 9d5e2a4a..2e544bd1 100644 --- a/test/model.js +++ b/test/model.js @@ -3,7 +3,7 @@ $(document).ready(function() { module("Backbone model"); // Variable to catch the last request. - var lastRequest = null; + window.lastRequest = null; // Stub out Backbone.request... Backbone.request = function() { From b1277b92580cc08cd7d99212202dd0e42c8dc5e6 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 6 Oct 2010 13:41:37 -0400 Subject: [PATCH 16/29] defaulting Backbone.Collection#model to be Backbone.Model --- backbone.js | 7 +++++-- test/collection.js | 8 ++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/backbone.js b/backbone.js index c775e4e9..9206dfcd 100644 --- a/backbone.js +++ b/backbone.js @@ -276,7 +276,8 @@ // or unordered. If a `comparator` is specified, the Collection will maintain // its models in sort order, as they're added and removed. Backbone.Collection = function(models, options) { - if (options && options.comparator) { + options || (options = {}); + if (options.comparator) { this.comparator = options.comparator; delete options.comparator; } @@ -288,9 +289,11 @@ // Define the Collection's inheritable methods. _.extend(Backbone.Collection.prototype, Backbone.Bindable, { + model : Backbone.Model, + // Initialize or re-initialize all internal state. Called when the // collection is refreshed. - _initialize : function() { + _initialize : function(options) { this.length = 0; this.models = []; this._byId = {}; diff --git a/test/collection.js b/test/collection.js index 96ddd044..58e70305 100644 --- a/test/collection.js +++ b/test/collection.js @@ -79,4 +79,12 @@ $(document).ready(function() { equals(lastRequest[1], col); }); + test("collections: create", function() { + var model = col.create({label: 'f'}); + equals(lastRequest[0], 'POST'); + equals(lastRequest[1], model); + equals(model.get('label'), 'f'); + equals(model.collection, col); + }); + }); From 655ab7fa1c4cd46e35068d75ba19f6eee2662a03 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 6 Oct 2010 13:45:05 -0400 Subject: [PATCH 17/29] finished initial round of tests for collection --- backbone.js | 40 ++++++++++++++++++++-------------------- test/collection.js | 6 +++++- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/backbone.js b/backbone.js index 9206dfcd..4a863abd 100644 --- a/backbone.js +++ b/backbone.js @@ -291,13 +291,9 @@ model : Backbone.Model, - // Initialize or re-initialize all internal state. Called when the - // collection is refreshed. - _initialize : function(options) { - this.length = 0; - this.models = []; - this._byId = {}; - this._byCid = {}; + // Override this function to get convenient logging in the console. + toString : function() { + return 'Collection (' + this.length + " models)"; }, // Get a model from the set by id. @@ -372,6 +368,16 @@ return model; }, + // Force the set to re-sort itself. You don't need to call this under normal + // circumstances, as the set will maintain sort order as each item is added. + sort : function(options) { + options || (options = {}); + if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); + this.models = this.sortBy(this.comparator); + if (!options.silent) this.trigger('refresh'); + return this; + }, + // When you have more items than you want to add or remove individually, // you can refresh the entire set with a new list of models, without firing // any `added` or `removed` events. Fires `refreshed` when finished. @@ -416,14 +422,13 @@ return model.save(null, {success : success, error : options.error}); }, - // Force the set to re-sort itself. You don't need to call this under normal - // circumstances, as the set will maintain sort order as each item is added. - sort : function(options) { - options || (options = {}); - if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); - this.models = this.sortBy(this.comparator); - if (!options.silent) this.trigger('refresh'); - return this; + // Initialize or re-initialize all internal state. Called when the + // collection is refreshed. + _initialize : function(options) { + this.length = 0; + this.models = []; + this._byId = {}; + this._byCid = {}; }, // Internal method called every time a model in the set fires an event. @@ -434,11 +439,6 @@ this._byId[model.id] = model; } this.trigger('change', model); - }, - - // Inspect. - toString : function() { - return 'Set (' + this.length + " models)"; } }); diff --git a/test/collection.js b/test/collection.js index 58e70305..9a026acc 100644 --- a/test/collection.js +++ b/test/collection.js @@ -15,7 +15,7 @@ $(document).ready(function() { var e = null; var col = window.col = new Backbone.Collection([a,b,c,d]); - test("collections: simple and sorted", function() { + test("collections: new and sort", function() { equals(col.first(), a, "a should be first"); equals(col.last(), d, "d should be last"); col.comparator = function(model) { return model.id; }; @@ -25,6 +25,10 @@ $(document).ready(function() { equals(col.length, 4); }); + test("collections: toString", function() { + equals(col.toString(), 'Collection (4 models)'); + }); + test("collections: get, getByCid", function() { equals(col.get(1), d); equals(col.get(3), b); From d343aa5fec0d66e4a48b888da65fc3dc5c3b32a3 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 6 Oct 2010 13:52:25 -0400 Subject: [PATCH 18/29] testing a handful of the underscore methods on collections --- backbone.js | 13 +++++++++---- test/collection.js | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/backbone.js b/backbone.js index 4a863abd..26cc75f0 100644 --- a/backbone.js +++ b/backbone.js @@ -306,6 +306,11 @@ return cid && this._byCid[cid.cid || cid]; }, + // Get the model at the given index. + at: function(index) { + return this.models[index]; + }, + // What are the ids for every model in the set? getIds : function() { return _.keys(this._byId); @@ -316,9 +321,9 @@ return _.keys(this._byCid); }, - // Get the model at the given index. - at: function(index) { - return this.models[index]; + // Pluck an attribute from each model in the collection. + pluck : function(attr) { + return _.map(this.models, function(model){ return model.get(attr); }); }, // Add a model, or list of models to the set. Pass **silent** to avoid @@ -445,7 +450,7 @@ // Underscore methods that we want to implement on the Collection. var methods = ['each', 'map', 'reduce', 'reduceRight', 'detect', 'select', - 'reject', 'all', 'any', 'include', 'invoke', 'pluck', 'max', 'min', 'sortBy', + 'reject', 'all', 'any', 'include', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty']; diff --git a/test/collection.js b/test/collection.js index 9a026acc..cfd27a66 100644 --- a/test/collection.js +++ b/test/collection.js @@ -44,6 +44,10 @@ $(document).ready(function() { equals(col.at(2), b); }); + test("collections: pluck", function() { + equals(col.pluck('label').join(' '), 'd c b a'); + }); + test("collections: add", function() { var added = null; col.bind('add', function(model){ added = model.get('label'); }); @@ -91,4 +95,19 @@ $(document).ready(function() { equals(model.collection, col); }); + test("collections: Underscore methods", function() { + equals(col.map(function(model){ return model.get('label'); }).join(' '), 'd c b a'); + equals(col.any(function(model){ return model.id === 100; }), false); + equals(col.any(function(model){ return model.id === 1; }), true); + equals(col.indexOf(b), 2); + equals(col.size(), 4); + equals(col.rest().length, 3); + ok(!_.include(col.rest()), a); + ok(!_.include(col.rest()), d); + ok(!col.isEmpty()); + ok(!_.include(col.without(d)), d); + equals(col.max(function(model){ return model.id; }).id, 4); + equals(col.min(function(model){ return model.id; }).id, 1); + }); + }); From 81f82944cf99cbdac7ab06f9a145aa5f8103cce6 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 6 Oct 2010 14:43:24 -0400 Subject: [PATCH 19/29] finished commenting Views and Backbone.request -- regenerated docs. --- backbone.js | 86 +++++---- docs/backbone.html | 434 +++++++++++++++++++++++++-------------------- docs/docco.css | 8 +- 3 files changed, 295 insertions(+), 233 deletions(-) diff --git a/backbone.js b/backbone.js index 26cc75f0..c4113108 100644 --- a/backbone.js +++ b/backbone.js @@ -464,52 +464,51 @@ // Backbone.View // ------------- + // Creating a Backbone.View creates its intial element outside of the DOM, + // if an existing element is not provided... Backbone.View = function(options) { this.modes = {}; - this.configure(options); + this._initialize(options || {}); if (this.options.el) { this.el = this.options.el; } else { var attrs = {}; if (this.id) attrs.id = this.id; - if (this.className) attrs['class'] = this.className; + if (this.className) attrs.className = this.className; this.el = this.make(this.tagName, attrs); } return this; }; - // Set up all interitable view properties and methods. + // jQuery lookup, scoped to DOM elements within the current view. + // This should be prefered to global jQuery lookups, if you're dealing with + // a specific view. + var jQueryScoped = function(selector) { + return $(selector, this.el); + }; + + // Set up all interitable **Backbone.View** properties and methods. _.extend(Backbone.View.prototype, { - el : null, - model : null, - modes : null, - id : null, - className : null, - callbacks : null, - options : null, - tagName : 'div', + // The default tagName of a View's element is "div". + tagName : 'div', - configure : function(options) { - options || (options = {}); - if (this.options) options = _.extend({}, this.options, options); - if (options.model) this.model = options.model; - if (options.collection) this.collection = options.collection; - if (options.id) this.id = options.id; - if (options.className) this.className = options.className; - this.options = options; - }, + // Attach the jQuery function as the `$` and `jQuery` properties. + $ : jQueryScoped, + jQuery : jQueryScoped, + // **render** is the core function that your view should override, in order + // to populate its element (`this.el`), with the appropriate HTML. The + // convention is for **render** to always return `this`. render : function() { return this; }, - // jQuery lookup, scoped to the current view. - $ : function(selector) { - return $(selector, this.el); - }, - - // Quick-create a dom element with attributes. + // For small amounts of DOM Elements, where a full-blown template isn't + // needed, use **make** to manufacture elements, one at a time. + // + // var el = this.make('li', {'class': 'row'}, this.model.get('title')); + // make : function(tagName, attributes, content) { var el = document.createElement(tagName); if (attributes) $(el).attr(attributes); @@ -519,8 +518,8 @@ // Makes the view enter a mode. Modes have both a 'mode' and a 'group', // and are mutually exclusive with any other modes in the same group. - // Setting will update the view's modes hash, as well as set an HTML className - // of [mode]_[group] on the view's element. Convenient way to swap styles + // Setting will update the view's modes hash, as well as set an HTML class + // of *[mode]_[group]* on the view's element. Convenient way to swap styles // and behavior. setMode : function(mode, group) { if (this.modes[group] == mode) return; @@ -530,10 +529,11 @@ // Set callbacks, where this.callbacks is a hash of // {selector.event_name, callback_name} - // pairs. Callbacks will be bound to the view, with 'this' set properly. - // Passing a selector of 'el' binds to the view's root element. - // Change events are not delegated through the view because IE does not bubble - // change events at all. + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses jQuery event delegation for efficiency. + // Passing a selector of `el` binds to the view's root element. + // Change events are not delegated through the view because IE does not + // bubble change events at all. setCallbacks : function(callbacks) { $(this.el).unbind(); if (!(callbacks || (callbacks = this.callbacks))) return this; @@ -549,6 +549,18 @@ } } return this; + }, + + // Performs the initial configuration of a View with a set of options. + // Keys with special meaning *(model, collection, id, className)*, are + // attatched directly to the view. + _initialize : function(options) { + if (this.options) options = _.extend({}, this.options, options); + if (options.model) this.model = options.model; + if (options.collection) this.collection = options.collection; + if (options.id) this.id = options.id; + if (options.className) this.className = options.className; + this.options = options; } }); @@ -560,7 +572,15 @@ return child; }; - // `Backbone.request`... + // Override this function to change the manner in which Backbone persists + // models to the server. You will be passed the type of request, and the + // model in question. By default, uses jQuery to make a RESTful Ajax request + // to the model's `url()`. Some possible customizations could be: + // + // * Use `setTimeout` to batch rapid-fire updates into a single request. + // * Send up the models as XML instead of JSON. + // * Persist models via WebSockets instead of Ajax. + // Backbone.request = function(type, model, success, error) { var data = model.attributes ? {model : JSON.stringify(model.attributes())} : {}; $.ajax({ diff --git a/docs/backbone.html b/docs/backbone.html index 1e875184..2b94b022 100644 --- a/docs/backbone.html +++ b/docs/backbone.html @@ -1,13 +1,11 @@ - backbone.js

backbone.js

#

Backbone.js -(c) 2010 Jeremy Ashkenas, DocumentCloud Inc. -Backbone may be freely distributed under the terms of the MIT license. -For all details and documentation: -http://documentcloud.github.com/backbone

(function(){
#

Initial Setup

#

The top-level namespace.

  var Backbone = {};
#

Keep the version in sync with package.json.

  Backbone.VERSION = '0.1.0';
#

Export for both CommonJS and the Browser.

  (typeof exports !== 'undefined' ? exports : this).Backbone = Backbone;
#

Helper function to correctly set up the prototype chain, for subclasses.

  var inherits = function(parent, protoProps, classProps) {
-    if (protoProps.hasOwnProperty('constructor')) {
-      child = protoProps.constructor;
-    } else {
-      child = function(){ return parent.apply(this, arguments); };
-    }
+      backbone.js           

backbone.js

(c) 2010 Jeremy Ashkenas, DocumentCloud Inc. + Backbone may be freely distributed under the terms of the MIT license. + For all details and documentation: + http://documentcloud.github.com/backbone

(function(){

Initial Setup

The top-level namespace.

  var Backbone = {};

Keep the version in sync with package.json.

  Backbone.VERSION = '0.1.0';

Export for both CommonJS and the Browser.

  (typeof exports !== 'undefined' ? exports : this).Backbone = Backbone;

Helper function to correctly set up the prototype chain, for subclasses. +Similar to goog.inherits, but uses a hash of prototype properties and +static properties to be extended.

  var inherits = function(parent, protoProps, classProps) {
+    var child = protoProps.hasOwnProperty('constructor') ? protoProps.constructor :
+                function(){ return parent.apply(this, arguments); };
     var ctor = function(){};
     ctor.prototype = parent.prototype;
     child.prototype = new ctor();
@@ -15,14 +13,19 @@ http://documentcloud.github.com/backbone

if (classProps) _.extend(child, classProps); child.prototype.constructor = child; return child; - };
#

Backbone.Bindable

#

A module that can be mixed in to any object in order to provide it with -custom events.

  Backbone.Bindable = {
#

Bind an event, specified by a string name, ev, to a callback function. + };

Backbone.Bindable

A module that can be mixed in to any object in order to provide it with +custom events. You may bind or unbind a callback function to an event; +trigger-ing an event fires all callbacks in succession.

+ +

_.extend(object, Backbone.Bindable); + object.bind('expand', function(){ alert('expanded'); }); + object.trigger('expand');

  Backbone.Bindable = {

Bind an event, specified by a string name, ev, to a callback function. Passing "all" will bind the callback to all events fired.

    bind : function(ev, callback) {
       var calls = this._callbacks || (this._callbacks = {});
       var list  = this._callbacks[ev] || (this._callbacks[ev] = []);
       list.push(callback);
       return this;
-    },
#

Remove one or many callbacks. If callback is null, removes all + },

Remove one or many callbacks. If callback is null, removes all callbacks for the event. If ev is null, removes all bound callbacks for all events.

    unbind : function(ev, callback) {
       var calls;
@@ -33,6 +36,7 @@ for all events.

calls[ev] = []; } else { var list = calls[ev]; + if (!list) return this; for (var i = 0, l = list.length; i < l; i++) { if (callback === list[i]) { list.splice(i, 1); @@ -42,69 +46,90 @@ for all events.

} } return this; - },
#

Trigger an event, firing all bound callbacks

    trigger : function(ev) {
+    },

Trigger an event, firing all bound callbacks.

    trigger : function(ev) {
       var calls = this._callbacks;
       for (var i = 0; i < 2; i++) {
         var list = calls && calls[i ? 'all' : ev];
         if (!list) continue;
         for (var j = 0, l = list.length; j < l; j++) {
-          list[j].apply(this, arguments);
+          list[j].apply(this, _.rest(arguments));
         }
       }
       return this;
     }
 
-  };
#

Backbone.Model

#

Create a new model, with defined attributes. + };

Backbone.Model

Create a new model, with defined attributes. If you do not specify the id, a negative id will be assigned for you.

  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();
-  };
#

Create a model on the server and add it to the set. -When the server returns a JSON representation of the model, we update it -on the client.

  Backbone.Model.create = function(attributes, options) {
-    options || (options = {});
-    var model = new this(attributes);
-    $.ajax({
-      url       : model.set.resource,
-      type      : 'POST',
-      data      : {model : JSON.stringify(model.attributes())},
-      dataType  : 'json',
-      success   : function(resp) {
-        model.set(resp.model);
-        if (options.success) return options.success(model, resp);
-      },
-      error     : function(resp) {
-        if (options.error) options.error(model, resp);
-      }
-    });
-    return model;
-  };
#

Attach all inheritable methods to the Model prototype.

  _.extend(Backbone.Model.prototype, Backbone.Bindable, {
#

A snapshot of the model's previous attributes, taken immediately -after the last changed event was fired.

    _formerAttributes : null,
#

Has the item been changed since the last changed event?

    _changed : false,
#

Create a new model with identical attributes to this one.

    clone : function() {
+  };

Attach all inheritable methods to the Model prototype.

  _.extend(Backbone.Model.prototype, Backbone.Bindable, {

A snapshot of the model's previous attributes, taken immediately +after the last changed event was fired.

    _formerAttributes : null,

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());
-    },
#

Are this model's attributes identical to another model?

    isEqual : function(other) {
+    },

Are this model's attributes identical to another model?

    isEqual : function(other) {
       return other && _.isEqual(this._attributes, other._attributes);
-    },
#

A model is new if it has never been saved to the server, and has a negative + },

A model is new if it has never been saved to the server, and has a negative ID.

    isNew : function() {
       return !this.id;
-    },
#

Call this method to fire manually fire a changed event for this model. + },

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 +choose to silence it.

    set : function(attrs, options) {
+      options || (options = {});
+      if (!attrs) return this;
+      attrs = attrs._attributes || attrs;
+      var now = this._attributes;
+      if ('id' in attrs) {
+        this.id = attrs.id;
+        if (this.collection) this.resource = this.collection.resource + '/' + this.id;
+      }
+      for (var attr in attrs) {
+        var val = attrs[attr];
+        if (val === '') val = null;
+        if (!_.isEqual(now[attr], val)) {
+          now[attr] = val;
+          if (!options.silent) {
+            this._changed = true;
+            this.trigger('change:' + attr);
+          }
+        }
+      }
+      if (!options.silent && this._changed) this.changed();
+      return this;
+    },

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 = true;
+        this.trigger('change:' + attr);
+        this.changed();
+      }
+      return value;
+    },

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. + },

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 + },

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) {
@@ -116,47 +141,15 @@ the server.

} } return changed; - },
#

Set a hash of model attributes on the object, firing changed unless you -choose to silence it.

    set : function(attrs, options) {
-      options || (options = {});
-      if (!attrs) return this;
-      attrs = attrs._attributes || attrs;
-      var now = this._attributes;
-      if (attrs.id) {
-        this.id = attrs.id;
-        if (this.collection) this.resource = this.collection.resource + '/' + this.id;
-      }
-      for (var attr in attrs) {
-        var val = attrs[attr];
-        if (val === '') val = null;
-        if (!_.isEqual(now[attr], val)) {
-          if (!options.silent) this._changed = true;
-          now[attr] = val;
-        }
-      }
-      if (!options.silent && this._changed) this.changed();
-      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();
-      return value;
-    },
#

Return a copy of the model's attributes.

    attributes : function() {
-      return _.clone(this._attributes);
-    },
#

Bind all methods in the list to the model.

    bindAll : function() {
-      _.bindAll.apply(_, [this].concat(arguments));
-    },
-
-    toString : function() {
-      return 'Model ' + this.id;
-    },
#

Return the URL used to {save,delete}

    url : function() {
-      if (!this.id) throw new Error(this.toString() + " has no id.");
-      return this.collection.url() + '/' + this.id;
-    },
#

Set a hash of model attributes, and sync the model to the server.

    save : function(attrs, options) {
+    },

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.

    save : function(attrs, options) {
+      attrs   || (attrs = {});
       options || (options = {});
       this.set(attrs, options);
       var model = this;
@@ -164,158 +157,191 @@ silence it.

model.set(resp.model); if (options.success) options.success(model, resp); }; - Backbone.request('PUT', this, success, options.error); + var method = this.isNew() ? 'POST' : 'PUT'; + Backbone.request(method, this, success, options.error); return this; - },
#

Destroy this model on the server.

    destroy : function(options) {
-      Backbone.request('DELETE', this, options.success, options.error);
+    },

Destroy this model on the server.

    destroy : function(options) {
+      options || (options = {});
+      var model = this;
+      var success = function(resp) {
+        if (model.collection) model.collection.remove(model);
+        if (options.success) options.success(model, resp);
+      };
+      Backbone.request('DELETE', this, success, options.error);
       return this;
     }
 
-  });
#

Backbone.Collection

#

Provides a standard collection class for our sets of models, ordered + });

Backbone.Collection

Provides a standard collection class for our sets of models, ordered or unordered. If a comparator is specified, the Collection will maintain -its models in sort order, as they're added and removed.

  Backbone.Collection = function(options) {
-    this._boundOnModelEvent = _.bind(this._onModelEvent, this);
+its models in sort order, as they're added and removed.

  Backbone.Collection = function(models, options) {
+    options || (options = {});
+    if (options.comparator) {
+      this.comparator = options.comparator;
+      delete options.comparator;
+    }
+    this._boundOnModelChange = _.bind(this._onModelChange, this);
     this._initialize();
-  };
#

Define the Collection's inheritable methods.

  _.extend(Backbone.Collection.prototype, Backbone.Bindable, {
#

Initialize or re-initialize all internal state. Called when the -collection is refreshed.

    _initialize : function() {
-      this.length = 0;
-      this.models = [];
-      this._byId = {};
-      this._byCid = {};
-    },
#

Get a model from the set by id.

    get : function(id) {
-      return id && this._byId[id.id || id];
-    },
#

Get a model from the set by client id.

    getByCid : function(cid) {
+    if (models) this.refresh(models,true);
+  };

Define the Collection's inheritable methods.

  _.extend(Backbone.Collection.prototype, Backbone.Bindable, {
+
+    model : Backbone.Model,

Override this function to get convenient logging in the console.

    toString : function() {
+      return 'Collection (' + this.length + " models)";
+    },

Get a model from the set by id.

    get : function(id) {
+      return id && this._byId[id.id != null ? id.id : id];
+    },

Get a model from the set by client id.

    getByCid : function(cid) {
       return cid && this._byCid[cid.cid || cid];
-    },
#

What are the ids for every model in the set?

    getIds : function() {
-      return _.keys(this._byId);
-    },
#

What are the client ids for every model in the set?

    getCids : function() {
-      return _.keys(this._byCid);
-    },
#

Get the model at the given index.

    at: function(index) {
+    },

Get the model at the given index.

    at: function(index) {
       return this.models[index];
-    },
#

Add a model, or list of models to the set. Pass silent to avoid firing -the added event for every new model.

    add : function(models, silent) {
-      if (!_.isArray(models)) return this._add(models, silent);
-      for (var i=0; i<models.length; i++) this._add(models[i], silent);
+    },

What are the ids for every model in the set?

    getIds : function() {
+      return _.keys(this._byId);
+    },

What are the client ids for every model in the set?

    getCids : function() {
+      return _.keys(this._byCid);
+    },

Pluck an attribute from each model in the collection.

    pluck : function(attr) {
+      return _.map(this.models, function(model){ return model.get(attr); });
+    },

Add a model, or list of models to the set. Pass silent to avoid +firing the added event for every new model.

    add : function(models, options) {
+      if (!_.isArray(models)) return this._add(models, options);
+      for (var i=0; i<models.length; i++) this._add(models[i], options);
       return models;
-    },
#

Internal implementation of adding a single model to the set.

    _add : function(model, silent) {
+    },

Internal implementation of adding a single model to the set.

    _add : function(model, options) {
+      options || (options = {});
       var already = this.get(model);
       if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
       this._byId[model.id] = model;
       this._byCid[model.cid] = model;
-      var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length - 1;
+      model.collection = this;
+      var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length;
       this.models.splice(index, 0, model);
-      model.bind('all', this._boundOnModelEvent);
+      model.bind('change', this._boundOnModelChange);
       this.length++;
-      if (!silent) this.trigger('add', model);
+      if (!options.silent) this.trigger('add', model);
       return model;
-    },
#

Remove a model, or a list of models from the set. Pass silent to avoid -firing the removed event for every model removed.

    remove : function(models, silent) {
-      if (!_.isArray(models)) return this._remove(models, silent);
-      for (var i=0; i<models.length; i++) this._remove(models[i], silent);
+    },

Remove a model, or a list of models from the set. Pass silent to avoid +firing the removed event for every model removed.

    remove : function(models, options) {
+      if (!_.isArray(models)) return this._remove(models, options);
+      for (var i=0; i<models.length; i++) this._remove(models[i], options);
       return models;
-    },
#

Internal implementation of removing a single model from the set.

    _remove : function(model, silent) {
+    },

Internal implementation of removing a single model from the set.

    _remove : function(model, options) {
+      options || (options = {});
       model = this.get(model);
       if (!model) return null;
       delete this._byId[model.id];
       delete this._byCid[model.cid];
+      delete model.collection;
       this.models.splice(this.indexOf(model), 1);
-      model.unbind('all', this._boundOnModelEvent);
+      model.unbind('change', this._boundOnModelChange);
       this.length--;
-      if (!silent) this.trigger('remove', model);
+      if (!options.silent) this.trigger('remove', model);
       return model;
-    },
#

When you have more items than you want to add or remove individually, -you can refresh the entire set with a new list of models, without firing -any added or removed events. Fires refreshed when finished.

    refresh : function(models, silent) {
-      models = models || [];
-      if (models[0] && !(models[0] instanceof Backbone.Model)) {
-        for (var i = 0, l = models.length; i < l; i++) {
-          models[i].collection = this;
-          models[i] = new this.model(models[i]);
-        }
-      }
-      this._initialize();
-      this.add(models, true);
-      if (!silent) this.trigger('refresh');
-    },
#

Force the set to re-sort itself. You don't need to call this under normal -circumstances, as the set will maintain sort order as each item is added.

    sort : function(silent) {
+    },

Force the set to re-sort itself. You don't need to call this under normal +circumstances, as the set will maintain sort order as each item is added.

    sort : function(options) {
+      options || (options = {});
       if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
       this.models = this.sortBy(this.comparator);
-      if (!silent) this.trigger('refresh');
-    },
#

Internal method called every time a model in the set fires an event. -Sets need to update their indexes when models change ids.

    _onModelEvent : function(ev, model) {
-      if (ev == 'change') {
-        if (model.hasChanged('id')) {
-          delete this._byId[model.formerValue('id')];
-          this._byId[model.id] = model;
-        }
-        this.trigger('change', model);
+      if (!options.silent) this.trigger('refresh');
+      return this;
+    },

When you have more items than you want to add or remove individually, +you can refresh the entire set with a new list of models, without firing +any added or removed events. Fires refreshed when finished.

    refresh : function(models, options) {
+      options || (options = {});
+      models = models || [];
+      var collection = this;
+      if (models[0] && !(models[0] instanceof Backbone.Model)) {
+        models = _.map(models, function(attrs, i) {
+          return new collection.model(attrs);
+        });
       }
-    },
#

Inspect.

    toString : function() {
-      return 'Set (' + this.length + " models)";
+      this._initialize();
+      this.add(models, {silent: true});
+      if (!options.silent) this.trigger('refresh');
+      return this;
+    },

Fetch the default set of models for this collection, refreshing the +collection.

    fetch : function(options) {
+      options || (options = {});
+      var collection = this;
+      var success = function(resp) {
+        collection.refresh(resp.models);
+        if (options.success) options.success(collection, resp);
+      };
+      Backbone.request('GET', this, success, options.error);
+      return this;
+    },

Create a new instance of a model in this collection.

    create : function(model, options) {
+      options || (options = {});
+      if (!(model instanceof Backbone.Model)) model = new this.model(model);
+      model.collection = this;
+      var success = function(model, resp) {
+        model.set(resp.model);
+        model.collection.add(model);
+        if (options.success) options.success(model, resp);
+      };
+      return model.save(null, {success : success, error : options.error});
+    },

Initialize or re-initialize all internal state. Called when the +collection is refreshed.

    _initialize : function(options) {
+      this.length = 0;
+      this.models = [];
+      this._byId = {};
+      this._byCid = {};
+    },

Internal method called every time a model in the set fires an event. +Sets need to update their indexes when models change ids.

    _onModelChange : function(model) {
+      if (model.hasChanged('id')) {
+        delete this._byId[model.formerValue('id')];
+        this._byId[model.id] = model;
+      }
+      this.trigger('change', model);
     }
 
-  });
#

Underscore methods that we want to implement on the Collection.

  var methods = ['each', 'map', 'reduce', 'reduceRight', 'detect', 'select',
-    'reject', 'all', 'any', 'include', 'invoke', 'pluck', 'max', 'min', 'sortBy',
+  });

Underscore methods that we want to implement on the Collection.

  var methods = ['each', 'map', 'reduce', 'reduceRight', 'detect', 'select',
+    'reject', 'all', 'any', 'include', 'invoke', 'max', 'min', 'sortBy',
     'sortedIndex', 'toArray', 'size', 'first', 'rest', 'last', 'without',
-    'indexOf', 'lastIndexOf', 'isEmpty'];
#

Mix in each Underscore method as a proxy to Collection#models.

  _.each(methods, function(method) {
+    'indexOf', 'lastIndexOf', 'isEmpty'];

Mix in each Underscore method as a proxy to Collection#models.

  _.each(methods, function(method) {
     Backbone.Collection.prototype[method] = function() {
       return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
     };
-  });
#

Backbone.View

  Backbone.View = function(options) {
+  });

Backbone.View

Creating a Backbone.View creates its intial element outside of the DOM, +if an existing element is not provided...

  Backbone.View = function(options) {
     this.modes = {};
-    this.configure(options || {});
+    this._initialize(options || {});
     if (this.options.el) {
       this.el = this.options.el;
     } else {
       var attrs = {};
       if (this.id) attrs.id = this.id;
-      if (this.className) attrs['class'] = this.className;
+      if (this.className) attrs.className = this.className;
       this.el = this.make(this.tagName, attrs);
     }
     return this;
-  };
#

Set up all interitable view properties and methods.

  _.extend(Backbone.View.prototype, {
-
-    el        : null,
-    model     : null,
-    modes     : null,
-    id        : null,
-    className : null,
-    callbacks : null,
-    options   : null,
-    tagName   : 'div',
-
-    configure : function(options) {
-      if (this.options) options = _.extend({}, this.options, options);
-      if (options.model)      this.model      = options.model;
-      if (options.collection) this.collection = options.collection;
-      if (options.id)         this.id         = options.id;
-      if (options.className)  this.className  = options.className;
-      this.options = options;
-    },
-
-    render : function() {
+  };

jQuery lookup, scoped to DOM elements within the current view. +This should be prefered to global jQuery lookups, if you're dealing with +a specific view.

  var jQueryScoped = function(selector) {
+    return $(selector, this.el);
+  };

Set up all interitable Backbone.View properties and methods.

  _.extend(Backbone.View.prototype, {

The default tagName of a View's element is "div".

    tagName : 'div',

Attach the jQuery function as the $ and jQuery properties.

    $       : jQueryScoped,
+    jQuery  : jQueryScoped,

render is the core function that your view should override, in order +to populate its element (this.el), with the appropriate HTML. The +convention is for render to always return this.

    render : function() {
       return this;
-    },
#

jQuery lookup, scoped to the current view.

    $ : function(selector) {
-      return $(selector, this.el);
-    },
#

Quick-create a dom element with attributes.

    make : function(tagName, attributes, content) {
+    },

For small amounts of DOM Elements, where a full-blown template isn't +needed, use make to manufacture elements, one at a time.

+ +

var el = this.make('li', {'class': 'row'}, this.model.get('title'));

    make : function(tagName, attributes, content) {
       var el = document.createElement(tagName);
       if (attributes) $(el).attr(attributes);
       if (content) $(el).html(content);
       return el;
-    },
#

Makes the view enter a mode. Modes have both a 'mode' and a 'group', + },

Makes the view enter a mode. Modes have both a 'mode' and a 'group', and are mutually exclusive with any other modes in the same group. -Setting will update the view's modes hash, as well as set an HTML className -of [mode]_[group] on the view's element. Convenient way to swap styles +Setting will update the view's modes hash, as well as set an HTML class +of [mode]_[group] on the view's element. Convenient way to swap styles and behavior.

    setMode : function(mode, group) {
       if (this.modes[group] == mode) return;
       $(this.el).setMode(mode, group);
       this.modes[group] = mode;
-    },
#

Set callbacks, where this.callbacks is a hash of + },

Set callbacks, where this.callbacks is a hash of {selector.eventname, callbackname} -pairs. Callbacks will be bound to the view, with 'this' set properly. -Passing a selector of 'el' binds to the view's root element. -Change events are not delegated through the view because IE does not bubble -change events at all.

    setCallbacks : function(callbacks) {
+pairs. Callbacks will be bound to the view, with this set properly.
+Uses jQuery event delegation for efficiency.
+Passing a selector of el binds to the view's root element.
+Change events are not delegated through the view because IE does not
+bubble change events at all.

    setCallbacks : function(callbacks) {
       $(this.el).unbind();
       if (!(callbacks || (callbacks = this.callbacks))) return this;
       for (key in callbacks) {
@@ -330,25 +356,41 @@ change events at all.

} } return this; + },

Performs the initial configuration of a View with a set of options. +Keys with special meaning (model, collection, id, className), are +attatched directly to the view.

    _initialize : function(options) {
+      if (this.options) options = _.extend({}, this.options, options);
+      if (options.model)      this.model      = options.model;
+      if (options.collection) this.collection = options.collection;
+      if (options.id)         this.id         = options.id;
+      if (options.className)  this.className  = options.className;
+      this.options = options;
     }
 
-  });
#

Set up inheritance for the model, collection, and view.

  var extend = Backbone.Model.extend = Backbone.Collection.extend = Backbone.View.extend = function (protoProps, classProps) {
+  });

Set up inheritance for the model, collection, and view.

  var extend = Backbone.Model.extend = Backbone.Collection.extend = Backbone.View.extend = function (protoProps, classProps) {
     var child = inherits(this, protoProps, classProps);
     child.extend = extend;
     return child;
-  };
+  };

Override this function to change the manner in which Backbone persists +models to the server. You will be passed the type of request, and the +model in question. By default, uses jQuery to make a RESTful Ajax request +to the model's url(). Some possible customizations could be:

- Backbone.request = function(type, model, success, error) { - +
    +
  • Use setTimeout to batch rapid-fire updates into a single request.
  • +
  • Send up the models as XML instead of JSON.
  • +
  • Persist models via WebSockets instead of Ajax.
  • +
  Backbone.request = function(type, model, success, error) {
+    var data = model.attributes ? {model : JSON.stringify(model.attributes())} : {};
     $.ajax({
       url       : model.url(),
       type      : type,
-      data      : {model : JSON.stringify(model.attributes())},
+      data      : data,
       dataType  : 'json',
       success   : success,
       error     : error
     });
-  }
+  };
 
 })();
 
diff --git a/docs/docco.css b/docs/docco.css
index 1ddec891..76e818b3 100644
--- a/docs/docco.css
+++ b/docs/docco.css
@@ -89,10 +89,10 @@ table td {
       font-size: 12px;
       padding: 0 0.2em;
     }
-    .octowrap {
+    .pilwrap {
       position: relative;
     }
-      .octothorpe {
+      .pilcrow {
         font: 12px Arial;
         text-decoration: none;
         color: #454545;
@@ -102,11 +102,11 @@ table td {
         opacity: 0;
         -webkit-transition: opacity 0.2s linear;
       }
-        td.docs:hover .octothorpe {
+        td.docs:hover .pilcrow {
           opacity: 1;
         }
   td.code, th.code {
-    padding: 14px 15px 16px 50px;
+    padding: 14px 15px 16px 25px;
     width: 100%;
     vertical-align: top;
     background: #f5f5ff;

From 2081bbf3316cc6826616e8eca6cc044d029914c5 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas 
Date: Wed, 6 Oct 2010 14:48:01 -0400
Subject: [PATCH 20/29] more comments, more docs

---
 backbone.js        | 25 ++++++++++++++++---------
 docs/backbone.html | 33 ++++++++++++++++++++++-----------
 2 files changed, 38 insertions(+), 20 deletions(-)

diff --git a/backbone.js b/backbone.js
index c4113108..4aeb2677 100644
--- a/backbone.js
+++ b/backbone.js
@@ -1,7 +1,7 @@
-//    (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
-//    Backbone may be freely distributed under the terms of the MIT license.
-//    For all details and documentation:
-//    http://documentcloud.github.com/backbone
+//     (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
+//     Backbone may be freely distributed under the terms of the MIT license.
+//     For all details and documentation:
+//     http://documentcloud.github.com/backbone
 
 (function(){
 
@@ -39,9 +39,9 @@
   // custom events. You may `bind` or `unbind` a callback function to an event;
   // `trigger`-ing an event fires all callbacks in succession.
   //
-  //    _.extend(object, Backbone.Bindable);
-  //    object.bind('expand', function(){ alert('expanded'); });
-  //    object.trigger('expand');
+  //     _.extend(object, Backbone.Bindable);
+  //     object.bind('expand', function(){ alert('expanded'); });
+  //     object.trigger('expand');
   //
   Backbone.Bindable = {
 
@@ -507,7 +507,7 @@
     // For small amounts of DOM Elements, where a full-blown template isn't
     // needed, use **make** to manufacture elements, one at a time.
     //
-    //    var el = this.make('li', {'class': 'row'}, this.model.get('title'));
+    //     var el = this.make('li', {'class': 'row'}, this.model.get('title'));
     //
     make : function(tagName, attributes, content) {
       var el = document.createElement(tagName);
@@ -528,7 +528,14 @@
     },
 
     // Set callbacks, where this.callbacks is a hash of
-    //   {selector.event_name, callback_name}
+    //
+    // *{selector.event_name: callback_name}*
+    //
+    //     {
+    //       '.icon.pencil.mousedown':  'edit',
+    //       '.button.click':           'save'
+    //     }
+    //
     // pairs. Callbacks will be bound to the view, with `this` set properly.
     // Uses jQuery event delegation for efficiency.
     // Passing a selector of `el` binds to the view's root element.
diff --git a/docs/backbone.html b/docs/backbone.html
index 2b94b022..4974c9da 100644
--- a/docs/backbone.html
+++ b/docs/backbone.html
@@ -1,7 +1,8 @@
-      backbone.js           

backbone.js

(c) 2010 Jeremy Ashkenas, DocumentCloud Inc. - Backbone may be freely distributed under the terms of the MIT license. - For all details and documentation: - http://documentcloud.github.com/backbone

(function(){

Initial Setup

The top-level namespace.

  var Backbone = {};

Keep the version in sync with package.json.

  Backbone.VERSION = '0.1.0';

Export for both CommonJS and the Browser.

  (typeof exports !== 'undefined' ? exports : this).Backbone = Backbone;

Helper function to correctly set up the prototype chain, for subclasses. + backbone.js

this._boundOnModelChange=_.bind(this._onModelChange,this);this._initialize();if(models)this.refresh(models,true); - };

backbone.js

(c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
+Backbone may be freely distributed under the terms of the MIT license.
+For all details and documentation:
+http://documentcloud.github.com/backbone
+
(function(){

Initial Setup

The top-level namespace.

  var Backbone = {};

Keep the version in sync with package.json.

  Backbone.VERSION = '0.1.0';

Export for both CommonJS and the Browser.

  (typeof exports !== 'undefined' ? exports : this).Backbone = Backbone;

Helper function to correctly set up the prototype chain, for subclasses. Similar to goog.inherits, but uses a hash of prototype properties and static properties to be extended.

  var inherits = function(parent, protoProps, classProps) {
     var child = protoProps.hasOwnProperty('constructor') ? protoProps.constructor :
@@ -17,9 +18,10 @@ static properties to be extended.

  Backbone.Bindable = {

Bind an event, specified by a string name, ev, to a callback function. +

_.extend(object, Backbone.Bindable);
+object.bind('expand', function(){ alert('expanded'); });
+object.trigger('expand');
+
  Backbone.Bindable = {

Bind an event, specified by a string name, ev, to a callback function. Passing "all" will bind the callback to all events fired.

    bind : function(ev, callback) {
       var calls = this._callbacks || (this._callbacks = {});
       var list  = this._callbacks[ev] || (this._callbacks[ev] = []);
@@ -322,7 +324,8 @@ convention is for render to always return this.

},

For small amounts of DOM Elements, where a full-blown template isn't needed, use make to manufacture elements, one at a time.

-

var el = this.make('li', {'class': 'row'}, this.model.get('title'));

    make : function(tagName, attributes, content) {
+
var el = this.make('li', {'class': 'row'}, this.model.get('title'));
+
    make : function(tagName, attributes, content) {
       var el = document.createElement(tagName);
       if (attributes) $(el).attr(attributes);
       if (content) $(el).html(content);
@@ -335,9 +338,17 @@ and behavior.

if (this.modes[group] == mode) return; $(this.el).setMode(mode, group); this.modes[group] = mode; - },

Set callbacks, where this.callbacks is a hash of - {selector.eventname, callbackname} -pairs. Callbacks will be bound to the view, with this set properly. + },

Set callbacks, where this.callbacks is a hash of

+ +

{selector.event_name: callback_name}

+ +
{
+  '.icon.pencil.mousedown':  'edit',
+  '.button.click':           'save'
+}
+
+ +

pairs. Callbacks will be bound to the view, with this set properly. Uses jQuery event delegation for efficiency. Passing a selector of el binds to the view's root element. Change events are not delegated through the view because IE does not From 2f4d1ac270771440334062f1a9d544613d676b8d Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 6 Oct 2010 15:17:07 -0400 Subject: [PATCH 21/29] -- --- backbone.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index 4aeb2677..5c628347 100644 --- a/backbone.js +++ b/backbone.js @@ -78,7 +78,8 @@ return this; }, - // Trigger an event, firing all bound callbacks. + // Trigger an event, firing all bound callbacks. Callbacks are passed the + // same arguments as `trigger` is, apart from the event name. trigger : function(ev) { var calls = this._callbacks; for (var i = 0; i < 2; i++) { From 9bdefd0273f1f094aabba1b5653e3a759927d3c2 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 6 Oct 2010 17:41:10 -0400 Subject: [PATCH 22/29] started working on the index page, copied from Jammit --- .gitignore | 1 + TODO | 5 + backbone.js | 8 +- docs/backbone.png | Bin 0 -> 30207 bytes index.html | 883 ++++++++++++++++++++++++++++++++++++++++++++++ test/model.js | 2 +- 6 files changed, 894 insertions(+), 5 deletions(-) create mode 100644 .gitignore create mode 100644 TODO create mode 100644 docs/backbone.png create mode 100644 index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..48ecc30c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +raw \ No newline at end of file diff --git a/TODO b/TODO new file mode 100644 index 00000000..7e5e309a --- /dev/null +++ b/TODO @@ -0,0 +1,5 @@ +* Make Collection a Pseudo-array, a-la jQuery. + +* Hook up data binding in the view. Both ways. Parallel to models. + +* Add simple validations. diff --git a/backbone.js b/backbone.js index 5c628347..75e89a83 100644 --- a/backbone.js +++ b/backbone.js @@ -180,7 +180,7 @@ } } } - if (!options.silent && this._changed) this.changed(); + if (!options.silent && this._changed) this.change(); return this; }, @@ -193,14 +193,14 @@ if (!options.silent) { this._changed = true; this.trigger('change:' + attr); - this.changed(); + this.change(); } return value; }, - // Call this method to fire manually fire a `changed` event for this model. + // Call this method to fire manually fire a `change` event for this model. // Calling this will cause all objects observing the model to update. - changed : function() { + change : function() { this.trigger('change', this); this._formerAttributes = this.attributes(); this._changed = false; diff --git a/docs/backbone.png b/docs/backbone.png new file mode 100644 index 0000000000000000000000000000000000000000..b69682c8e62f4779156d74e4ac76120f60f139aa GIT binary patch literal 30207 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 literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 00000000..165a4943 --- /dev/null +++ b/index.html @@ -0,0 +1,883 @@ + + + + + + Backbone: ... + + + + +

+
+ Bindable +
+
    +
  • bind
  • +
  • unbind
  • +
  • trigger
  • +
+
+ Model +
+
    +
  • get
  • +
  • set
  • +
  • unset
  • +
  • attributes
  • +
  • save
  • +
  • destroy
  • +
  • url
  • +
  • clone
  • +
  • toString
  • +
  • isEqual
  • +
  • isNew
  • +
  • change
  • +
  • hasChanged
  • +
  • changedAttributes
  • +
  • formerValue
  • +
  • formerAttributes
  • +
+
+ Collection +
+
    +
  • add
  • +
  • remove
  • +
  • get
  • +
  • getIds
  • +
  • at
  • +
  • sort
  • +
  • refresh
  • +
  • fetch
  • +
  • create
  • +
  • getByCid
  • +
  • getCids
  • +
  • toString
  • +
  • pluck
  • +
  • Underscore Methods (24)
  • +
+
+ Request +
+
    +
  • Backbone.request
  • +
+
+ View +
+
    +
  • $ (jQuery)
  • +
  • render
  • +
  • make
  • +
  • setMode
  • +
  • setCallbacks
  • +
+
+ +
+ +

+ Backbone.js +

+ +

+ Backbone gives + structure to JavaScript applications by providing models with + key-value binding, collections with rich enumerable functions, + views with declarative callbacks, and connects to your existing webapp + over a RESTful JSON interface. +

+ +

+ Current Version: 0.1.0 +

+ +

+ Backbone is an open-source component of DocumentCloud. +

+ +

Installation

+ +
    +
  1. + Grab the gem:
    + gem install jammit +
  2. +
  3. + Add the gem to Rails' environment.rb inside of the initializer:
    + config.gem "jammit" +
  4. +
  5. + If you're using Rails 2, edit config/routes.rb to give Jammit a route + ( /assets by default) for dynamic asset packaging and caching. + In Rails 3, this route is loaded automatically. +

    + +
    +ActionController::Routing::Routes.draw do |map|
    +  ...
    +  Jammit::Routes.draw(map)
    +  ...
    +end
    +
  6. +
+ +

+ Note: If you don't already have the + ruby-yui-compressor or + closure-compiler + gems installed, downloading make take a minute — the jar files together + weigh in at 5 megabytes. +

+ +

Configuration

+ +

+ Jammit uses the config/assets.yml YAML configuration file to define + packages and to set extra options. A package is an ordered set of directory glob + rules that will be expanded into a unique list of files. An example of + a complete assets.yml follows: +

+ +
+embed_assets: on
+
+javascripts:
+  workspace:
+    - public/javascripts/vendor/jquery.js
+    - public/javascripts/lib/*.js
+    - public/javascripts/views/**/*.js
+    - app/views/workspace/*.jst
+
+stylesheets:
+  common:
+    - public/stylesheets/reset.css
+    - public/stylesheets/widgets/*.css
+  workspace:
+    - public/stylesheets/pages/workspace.css
+  empty:
+    - public/stylesheets/pages/empty.css
+ +

+ There are a number of extra configuration options that you may add to the + assets.yml configuration file to customize the way Jammit behaves. + Here's an example configuration file + using all of the possible options. The meaning of the options and their + default values are listed below. Don't be bewildered by the length of the + list — none of the options are required, they just give you fine-grained + control over how your packages are generated. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
package_assetson | off | always + Defaults to on, packaging and caching assets in every environment but development. + Never packages when off, always packages when always. +
embed_assetson | off | datauri + Defaults to off. When on, packages and caches Data-URI + and MTHML variants of your stylesheets, with whitelisted images + embedded inline. Using datauri serves embedded images only + to browsers that support Data-URIs, and serves unmodified stylesheets + to Internet Explorer 7 or lower. +
compress_assetson | off + Defaults to on. When off, JavaScript and CSS packages + will be left uncompressed (by YUI or Closure). Disabling compression is only recommended + if you're packaging assets in development. +
gzip_assetson | off + Defaults to on. When off, only the plain version of the + asset will be written out, not the .gz alternative. Disable this + if you don't plan on configuring your webserver to serve the static + gzip alternatives. +
javascript_compressoryui | closure + Defaults to yui. As of 0.2.0, the Jammit gem can use either the + YUI Compressor + or the + Google Closure Compiler + to compress your JavaScript. +
template_functionon | off | ... + The JavaScript function that compiles your JavaScript templates (JST). + Defaults to on, which uses a bundled variant of + Micro-Templating. + Set it to _.template if you use + Underscore.js, + or new Template for + Prototype. Turn it off + to pass through the template strings unaltered. +
template_namespace... + By default, all of your compiled templates will be added to a top-level + window.JST object. If you'd like to add them instead to your + own JavaScript namespace, change it to the + object of your choice. +
template_extension... + By default, Jammit treats files with a jst extension as + JavaScript templates. If you'd prefer to use a different extension, like + html.mustache, set the template_extension option. +
package_path... + The URL at which packaged assets are cached and made available. + Defaults to assets, but if you already have an existing + AssetsController with a different purpose, you could change it to, say, + packages. (Single directory names only, please.) +
compressor_options... + Pass an options hash directly to the underlying JavaScript compressor + to configure it. See the + ruby-yui-compressor or + closure-compiler + gem documentation for the full list of available options. +
css_compressor_options... + Pass an options hash directly to the YUI CSS Compressor. + Available options + are charset, and line_break, which can be used to write + out each CSS rule on a separate line. +
+ +

+ The contents of assets.yml are passed through ERB before + being loaded, in case you find the need to configure Jammit + for different environments. +

+ +

+ Warning: In Rails 3, static assets are not served by default. + You either have to configure your webserver to serve static assets out + of public (recommended), or set + config.serve_static_assets = true +

+ +

Usage

+ +

+ To access your packages in views, use the corresponding helper. The + helper methods can include multiple packages at once: +

+ +
+<%= include_stylesheets :common, :workspace, :media => 'all' %>
+<%= include_javascripts :workspace %>
+ +

+ In development, no packaging is performed, so you'll see a list of individual + references to all of the JavaScript and CSS files. The assets.yml + configuration file is reloaded on every development request, so you can + change the contents of your packages without needing to restart Rails. + In all other environments, or if package_assets is set to "always", + you'll get tags for the merged packages. +

+ +

+ It's recommended to keep the default behavior of unpackaged assets in + development, because it's so much easier to debug JavaScript that way. + But if you'd like to use fully packaged assets in development, while only rebuilding the packages whose + sources have changed between requests, use "always" and add the following + before_filter to your ApplicationController to keep your + packages fresh: +

+ +
before_filter { Jammit.packager.precache_all } if Rails.env.development?
+ +

YUI & Closure

+ +

+ Jammit can be configured to use either the + YUI Compressor or the + Google Closure Compiler + to compress and optimize your JavaScript. (CSS is always run through the + YUI Compressor.) Specify the javascript_compressor to choose either yui + or closure backends. If left blank, Jammit defaults to yui. +

+ +

+ You can configure the JavaScript compilation by adding compressor_options + to your assets.yml. The compressor_options + will be passed directly to the Gem backend of your chosen compressor. See the + ruby-yui-compressor or + closure-compiler + gem documentation for all the available options. For example, to configure + the Closure Compiler to use its + advanced optimizations, + you would add the compilation_level: +

+ +
+javascript_compressor: closure
+compressor_options:
+  compilation_level: "ADVANCED_OPTIMIZATIONS"
+
+ +

+ Jammit always uses the YUI CSS Compressor to compress CSS files. You can + configure it + by specifying the css_compressor_options, in assets.yml. +

+ +

+ + Warning: Google's Closure Compiler has been known to choke on certain + already pre-compressed JavaScripts, such as the production version of jQuery. + Using the uncompressed development version is recommended. + +

+ +

Precaching Assets

+ +

+ Installing the Jammit gem provides the optional but handy jammit command-line utility, + which can be hooked into your deployment process. The jammit + command reads in your configuration file, generates all of the defined + packages, and gzips them at the highest compression setting. In order to + serve these static gzipped versions, configure your Nginx + http_gzip_static_module, + or your Apache + Content Negotiation MultiViews. + It's also a good idea to have gzip compression turned on for the remainder + of your static assets, including any asset packages that aren't gzipped-in-advance. + Adding Nginx's + http_gzip_module + or Apache's + mod_deflate + will do the trick. +

+ +

+ The jammit command can be passed options to configure the + path to assets.yml, and the output directory in which all packages + are compiled. Run jammit --help to see all of the options. For + speedy builds, jammit will check the modification times of your packages + and source files: only the packages that have changed are rebuilt. If you'd + like to force all packages to build, use jammit --force. +

+ +

+ In production, you'll want to run Jammit during deployment, somewhere + in between updating the source and symlinking to the new release. Whether you're + using Capistrano, + Vlad, + or just good 'ol Rake, + it shouldn't be too hard to add a step that calls the jammit command. + For an example Jammit setup under Capistrano and Apache, see + Mike Gunderloy's blog post. +

+ +

Expires Headers

+ +

+ To get the fastest page load times for your regular visitors, it's recommended + to set the HTTP Expires header to a date far in the future. You don't need + to worry about clearing the cached assets when you deploy a new release, + because Rails will write out the asset's modification time as part of the URL, + causing browsers to fetch a fresh copy of the asset. +

+ +

+ If you're using an Nginx webserver, add the following snippet to the +
server { ... } block for your application: +

+ +
+location ~ ^/assets/ {
+  passenger_enabled on;
+  expires max;
+}
+ +

+ If you're using Apache, make sure that + mod_expires + is enabled, and then add the following snippet to the + appropriate VirtualHost section of your configuration, + filling in the path to your deployed application: +

+ +
+ExpiresActive On
+<Directory "/path/to/public/assets">
+  ExpiresDefault "access plus 1 year"
+</Directory>
+ +

+ If for any reason (multiple physical servers, rsync-only deploys) you don't + wish to rely on modification times for cache expiry, Jammit uses the + standard Rails helpers to generate URLs, and will respect + the RAILS_ASSET_ID environment variable. +

+ +

Embedding Images ...

+ +

+ After you've finished concatenating and compressing your JavaScript and + CSS files into streamlined downloads, the slowest part of your page load + is probably the images. It's common to use image sprites to avoid the + avalanche of HTTP requests that are needed to load a bunch of small images. + Unfortunately, image sprites can be complicated to position (especially + with horizontal and vertical tiling), and a real pain to create and + maintain. With a little elbow grease from Jammit, your spriting woes can be a + thing of the past. +

+ +

+ With embed_assets turned on, Jammit will inline image files directly + into your compiled CSS, using + Data-URIs in + supported browsers, and + MHTML in Internet Explorer 7 and below. + Instead of ten CSS files referencing 30 images, you can have a single, + packaged, minified, gzipped CSS file, with the images coming in all at + once instead of piecemeal, making just a single HTTP request. +

+ +

+ Take a look at this example (especially on a slow connection or wifi):
+ Normal Image Loading vs. + Jammit Image Embedding
+

+ +

+ Embedded images can be a little tricky, which is why using them is strictly + on an opt-in basis. After enabling embed_assets in the configuration file, + you'll need to whitelist the images that you'd like to make embeddable. + When processing CSS, Jammit will only embed images that have + .../embed/... somewhere in their path — the other images will be + left untouched. You can make a single public/images/embed folder for + your embedded images, or organize them into directories however you + prefer. It's not recommended to embed all of your site's images, just + the ones that conform to the following three rules: +

+ +
    +
  1. + Images that are small. Large images will simply delay the rendering + of your CSS. Jammit won't embed images larger than 32 kilobytes, because + Internet Explorer 8 won't render them. +
  2. +
  3. + Images that are immediately visible. It's better to leave the + images that are hidden at page load time to download in the background. +
  4. +
  5. + Images that are referenced by a single CSS rule. Referencing the + same embedded image in multiple rules will cause that image's contents to be + embedded more than once, defeating the purpose. Replace the duplicated + rules with an image-specific HTML class, and you're good to go. +
  6. +
+ +

+ A final cautionary note. Internet Explorer's implementation of MHTML + requires the use of absolute paths in image references. This means that + the timestamp that Rails appends to the stylesheet URL (to allow + far-future expires headers) needs to also be in the contents of the + stylesheet. If a process on your webserver changes the modification time + of the cached MHTML stylesheet, it will break the image references. To fix + it, use the jammit command (with --base-url) to rebuild your assets, or simply delete + the cached file, and let Jammit automatically rebuild the file on the next + request. +

+ +

+ If the MHTML stylesheets sound too fragile, or if you encounter any problems + with them in Internet Explorer (such as mixed-mode warnings when serving + MHTML through SSL), we recommend setting embed_assets to "datauri". + Using "datauri" will cause Internet Explorer 7 and below to receive + plain versions of the packaged stylesheets, while all other browsers get the + Data-URI flavor. +

+ +

... and Embedding Fonts

+ +

+ Embedded fonts work in largely the same way as images, TTF and OTF files + that are whitelisted within an "embed" folder will be inlined as Data-URIs. + There is no MHTML variant for fonts, because Internet Explorer only supports + unembeddable EOT files. Embedding is especially important + for fonts in general, because it helps avoid + the flash of unstyled text + that the browser would otherwise display while waiting for the font to + download. If you're looking to get started with web fonts, + FontSquirrel has a great tool + that can generate the proper fonts and styles you'll need. Here's an example + of a CSS rule that would activate the proper Jammit embedding: +

+ +
+@font-face {
+  font-family: 'DroidSans';
+  src: url(/fonts/DroidSans.eot);
+  src: local('Droid Sans'), local('DroidSans'), url(/fonts/embed/DroidSans.ttf) format('truetype');
+}
+ +

JavaScript Templates

+ +

+ If you're using enough JavaScript to want it compressed and concatenated, + you're probably using it to generate at least a little of your + HTML. Traditionally, this has been accomplished by joining together strings + and inserting them into the DOM. A far more agreeable way to generate HTML + on the client is to use JavaScript templates (referred to here as JST). +

+ +

+ Jammit helps keep your JavaScript views organized alongside your Rails + views, bundling them together into packages, and providing them pre-compiled + for your client-side code to evaluate. If left unspecified, Jammit uses a variant of + John Resig's Micro Templating + to compile the templates, but you can choose your preferred JavaScript templating engine + of your choosing by setting the template_function in assets.yml. + Jammit will run all of your templates through the function, and assign + each one by filename to a top-level JST object. For example, the following + template, app/views/drivers/license.jst: +

+ +
+<div class="drivers-license">
+  <h2>Name: <%= name %></h2>
+  <em>Hometown: <%= birthplace %></em>
+  <div class="biography">
+    <%= bio %>
+  </div>
+</div>
+ +

+ Including this template within a JavaScript asset package makes it + available within the browser as the JST.license function. +

+ +
+JST.license({name : "Moe", birthplace : "Brooklyn", bio : "Moe was always..."});
+ +

+ Since 0.5.0, templates should be included in the appropriate + javascripts package. Here's an example of an assets.yml + that uses templates, and shows the location at which they'll be available + on the client. When including templates from different directories, the + common prefix is ignored, and the rest of the path becomes the name of the + template: +

+ +
+javascripts:
+  workspace:
+    - app/views/accounts/badge.jst
+    - app/views/common/dialog.jst
+    - app/views/common/menu.jst
+ +

+ Then, from your JavaScript: +

+ +
+JST['accounts/badge']
+JST['common/dialog']
+JST['common/menu']
+ +

+ To use Underscore.js + templates, set template_function to "_.template".
+ To use Prototype templates, set + it to "new Template".
To use + Mustache.js templates, + you'll need + a little extra setup. +

+ +

+ The default extension for template files is jst, + but you can set template_extension in your assets.yml if you're + using an engine with a preferred extension, such as html.mustache +

+ +

Change Log

+ +

+ 0.5.3
+ You can now embed WOFF-formatted fonts. + Jammit warnings avoid use of Rails.logger. + Bugfixes for Ruby 1.9.2 with asset files containing Unicode characters. +

+ +

+ 0.5.1
+ Included missing rails/routes.rb file in the Gem manifest for Rails 3. +

+ +

+ 0.5.0
+ The Jammit gem is now compatible with Rails 3. JST template packages are + no longer specified separately. This is a backwards-incompatible change, + and you'll have to update your assets.yml file to simply include + the JST in your existing JavaScript packages, as shown above. Alongside + this change, template names are now better namespaced — see the + section on JavaScript Templates for the details. + Bugfix for IE8 and Data-URIs slightly under 32kb in size, as well as + for MHTML in IE7-mode in Vista. +

+ +

+ 0.4.4
+ Jammit will now add the RAILS_ASSET_ID timestamp to image URLs + within your CSS packages, if configured, for better cache-busting. + Greg Hazel contributed a series + of Jammit/Windows bug fixes. +

+ +

+ 0.4.3
+ Bugfix for building on Windows with drive-letter absolute paths. +

+ +

+ 0.4.2
+ Added a logged warning when you have rules in assets.yml that don't + match any files. +

+ +

+ 0.4.1
+ Jammit is now able to embed @font-face web fonts in TTF and + OTF formats. The embed_images option has been renamed to + embed_assets for this change. The assets.yml file is now + passed through ERB before being loaded, so you can configure + environment-specific settings, just like you would with database.yml. +

+ +

+ 0.3.3
+ Added css_compressor_options to assets.yml, so you can + set charset and line_break for the YUI CSS Compressor. +

+ +

+ 0.3.2
+ If Java isn't available on your machine, Jammit will now run in a graceful + degraded mode, where assets are packaged but not compressed. You can now + pass :embed_assets => false to the include_stylesheets + helper to disable image embedding on a per-package basis. +

+ +

+ 0.2.8
+ Jammit now correctly rewrites relative image URLs within CSS, for easier + integration with partial Rails apps deployed on sub-paths. +

+ +

+ 0.2.7
+ The jammit command has been enhanced to check the modification times of + your packages — if no source files have changed, the package isn't + rebuilt. +

+ +

+ 0.2.6
+ Jammit now raises an exception if Java isn't installed, or if the Java + version is unsupported by your JavaScript compressor of choice. +

+ +

+ 0.2.5
+ When specifying your asset packages as directory globs, absolute globs are + now absolute, and relative globs are relative to RAILS_ROOT. +

+ +

+ 0.2.4
+ Jammit now throws a ConfigurationNotFound error when attempting to load + a nonexistent configuration file. Resolved an issue with asset caching from + daemonized mongrels. +

+ +

+ 0.2.1
+ The include_stylesheets helper now takes the same options as the + Rails stylesheet_link_tag helper (such as :media => 'print'). +

+ +

+ 0.2.0
+ Jammit now supports the Google Closure Compiler as an alternative + to the YUI compressor. +

+ +

+ 0.1.3
+ Fixed a bug that conflicted with other plugins trying to alter + ApplicationController in development. +

+ +

+ 0.1.1
+ Added support for embedding images with stylesheet-relative paths. + Shortened the MHTML identifiers. +

+ +

+ 0.1.0
+ Initial Jammit release. +

+ +

+
+ + A DocumentCloud Project + +

+ +
+ + + + + diff --git a/test/model.js b/test/model.js index 2e544bd1..356a7a36 100644 --- a/test/model.js +++ b/test/model.js @@ -103,7 +103,7 @@ $(document).ready(function() { ok(_.isEqual(model.formerAttributes(), {name : "Tim", age : 10}), 'formerAttributes is correct'); }); model.set({name : 'Rob'}, {silent : true}); - model.changed(); + model.change(); equals(model.get('name'), 'Rob'); }); From d0e27756f287e97c4b99d34a4151a4d913091789 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 7 Oct 2010 09:01:59 -0400 Subject: [PATCH 23/29] Attributes should remain a safe copy. --- backbone.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backbone.js b/backbone.js index 75e89a83..350bbeed 100644 --- a/backbone.js +++ b/backbone.js @@ -101,9 +101,8 @@ // If you do not specify the id, a negative id will be assigned for you. Backbone.Model = function(attributes) { this._attributes = {}; - attributes = attributes || {}; - this.set(attributes, {silent : true}); this.cid = _.uniqueId('c'); + this.set(attributes || {}, {silent : true}); this._formerAttributes = this.attributes(); }; From ed1871cbfed602d3fe117c7c2903f4cd0a2f20be Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 7 Oct 2010 09:33:50 -0400 Subject: [PATCH 24/29] first draft of basic validations --- TODO | 2 -- backbone.js | 29 ++++++++++++++++++++++------- index.html | 2 +- test/model.js | 21 ++++++++++++++++++++- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/TODO b/TODO index 7e5e309a..54b34394 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,3 @@ -* Make Collection a Pseudo-array, a-la jQuery. - * Hook up data binding in the view. Both ways. Parallel to models. * Add simple validations. diff --git a/backbone.js b/backbone.js index 350bbeed..babd2439 100644 --- a/backbone.js +++ b/backbone.js @@ -35,10 +35,11 @@ // Backbone.Bindable // ----------------- - // A module that can be mixed in to any object in order to provide it with + // A module that can be mixed in to *any object* in order to provide it with // custom events. You may `bind` or `unbind` a callback function to an event; // `trigger`-ing an event fires all callbacks in succession. // + // var object = {}; // _.extend(object, Backbone.Bindable); // object.bind('expand', function(){ alert('expanded'); }); // object.trigger('expand'); @@ -160,14 +161,26 @@ // Set a hash of model attributes on the object, firing `changed` unless you // choose to silence it. set : function(attrs, options) { + + // Extract attributes and options. options || (options = {}); if (!attrs) return this; attrs = attrs._attributes || attrs; var now = this._attributes; - if ('id' in attrs) { - this.id = attrs.id; - if (this.collection) this.resource = this.collection.resource + '/' + this.id; + + // Run validation if `validate` is defined. + if (this.validate) { + var error = this.validate(attrs); + if (error) { + this.trigger('error', error); + return false; + } } + + // Check for changes of `id`. + if ('id' in attrs) this.id = attrs.id; + + // Update attributes. for (var attr in attrs) { var val = attrs[attr]; if (val === '') val = null; @@ -179,6 +192,8 @@ } } } + + // Fire the `change` event, if the model has been changed. if (!options.silent && this._changed) this.change(); return this; }, @@ -244,10 +259,10 @@ save : function(attrs, options) { attrs || (attrs = {}); options || (options = {}); - this.set(attrs, options); + if (!this.set(attrs, options)) return false; var model = this; var success = function(resp) { - model.set(resp.model); + if (!model.set(resp.model)) return false; if (options.success) options.success(model, resp); }; var method = this.isNew() ? 'POST' : 'PUT'; @@ -420,7 +435,7 @@ if (!(model instanceof Backbone.Model)) model = new this.model(model); model.collection = this; var success = function(model, resp) { - model.set(resp.model); + if (!model.set(resp.model)) return false; model.collection.add(model); if (options.success) options.success(model, resp); }; diff --git a/index.html b/index.html index 165a4943..17cb08be 100644 --- a/index.html +++ b/index.html @@ -35,7 +35,7 @@ ul.toc_section { font-size: 11px; line-height: 14px; - margin: 5px 0 20px 0; + margin: 5px 0 15px 0; padding-left: 0px; list-style-type: none; font-family: Lucida Grande; diff --git a/test/model.js b/test/model.js index 356a7a36..92b2e421 100644 --- a/test/model.js +++ b/test/model.js @@ -119,4 +119,23 @@ $(document).ready(function() { ok(_.isEqual(lastRequest[1], doc)); }); -}); \ No newline at end of file + test("model: validate", function() { + var lastError; + var model = new Backbone.Model(); + model.validate = function(attrs) { + if (attrs.admin) return "Can't change admin status."; + }; + model.bind('error', function(error) { + lastError = error; + }); + var result = model.set({a: 100}); + equals(result, model); + equals(model.get('a'), 100); + equals(lastError, undefined); + result = model.set({a: 200, admin: true}); + equals(result, false); + equals(model.get('a'), 100); + equals(lastError, "Can't change admin status."); + }); + +}); From da07cedb0f6a3ead78730b8269f5bde523c1d3ab Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 7 Oct 2010 10:02:06 -0400 Subject: [PATCH 25/29] sets can fire 'error' events for all models within, as well. --- backbone.js | 50 +++++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/backbone.js b/backbone.js index babd2439..9ad40b0a 100644 --- a/backbone.js +++ b/backbone.js @@ -81,13 +81,19 @@ // Trigger an event, firing all bound callbacks. Callbacks are passed the // same arguments as `trigger` is, apart from the event name. + // Listening for `"all"` passes the true event name as the first argument. trigger : function(ev) { + var list, calls, i, l; var calls = this._callbacks; - for (var i = 0; i < 2; i++) { - var list = calls && calls[i ? 'all' : ev]; - if (!list) continue; - for (var j = 0, l = list.length; j < l; j++) { - list[j].apply(this, _.rest(arguments)); + if (!(calls = this._callbacks)) return this; + if (list = calls[ev]) { + for (i = 0, l = list.length; i < l; i++) { + list[i].apply(this, _.rest(arguments)); + } + } + if (list = calls['all']) { + for (i = 0, l = list.length; i < l; i++) { + list[i].apply(this, arguments); } } return this; @@ -172,7 +178,7 @@ if (this.validate) { var error = this.validate(attrs); if (error) { - this.trigger('error', error); + this.trigger('error', this, error); return false; } } @@ -188,7 +194,7 @@ now[attr] = val; if (!options.silent) { this._changed = true; - this.trigger('change:' + attr); + this.trigger('change:' + attr, this); } } } @@ -206,7 +212,7 @@ delete this._attributes[attr]; if (!options.silent) { this._changed = true; - this.trigger('change:' + attr); + this.trigger('change:' + attr, this); this.change(); } return value; @@ -296,7 +302,7 @@ this.comparator = options.comparator; delete options.comparator; } - this._boundOnModelChange = _.bind(this._onModelChange, this); + this._boundOnModelEvent = _.bind(this._onModelEvent, this); this._initialize(); if (models) this.refresh(models,true); }; @@ -359,7 +365,7 @@ model.collection = this; var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length; this.models.splice(index, 0, model); - model.bind('change', this._boundOnModelChange); + model.bind('all', this._boundOnModelEvent); this.length++; if (!options.silent) this.trigger('add', model); return model; @@ -382,7 +388,7 @@ delete this._byCid[model.cid]; delete model.collection; this.models.splice(this.indexOf(model), 1); - model.unbind('change', this._boundOnModelChange); + model.unbind('all', this._boundOnModelEvent); this.length--; if (!options.silent) this.trigger('remove', model); return model; @@ -394,7 +400,7 @@ options || (options = {}); if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); this.models = this.sortBy(this.comparator); - if (!options.silent) this.trigger('refresh'); + if (!options.silent) this.trigger('refresh', this); return this; }, @@ -412,7 +418,7 @@ } this._initialize(); this.add(models, {silent: true}); - if (!options.silent) this.trigger('refresh'); + if (!options.silent) this.trigger('refresh', this); return this; }, @@ -453,12 +459,18 @@ // Internal method called every time a model in the set fires an event. // Sets need to update their indexes when models change ids. - _onModelChange : function(model) { - if (model.hasChanged('id')) { - delete this._byId[model.formerValue('id')]; - this._byId[model.id] = model; + _onModelEvent : function(ev, model, error) { + switch (ev) { + case 'change': + if (model.hasChanged('id')) { + delete this._byId[model.formerValue('id')]; + this._byId[model.id] = model; + } + this.trigger('change', model); + break; + case 'error': + this.trigger('error', model, error); } - this.trigger('change', model); } }); @@ -537,7 +549,7 @@ // of *[mode]_[group]* on the view's element. Convenient way to swap styles // and behavior. setMode : function(mode, group) { - if (this.modes[group] == mode) return; + if (this.modes[group] === mode) return; $(this.el).setMode(mode, group); this.modes[group] = mode; }, From 626f7d2cfa0b9856aee66d187884142782126f5a Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 7 Oct 2010 10:29:47 -0400 Subject: [PATCH 26/29] removing todo --- TODO | 3 --- backbone.js | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) delete mode 100644 TODO diff --git a/TODO b/TODO deleted file mode 100644 index 54b34394..00000000 --- a/TODO +++ /dev/null @@ -1,3 +0,0 @@ -* Hook up data binding in the view. Both ways. Parallel to models. - -* Add simple validations. diff --git a/backbone.js b/backbone.js index 9ad40b0a..6e2d3818 100644 --- a/backbone.js +++ b/backbone.js @@ -510,7 +510,7 @@ // jQuery lookup, scoped to DOM elements within the current view. // This should be prefered to global jQuery lookups, if you're dealing with // a specific view. - var jQueryScoped = function(selector) { + var jQueryDelegate = function(selector) { return $(selector, this.el); }; @@ -521,8 +521,8 @@ tagName : 'div', // Attach the jQuery function as the `$` and `jQuery` properties. - $ : jQueryScoped, - jQuery : jQueryScoped, + $ : jQueryDelegate, + jQuery : jQueryDelegate, // **render** is the core function that your view should override, in order // to populate its element (`this.el`), with the appropriate HTML. The From 1e8c0b88c63fc697c321662c10f5aae2aa868846 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 7 Oct 2010 11:05:33 -0400 Subject: [PATCH 27/29] a pleasant boney background... --- docs/background.png | Bin 0 -> 2203 bytes index.html | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs/background.png diff --git a/docs/background.png b/docs/background.png new file mode 100644 index 0000000000000000000000000000000000000000..fda0500fbf33099e61270d080f742f098ed42625 GIT binary patch literal 2203 zcmV;M2xRw(P)+9>+*Vp{b`q$T2i1+*b&wS3#&UU+<^=!|Ud=6QoImz*1wVO})WIZ(5l67{WOm<~ue2N~9K^JYfBXnUzO6#GC&8W*fT#%paon2X> zOm^jGUUCxW+QNP$%f@&^U~qBP9k;u=x#8Vv5Q@e}NNLF&0@Ia1ry;b20}d43sYyl`zqnY8;@*rFKw(R0KjlhP4UL$I zi1{QWc89|u`y&k>aTW`SRti-Ve#8QIThLR0J<7`2vYYu?8e5iTS7wedizv$v9Ki`E zGD8!D9$8P4=4TE=90E9!2SDH;xkO&#I$!xA0);J+<$m@AU+9X3ipAN9PB5g5JQ5ANd&df$=+fFzo=S@ z4=F*TyaTyn3+zRQfswbjw^4!+jDdpJ7=X>g9l&y2Rtk(EuB=crR%ew$&(;_MF%030 z49%y!z}`{s$mko{!GYAUL7;5Q1=GT+6$59eK_KTzf_%zfxokVO3?FmC2*%@|-SK$L zs?vc@Fe*5l%LXKFq0|V7P>&>#SpwWo#zw=&u@lUr1DJ@>;N)0o0cAAXM=XyYQB= zWy8QNQIb8-g&yP+Sdvbd12g`uirZ3HXOY`U9Et;+Yg0r-T4|QXF6DDHg!2)M(g7mp zBrZ_rMJ3}|7m+ZTb*aSm2_uzF!V+4hQ(KsF4o3}xJv}{%4>7I?wogL0pHDQ-SLIx; zwG7j;aFx&ru_{Cdl7R-kL+{XtSoCt@5UU<7z_feG=5QBM0!W+*RZeNf)DdhPQGCs! zUQZ2espbcG#-Z2_8_^Fb{E*VLG!Xz&+3xrplb8Gzc2%h%NYFOi3R(fo3*rd3_U|+6 zbK(GzWTL2|sC2WktqfAvX7S;8iOJnr6#L-K3L9tL%4k|<*AH}j;!5TStFgj#ol@`u zdTb8CNygnQ-ZbA&fI;`yhed@ILmJw;uQLznU zn^H&%sPdvaYGrGkEeDWmyj!39`+KPb4CN<#=yVG+Z6 zI%Ca=x<)K52WlL_VEclO_D)hKn2nd)g|@{gl(a!=imRzZe!$AoGkfWCe zZrh5{+ZBd{d)|4C#+^brg-Wa_Fw{pY+9x>D@=A?1RJ!dEzR*HFp=i5d{zBaWMsl!+ znALM5gW$?&c)EdVz*#J%Q#dz|bTLCLnn{*qBq7q`Bd{Q9mEMA~PQL-v;1vv@5~Y|c zjA7dvkNH~tgld1w9Ug%c&a2tlDz*0lvpk}m6)nC8?GoI9T>pXhA2w)M=oSlC9X5MK(0S7(Wyd(sdK2=$X0qe)XrkijHYYS|q_?+pI|q~cUn4DW{6Z9s zKc{N~+JqUXe5&-6QMX*duA$I@(002ovPDHLkV1gx73Jw4O literal 0 HcmV?d00001 diff --git a/index.html b/index.html index 17cb08be..3dfd5e40 100644 --- a/index.html +++ b/index.html @@ -9,7 +9,7 @@ font-size: 15px; line-height: 22px; font-family: Helvetica Neue, Helvetica, Arial; - background: #f4f4f4; + background: #f4f4f4 url(docs/background.png); } .interface { font-family: "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, sans-serif !important; From cc48c76485d947b94e2331f9a89ae6c255bd1b7b Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 7 Oct 2010 11:24:44 -0400 Subject: [PATCH 28/29] Backbone.Bindable -> Backbone.Events --- backbone.js | 10 ++-- docs/backbone.html | 10 ++-- index.html | 131 ++------------------------------------------- test/bindable.js | 6 +-- test/model.js | 2 +- 5 files changed, 19 insertions(+), 140 deletions(-) diff --git a/backbone.js b/backbone.js index 6e2d3818..b71e8593 100644 --- a/backbone.js +++ b/backbone.js @@ -32,7 +32,7 @@ return child; }; - // Backbone.Bindable + // Backbone.Events // ----------------- // A module that can be mixed in to *any object* in order to provide it with @@ -40,11 +40,11 @@ // `trigger`-ing an event fires all callbacks in succession. // // var object = {}; - // _.extend(object, Backbone.Bindable); + // _.extend(object, Backbone.Events); // object.bind('expand', function(){ alert('expanded'); }); // object.trigger('expand'); // - Backbone.Bindable = { + Backbone.Events = { // Bind an event, specified by a string name, `ev`, to a `callback` function. // Passing `"all"` will bind the callback to all events fired. @@ -114,7 +114,7 @@ }; // Attach all inheritable methods to the Model prototype. - _.extend(Backbone.Model.prototype, Backbone.Bindable, { + _.extend(Backbone.Model.prototype, Backbone.Events, { // A snapshot of the model's previous attributes, taken immediately // after the last `changed` event was fired. @@ -308,7 +308,7 @@ }; // Define the Collection's inheritable methods. - _.extend(Backbone.Collection.prototype, Backbone.Bindable, { + _.extend(Backbone.Collection.prototype, Backbone.Events, { model : Backbone.Model, diff --git a/docs/backbone.html b/docs/backbone.html index 4974c9da..a0268b39 100644 --- a/docs/backbone.html +++ b/docs/backbone.html @@ -14,14 +14,14 @@ static properties to be extended.

if (classProps) _.extend(child, classProps); child.prototype.constructor = child; return child; - };

Backbone.Bindable

A module that can be mixed in to any object in order to provide it with + };

Backbone.Events

A module that can be mixed in to any object in order to provide it with custom events. You may bind or unbind a callback function to an event; trigger-ing an event fires all callbacks in succession.

-
_.extend(object, Backbone.Bindable);
+
_.extend(object, Backbone.Events);
 object.bind('expand', function(){ alert('expanded'); });
 object.trigger('expand');
-
  Backbone.Bindable = {

Bind an event, specified by a string name, ev, to a callback function. +

  Backbone.Events = {

Bind an event, specified by a string name, ev, to a callback function. Passing "all" will bind the callback to all events fired.

    bind : function(ev, callback) {
       var calls = this._callbacks || (this._callbacks = {});
       var list  = this._callbacks[ev] || (this._callbacks[ev] = []);
@@ -67,7 +67,7 @@ If you do not specify the id, a negative id will be assigned for you.

this.set(attributes, {silent : true}); this.cid = _.uniqueId('c'); this._formerAttributes = this.attributes(); - };

Attach all inheritable methods to the Model prototype.

  _.extend(Backbone.Model.prototype, Backbone.Bindable, {

A snapshot of the model's previous attributes, taken immediately + };

Attach all inheritable methods to the Model prototype.

  _.extend(Backbone.Model.prototype, Backbone.Events, {

A snapshot of the model's previous attributes, taken immediately after the last changed event was fired.

    _formerAttributes : null,

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 @@ -184,7 +184,7 @@ its models in sort order, as they're added and removed.

Define the Collection's inheritable methods.

  _.extend(Backbone.Collection.prototype, Backbone.Bindable, {
+  };

Define the Collection's inheritable methods.

  _.extend(Backbone.Collection.prototype, Backbone.Events, {
 
     model : Backbone.Model,

Override this function to get convenient logging in the console.

    toString : function() {
       return 'Collection (' + this.length + " models)";
diff --git a/index.html b/index.html
index 3dfd5e40..c03c1e56 100644
--- a/index.html
+++ b/index.html
@@ -98,8 +98,8 @@
       }
       pre {
         font-size: 12px;
-        padding: 2px 0 2px 12px;
-        border-left: 6px solid black;
+        padding: 2px 0 2px 15px;
+        border-left: 2px solid #bbb;
         margin: 0px 0 30px;
       }
     #diagram {
@@ -111,7 +111,7 @@
 
   
- Bindable + Events
  • bind
  • @@ -128,6 +128,7 @@
  • attributes
  • save
  • destroy
  • +
  • validate
  • url
  • clone
  • toString
  • @@ -741,131 +742,9 @@ JST['common/menu']

Change Log

-

- 0.5.3
- You can now embed WOFF-formatted fonts. - Jammit warnings avoid use of Rails.logger. - Bugfixes for Ruby 1.9.2 with asset files containing Unicode characters. -

- -

- 0.5.1
- Included missing rails/routes.rb file in the Gem manifest for Rails 3. -

- -

- 0.5.0
- The Jammit gem is now compatible with Rails 3. JST template packages are - no longer specified separately. This is a backwards-incompatible change, - and you'll have to update your assets.yml file to simply include - the JST in your existing JavaScript packages, as shown above. Alongside - this change, template names are now better namespaced — see the - section on JavaScript Templates for the details. - Bugfix for IE8 and Data-URIs slightly under 32kb in size, as well as - for MHTML in IE7-mode in Vista. -

- -

- 0.4.4
- Jammit will now add the RAILS_ASSET_ID timestamp to image URLs - within your CSS packages, if configured, for better cache-busting. - Greg Hazel contributed a series - of Jammit/Windows bug fixes. -

- -

- 0.4.3
- Bugfix for building on Windows with drive-letter absolute paths. -

- -

- 0.4.2
- Added a logged warning when you have rules in assets.yml that don't - match any files. -

- -

- 0.4.1
- Jammit is now able to embed @font-face web fonts in TTF and - OTF formats. The embed_images option has been renamed to - embed_assets for this change. The assets.yml file is now - passed through ERB before being loaded, so you can configure - environment-specific settings, just like you would with database.yml. -

- -

- 0.3.3
- Added css_compressor_options to assets.yml, so you can - set charset and line_break for the YUI CSS Compressor. -

- -

- 0.3.2
- If Java isn't available on your machine, Jammit will now run in a graceful - degraded mode, where assets are packaged but not compressed. You can now - pass :embed_assets => false to the include_stylesheets - helper to disable image embedding on a per-package basis. -

- -

- 0.2.8
- Jammit now correctly rewrites relative image URLs within CSS, for easier - integration with partial Rails apps deployed on sub-paths. -

- -

- 0.2.7
- The jammit command has been enhanced to check the modification times of - your packages — if no source files have changed, the package isn't - rebuilt. -

- -

- 0.2.6
- Jammit now raises an exception if Java isn't installed, or if the Java - version is unsupported by your JavaScript compressor of choice. -

- -

- 0.2.5
- When specifying your asset packages as directory globs, absolute globs are - now absolute, and relative globs are relative to RAILS_ROOT. -

- -

- 0.2.4
- Jammit now throws a ConfigurationNotFound error when attempting to load - a nonexistent configuration file. Resolved an issue with asset caching from - daemonized mongrels. -

- -

- 0.2.1
- The include_stylesheets helper now takes the same options as the - Rails stylesheet_link_tag helper (such as :media => 'print'). -

- -

- 0.2.0
- Jammit now supports the Google Closure Compiler as an alternative - to the YUI compressor. -

- -

- 0.1.3
- Fixed a bug that conflicted with other plugins trying to alter - ApplicationController in development. -

- -

- 0.1.1
- Added support for embedding images with stylesheet-relative paths. - Shortened the MHTML identifiers. -

-

0.1.0
- Initial Jammit release. + Initial Backbone release.

diff --git a/test/bindable.js b/test/bindable.js index 2c53699a..1d92bfab 100644 --- a/test/bindable.js +++ b/test/bindable.js @@ -4,7 +4,7 @@ $(document).ready(function() { test("bindable: bind and trigger", function() { var obj = { counter: 0 }; - _.extend(obj,Backbone.Bindable); + _.extend(obj,Backbone.Events); obj.bind('event', function() { obj.counter += 1; }); obj.trigger('event'); equals(obj.counter,1,'counter should be incremented.'); @@ -17,7 +17,7 @@ $(document).ready(function() { test("bindable: bind, then unbind all functions", function() { var obj = { counter: 0 }; - _.extend(obj,Backbone.Bindable); + _.extend(obj,Backbone.Events); var callback = function() { obj.counter += 1; }; obj.bind('event', callback); obj.trigger('event'); @@ -28,7 +28,7 @@ $(document).ready(function() { test("bindable: bind two callbacks, unbind only one", function() { var obj = { counterA: 0, counterB: 0 }; - _.extend(obj,Backbone.Bindable); + _.extend(obj,Backbone.Events); var callback = function() { obj.counterA += 1; }; obj.bind('event', callback); obj.bind('event', function() { obj.counterB += 1; }); diff --git a/test/model.js b/test/model.js index 92b2e421..c2797666 100644 --- a/test/model.js +++ b/test/model.js @@ -125,7 +125,7 @@ $(document).ready(function() { model.validate = function(attrs) { if (attrs.admin) return "Can't change admin status."; }; - model.bind('error', function(error) { + model.bind('error', function(model, error) { lastError = error; }); var result = model.set({a: 100}); From ad73393f400577467960e95db414b305c66f8432 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 7 Oct 2010 12:54:56 -0400 Subject: [PATCH 29/29] A real start on the documentation ... intro is done-ish. --- backbone.js | 2 +- docs/backbone.html | 170 ++++++----- docs/docco.css | 9 +- index.html | 729 +++++++++------------------------------------ 4 files changed, 236 insertions(+), 674 deletions(-) diff --git a/backbone.js b/backbone.js index b71e8593..5dcfcbe1 100644 --- a/backbone.js +++ b/backbone.js @@ -1,5 +1,5 @@ // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. -// Backbone may be freely distributed under the terms of the MIT license. +// Backbone may be freely distributed under the MIT license. // For all details and documentation: // http://documentcloud.github.com/backbone diff --git a/docs/backbone.html b/docs/backbone.html index a0268b39..ad805a2b 100644 --- a/docs/backbone.html +++ b/docs/backbone.html @@ -1,5 +1,5 @@ backbone.js

this.comparator=options.comparator;deleteoptions.comparator;} - this._boundOnModelChange=_.bind(this._onModelChange,this); + this._boundOnModelEvent=_.bind(this._onModelEvent,this);this._initialize();if(models)this.refresh(models,true); - };model.collection=this;varindex=this.comparator?this.sortedIndex(model,this.comparator):this.length;this.models.splice(index,0,model); - model.bind('change',this._boundOnModelChange); + model.bind('all',this._boundOnModelEvent);this.length++;if(!options.silent)this.trigger('add',model);returnmodel; - },

backbone.js

(c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
-Backbone may be freely distributed under the terms of the MIT license.
+Backbone may be freely distributed under the MIT license.
 For all details and documentation:
 http://documentcloud.github.com/backbone
 
(function(){

Initial Setup

The top-level namespace.

  var Backbone = {};

Keep the version in sync with package.json.

  Backbone.VERSION = '0.1.0';

Export for both CommonJS and the Browser.

  (typeof exports !== 'undefined' ? exports : this).Backbone = Backbone;

Helper function to correctly set up the prototype chain, for subclasses. @@ -14,11 +14,12 @@ static properties to be extended.

if (classProps) _.extend(child, classProps); child.prototype.constructor = child; return child; - };

Backbone.Events

A module that can be mixed in to any object in order to provide it with + };

Backbone.Events

A module that can be mixed in to any object in order to provide it with custom events. You may bind or unbind a callback function to an event; trigger-ing an event fires all callbacks in succession.

-
_.extend(object, Backbone.Events);
+
var object = {};
+_.extend(object, Backbone.Events);
 object.bind('expand', function(){ alert('expanded'); });
 object.trigger('expand');
 
  Backbone.Events = {

Bind an event, specified by a string name, ev, to a callback function. @@ -48,13 +49,20 @@ for all events.

} } return this; - },

Trigger an event, firing all bound callbacks.

    trigger : function(ev) {
+    },

Trigger an event, firing all bound callbacks. Callbacks are passed the +same arguments as trigger is, apart from the event name. +Listening for "all" passes the true event name as the first argument.

    trigger : function(ev) {
+      var list, calls, i, l;
       var calls = this._callbacks;
-      for (var i = 0; i < 2; i++) {
-        var list = calls && calls[i ? 'all' : ev];
-        if (!list) continue;
-        for (var j = 0, l = list.length; j < l; j++) {
-          list[j].apply(this, _.rest(arguments));
+      if (!(calls = this._callbacks)) return this;
+      if (list = calls[ev]) {
+        for (i = 0, l = list.length; i < l; i++) {
+          list[i].apply(this, _.rest(arguments));
+        }
+      }
+      if (list = calls['all']) {
+        for (i = 0, l = list.length; i < l; i++) {
+          list[i].apply(this, arguments);
         }
       }
       return this;
@@ -63,9 +71,8 @@ for all events.

};

Backbone.Model

Create a new model, with defined attributes. If you do not specify the id, a negative id will be assigned for you.

  Backbone.Model = function(attributes) {
     this._attributes = {};
-    attributes = attributes || {};
-    this.set(attributes, {silent : true});
     this.cid = _.uniqueId('c');
+    this.set(attributes || {}, {silent : true});
     this._formerAttributes = this.attributes();
   };

Attach all inheritable methods to the Model prototype.

  _.extend(Backbone.Model.prototype, Backbone.Events, {

A snapshot of the model's previous attributes, taken immediately after the last changed event was fired.

    _formerAttributes : null,

Has the item been changed since the last changed event?

    _changed : false,

Return a copy of the model's attributes object.

    attributes : function() {
@@ -89,49 +96,48 @@ ID.

},

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 -choose to silence it.

    set : function(attrs, options) {
-      options || (options = {});
+choose to silence it.

    set : function(attrs, options) {

Extract attributes and options.

      options || (options = {});
       if (!attrs) return this;
       attrs = attrs._attributes || attrs;
-      var now = this._attributes;
-      if ('id' in attrs) {
-        this.id = attrs.id;
-        if (this.collection) this.resource = this.collection.resource + '/' + this.id;
-      }
-      for (var attr in attrs) {
+      var now = this._attributes;

Run validation if validate is defined.

      if (this.validate) {
+        var error = this.validate(attrs);
+        if (error) {
+          this.trigger('error', this, error);
+          return false;
+        }
+      }

Check for changes of id.

      if ('id' in attrs) this.id = attrs.id;

Update attributes.

      for (var attr in attrs) {
         var val = attrs[attr];
         if (val === '') val = null;
         if (!_.isEqual(now[attr], val)) {
           now[attr] = val;
           if (!options.silent) {
             this._changed = true;
-            this.trigger('change:' + attr);
+            this.trigger('change:' + attr, this);
           }
         }
-      }
-      if (!options.silent && this._changed) this.changed();
+      }

Fire the change event, if the model has been changed.

      if (!options.silent && this._changed) this.change();
       return this;
-    },

Remove an attribute from the model, firing changed unless you choose to + },

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 = true;
-        this.trigger('change:' + attr);
-        this.changed();
+        this.trigger('change:' + attr, this);
+        this.change();
       }
       return value;
-    },

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() {
+    },

Call this method to fire manually fire a change event for this model. +Calling this will cause all objects observing the model to update.

    change : function() {
       this.trigger('change', this);
       this._formerAttributes = this.attributes();
       this._changed = false;
-    },

Determine if the model has changed since the last changed event. + },

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;
-    },

Return an object containing all the attributes that have changed, or false + },

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) {
@@ -143,26 +149,26 @@ the server.

} } return changed; - },

Get the previous value of an attribute, recorded at the time the last + },

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 + },

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.

    save : function(attrs, options) {
+    },

Set a hash of model attributes, and sync the model to the server.

    save : function(attrs, options) {
       attrs   || (attrs = {});
       options || (options = {});
-      this.set(attrs, options);
+      if (!this.set(attrs, options)) return false;
       var model = this;
       var success = function(resp) {
-        model.set(resp.model);
+        if (!model.set(resp.model)) return false;
         if (options.success) options.success(model, resp);
       };
       var method = this.isNew() ? 'POST' : 'PUT';
       Backbone.request(method, this, success, options.error);
       return this;
-    },

Destroy this model on the server.

    destroy : function(options) {
+    },

Destroy this model on the server.

    destroy : function(options) {
       options || (options = {});
       var model = this;
       var success = function(resp) {
@@ -173,7 +179,7 @@ the server.

return this; } - });

Backbone.Collection

Provides a standard collection class for our sets of models, ordered + });

Backbone.Collection

Provides a standard collection class for our sets of models, ordered or unordered. If a comparator is specified, the Collection will maintain its models in sort order, as they're added and removed.

  Backbone.Collection = function(models, options) {
     options || (options = {});
@@ -181,31 +187,31 @@ its models in sort order, as they're added and removed.

Define the Collection's inheritable methods.

  _.extend(Backbone.Collection.prototype, Backbone.Events, {
+  };

Define the Collection's inheritable methods.

  _.extend(Backbone.Collection.prototype, Backbone.Events, {
 
-    model : Backbone.Model,

Override this function to get convenient logging in the console.

    toString : function() {
+    model : Backbone.Model,

Override this function to get convenient logging in the console.

    toString : function() {
       return 'Collection (' + this.length + " models)";
-    },

Get a model from the set by id.

    get : function(id) {
+    },

Get a model from the set by id.

    get : function(id) {
       return id && this._byId[id.id != null ? id.id : id];
-    },

Get a model from the set by client id.

    getByCid : function(cid) {
+    },

Get a model from the set by client id.

    getByCid : function(cid) {
       return cid && this._byCid[cid.cid || cid];
-    },

Get the model at the given index.

    at: function(index) {
+    },

Get the model at the given index.

    at: function(index) {
       return this.models[index];
-    },

What are the ids for every model in the set?

    getIds : function() {
+    },

What are the ids for every model in the set?

    getIds : function() {
       return _.keys(this._byId);
-    },

What are the client ids for every model in the set?

    getCids : function() {
+    },

What are the client ids for every model in the set?

    getCids : function() {
       return _.keys(this._byCid);
-    },

Pluck an attribute from each model in the collection.

    pluck : function(attr) {
+    },

Pluck an attribute from each model in the collection.

    pluck : function(attr) {
       return _.map(this.models, function(model){ return model.get(attr); });
-    },

Add a model, or list of models to the set. Pass silent to avoid + },

Add a model, or list of models to the set. Pass silent to avoid firing the added event for every new model.

    add : function(models, options) {
       if (!_.isArray(models)) return this._add(models, options);
       for (var i=0; i<models.length; i++) this._add(models[i], options);
       return models;
-    },

Internal implementation of adding a single model to the set.

    _add : function(model, options) {
+    },

Internal implementation of adding a single model to the set.

    _add : function(model, options) {
       options || (options = {});
       var already = this.get(model);
       if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
@@ -214,16 +220,16 @@ firing the added event for every new model.

Remove a model, or a list of models from the set. Pass silent to avoid + },

Remove a model, or a list of models from the set. Pass silent to avoid firing the removed event for every model removed.

    remove : function(models, options) {
       if (!_.isArray(models)) return this._remove(models, options);
       for (var i=0; i<models.length; i++) this._remove(models[i], options);
       return models;
-    },

Internal implementation of removing a single model from the set.

    _remove : function(model, options) {
+    },

Internal implementation of removing a single model from the set.

    _remove : function(model, options) {
       options || (options = {});
       model = this.get(model);
       if (!model) return null;
@@ -231,18 +237,18 @@ firing the removed event for every model removed.

< delete this._byCid[model.cid]; delete model.collection; this.models.splice(this.indexOf(model), 1); - model.unbind('change', this._boundOnModelChange); + model.unbind('all', this._boundOnModelEvent); this.length--; if (!options.silent) this.trigger('remove', model); return model; - },

Force the set to re-sort itself. You don't need to call this under normal + },

Force the set to re-sort itself. You don't need to call this under normal circumstances, as the set will maintain sort order as each item is added.

    sort : function(options) {
       options || (options = {});
       if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
       this.models = this.sortBy(this.comparator);
-      if (!options.silent) this.trigger('refresh');
+      if (!options.silent) this.trigger('refresh', this);
       return this;
-    },

When you have more items than you want to add or remove individually, + },

When you have more items than you want to add or remove individually, you can refresh the entire set with a new list of models, without firing any added or removed events. Fires refreshed when finished.

    refresh : function(models, options) {
       options || (options = {});
@@ -255,9 +261,9 @@ any added or removed events. Fires refreshed}
       this._initialize();
       this.add(models, {silent: true});
-      if (!options.silent) this.trigger('refresh');
+      if (!options.silent) this.trigger('refresh', this);
       return this;
-    },

Fetch the default set of models for this collection, refreshing the + },

Fetch the default set of models for this collection, refreshing the collection.

    fetch : function(options) {
       options || (options = {});
       var collection = this;
@@ -267,39 +273,45 @@ collection.

}; Backbone.request('GET', this, success, options.error); return this; - },

Create a new instance of a model in this collection.

    create : function(model, options) {
+    },

Create a new instance of a model in this collection.

    create : function(model, options) {
       options || (options = {});
       if (!(model instanceof Backbone.Model)) model = new this.model(model);
       model.collection = this;
       var success = function(model, resp) {
-        model.set(resp.model);
+        if (!model.set(resp.model)) return false;
         model.collection.add(model);
         if (options.success) options.success(model, resp);
       };
       return model.save(null, {success : success, error : options.error});
-    },

Initialize or re-initialize all internal state. Called when the + },

Initialize or re-initialize all internal state. Called when the collection is refreshed.

    _initialize : function(options) {
       this.length = 0;
       this.models = [];
       this._byId = {};
       this._byCid = {};
-    },

Internal method called every time a model in the set fires an event. -Sets need to update their indexes when models change ids.

    _onModelChange : function(model) {
-      if (model.hasChanged('id')) {
-        delete this._byId[model.formerValue('id')];
-        this._byId[model.id] = model;
+    },

Internal method called every time a model in the set fires an event. +Sets need to update their indexes when models change ids.

    _onModelEvent : function(ev, model, error) {
+      switch (ev) {
+        case 'change':
+          if (model.hasChanged('id')) {
+            delete this._byId[model.formerValue('id')];
+            this._byId[model.id] = model;
+          }
+          this.trigger('change', model);
+          break;
+        case 'error':
+          this.trigger('error', model, error);
       }
-      this.trigger('change', model);
     }
 
-  });

Underscore methods that we want to implement on the Collection.

  var methods = ['each', 'map', 'reduce', 'reduceRight', 'detect', 'select',
+  });

Underscore methods that we want to implement on the Collection.

  var methods = ['each', 'map', 'reduce', 'reduceRight', 'detect', 'select',
     'reject', 'all', 'any', 'include', 'invoke', 'max', 'min', 'sortBy',
     'sortedIndex', 'toArray', 'size', 'first', 'rest', 'last', 'without',
-    'indexOf', 'lastIndexOf', 'isEmpty'];

Mix in each Underscore method as a proxy to Collection#models.

  _.each(methods, function(method) {
+    'indexOf', 'lastIndexOf', 'isEmpty'];

Mix in each Underscore method as a proxy to Collection#models.

  _.each(methods, function(method) {
     Backbone.Collection.prototype[method] = function() {
       return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
     };
-  });

Backbone.View

Creating a Backbone.View creates its intial element outside of the DOM, + });

Backbone.View

Creating a Backbone.View creates its intial element outside of the DOM, if an existing element is not provided...

  Backbone.View = function(options) {
     this.modes = {};
     this._initialize(options || {});
@@ -312,16 +324,16 @@ if an existing element is not provided...

this.el = this.make(this.tagName, attrs); } return this; - };

jQuery lookup, scoped to DOM elements within the current view. + };

jQuery lookup, scoped to DOM elements within the current view. This should be prefered to global jQuery lookups, if you're dealing with -a specific view.

  var jQueryScoped = function(selector) {
+a specific view.

  var jQueryDelegate = function(selector) {
     return $(selector, this.el);
-  };

Set up all interitable Backbone.View properties and methods.

  _.extend(Backbone.View.prototype, {

The default tagName of a View's element is "div".

    tagName : 'div',

Attach the jQuery function as the $ and jQuery properties.

    $       : jQueryScoped,
-    jQuery  : jQueryScoped,

render is the core function that your view should override, in order + };

Set up all interitable Backbone.View properties and methods.

  _.extend(Backbone.View.prototype, {

The default tagName of a View's element is "div".

    tagName : 'div',

Attach the jQuery function as the $ and jQuery properties.

    $       : jQueryDelegate,
+    jQuery  : jQueryDelegate,

render is the core function that your view should override, in order to populate its element (this.el), with the appropriate HTML. The convention is for render to always return this.

    render : function() {
       return this;
-    },

For small amounts of DOM Elements, where a full-blown template isn't + },

For small amounts of DOM Elements, where a full-blown template isn't needed, use make to manufacture elements, one at a time.

var el = this.make('li', {'class': 'row'}, this.model.get('title'));
@@ -330,15 +342,15 @@ needed, use make to manufacture elements, one at a time.

if (attributes) $(el).attr(attributes); if (content) $(el).html(content); return el; - },

Makes the view enter a mode. Modes have both a 'mode' and a 'group', + },

Makes the view enter a mode. Modes have both a 'mode' and a 'group', and are mutually exclusive with any other modes in the same group. Setting will update the view's modes hash, as well as set an HTML class of [mode]_[group] on the view's element. Convenient way to swap styles and behavior.

    setMode : function(mode, group) {
-      if (this.modes[group] == mode) return;
+      if (this.modes[group] === mode) return;
       $(this.el).setMode(mode, group);
       this.modes[group] = mode;
-    },

Set callbacks, where this.callbacks is a hash of

+ },

Set callbacks, where this.callbacks is a hash of

{selector.event_name: callback_name}

@@ -367,7 +379,7 @@ bubble change events at all.

} } return this; - },

Performs the initial configuration of a View with a set of options. + },

Performs the initial configuration of a View with a set of options. Keys with special meaning (model, collection, id, className), are attatched directly to the view.

    _initialize : function(options) {
       if (this.options) options = _.extend({}, this.options, options);
@@ -378,11 +390,11 @@ attatched directly to the view.

this.options = options; } - });

Set up inheritance for the model, collection, and view.

  var extend = Backbone.Model.extend = Backbone.Collection.extend = Backbone.View.extend = function (protoProps, classProps) {
+  });

Set up inheritance for the model, collection, and view.

  var extend = Backbone.Model.extend = Backbone.Collection.extend = Backbone.View.extend = function (protoProps, classProps) {
     var child = inherits(this, protoProps, classProps);
     child.extend = extend;
     return child;
-  };

Override this function to change the manner in which Backbone persists + };

Override this function to change the manner in which Backbone persists models to the server. You will be passed the type of request, and the model in question. By default, uses jQuery to make a RESTful Ajax request to the model's url(). Some possible customizations could be:

diff --git a/docs/docco.css b/docs/docco.css index 76e818b3..b2e60197 100644 --- a/docs/docco.css +++ b/docs/docco.css @@ -1,8 +1,8 @@ /*--------------------- Layout and Typography ----------------------------*/ body { font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; - font-size: 16px; - line-height: 24px; + font-size: 15px; + line-height: 22px; color: #252519; margin: 0; padding: 0; } @@ -72,10 +72,11 @@ table td { outline: 0; } td.docs, th.docs { - max-width: 500px; - min-width: 500px; + max-width: 450px; + min-width: 450px; min-height: 5px; padding: 10px 25px 1px 50px; + overflow-x: hidden; vertical-align: top; text-align: left; } diff --git a/index.html b/index.html index c03c1e56..ecda7ff4 100644 --- a/index.html +++ b/index.html @@ -6,8 +6,8 @@ Backbone: ...