From eb9f54c8fe9efa878093fa24a32755ad7d6fd43d Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
- refresh
fetchBackbone.Collection
});
- collection.refresh(models, [options])
-
- Adding and removing models one at a time is all well and good, but sometimes
- you have so many models to change that you'd rather just update the collection
- in bulk. Use refresh to replace a collection with a new list
- of models (or attribute hashes), triggering a single "refresh" event
- at the end. Pass {silent: true} to suppress the "refresh" event.
- collection.fetch([options])
@@ -992,6 +982,8 @@ Backbone.Collection
refreshing the collection when they arrive. The options hash takes
success and error
callbacks which will be passed (collection, response) as arguments.
+ When the model data returns from the server, the collection will
+ refresh.
Delegates to Backbone.sync
under the covers, for custom persistence strategies.
@@ -1020,6 +1012,27 @@
+ refreshcollection.refresh(models, [options])
+
+ Adding and removing models one at a time is all well and good, but sometimes
+ you have so many models to change that you'd rather just update the collection
+ in bulk. Use refresh to replace a collection with a new list
+ of models (or attribute hashes), triggering a single "refresh" event
+ at the end. Pass {silent: true} to suppress the "refresh" event.
+
+ Here's an example using refresh to bootstrap a collection during initial page load, + in a Rails application. +
+ ++<script> + Accounts.refresh(<%= @accounts.to_json %>); +</script> +
createcollection.create(attributes, [options])
@@ -1150,7 +1163,9 @@
@@ -1204,11 +1219,7 @@Backbone.View
The default implementation of render is a no-op. Override this function with your code that renders the view template from model data, - and updates this.el with the new HTML. You can use any flavor of - JavaScript templating or DOM-building you prefer. Because Underscore.js - is already on the page, - _.template - is already available. A good + and updates this.el with the new HTML. A good convention is to return this at the end of render to enable chained calls. @@ -1216,12 +1227,34 @@Backbone.View
var Bookmark = Backbone.View.extend({ render: function() { - $(this.el).html(this.template.render(this.model.toJSON())); + $(this.el).html(this.template(this.model.toJSON())); return this; } });++ Backbone is agnostic with respect to your preferred method of HTML templating. + Your render function could even munge together an HTML string, or use + document.createElement to generate a DOM tree. However, we suggest + choosing a nice JavaScript templating library. + Mustache.js, + Haml-js, and + Eco are all fine alternatives. + Because Underscore.js is already on the page, + _.template + is available, and is an excellent choice if you've already XSS-sanitized + your interpolated data. +
+ ++ Whatever templating strategy you end up with, it's nice if you never + have to put strings of HTML in your JavaScript. At DocumentCloud, we + use Jammit in order + to package up JavaScript templates stored in /app/views as part + of our main core.js asset package. +
+make
view.make(tagName, [attributes], [content])
@@ -1279,7 +1312,7 @@Backbone.View
}, render: function() { - $(this.el).html(this.template.render(this.model.toJSON())); + $(this.el).html(this.template(this.model.toJSON())); this.handleEvents(); return this; }, diff --git a/test/collection.js b/test/collection.js index 1499b0950..fb9cec857 100644 --- a/test/collection.js +++ b/test/collection.js @@ -58,20 +58,6 @@ $(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], 'read'); @@ -111,4 +97,23 @@ $(document).ready(function() { equals(col.min(function(model){ return model.id; }).id, 1); }); + 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); + col.refresh(_.map(models, function(m){ return m.attributes; })); + equals(refreshed, 3); + equals(col.length, 4); + ok(col.last() !== a); + ok(_.isEqual(col.last().attributes, a.attributes)); + }); + }); From 09e20c1599073b3325a6aeb59c3df2b379a510dd Mon Sep 17 00:00:00 2001 From: Jeremy AshkenasDate: Thu, 14 Oct 2010 10:46:11 -0400 Subject: [PATCH 03/32] Documenting a collection's 'model' property --- backbone.js | 2 ++ index.html | 33 +++++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/backbone.js b/backbone.js index 832dc8f46..20e5fa6b0 100644 --- a/backbone.js +++ b/backbone.js @@ -317,6 +317,8 @@ // Define the Collection's inheritable methods. _.extend(Backbone.Collection.prototype, Backbone.Events, { + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. model : Backbone.Model, // Add a model, or list of models to the set. Pass **silent** to avoid diff --git a/index.html b/index.html index 4cef29e78..5085f6805 100644 --- a/index.html +++ b/index.html @@ -176,6 +176,7 @@
- – extend
+- – model
- – constructor / initialize
- – models
- – Underscore Methods (24)
@@ -754,6 +755,22 @@Backbone.Collection
providing instance properties, as well as optional classProperties to be attached directly to the collection's constructor function. + ++ model
+ +collection.model
+
+ Override this property to specify the model class that the collection + contains. If defined, you can pass raw attributes objects (and arrays) to + add, create, + and refresh, and the attributes will be + converted into a model of the proper type. ++var Library = Backbone.Collection.extend({ + model: Book +}); +constructor / initialize
new Collection([models], [options])
@@ -834,11 +851,12 @@Backbone.Collection
addcollection.add(models, [options])
Add a model (or an array of models) to the collection. Fires an "add" - event, which you can pass {silent: true} to suppress. + event, which you can pass {silent: true} to suppress. If a + model property is defined, you may also pass + raw attributes objects.-var Ship = Backbone.Model; var ships = new Backbone.Collection; ships.bind("add", function(ship) { @@ -846,8 +864,8 @@@@ -1042,9 +1060,8 @@Backbone.Collection
}); ships.add([ - new Ship({name: "Flying Dutchman"}), - new Ship({name: "Black Pearl"}) + {name: "Flying Dutchman"}, + {name: "Black Pearl"} ]);Backbone.Collection
saving the model to the server, and adding the model to the set after being successfully created. Returns the model, or false if a validation error prevented the - model from being created. In order for this to work, your collection - must have a model property, referencing the type of model that - the collection contains. + model from being created. In order for this to work, your should set the + model property of the collection.@@ -1164,7 +1181,7 @@+Backbone.View
el, id, className, and tagName. If the view defines an initialize function, it will be called when the view is first created. If you'd like to create a view that references - an element already in the DOM, pass in the element as an option: + an element already in the DOM, pass in the element as an option: new View({el: existingElement}) From 0ac41263a084ef87122e63e55e597a842584b098 Mon Sep 17 00:00:00 2001 From: Jeremy AshkenasDate: Thu, 14 Oct 2010 11:10:38 -0400 Subject: [PATCH 04/32] brief aside about sort versus sortBy --- index.html | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 5085f6805..0e71dc9e7 100644 --- a/index.html +++ b/index.html @@ -942,13 +942,21 @@ Backbone.Collection
alert(chapters.pluck('title'));+ + Brief aside: This comparator function is different than JavaScript's regular + "sort", which must return 0, 1, or -1, + and is more similar to a sortBy — a much nicer API. + +
+sort
From b2cb44b8f77543c7c60ed3f47cface89ac437e25 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenascollection.sort([options])
Force a collection to re-sort itself. You don't need to call this under normal circumstances, as a collection with a comparator function - will maintain itself in proper sort order at all times. Triggers the - collection's "refresh" event, unless silenced by passing + will maintain itself in proper sort order at all times. Calling sort + triggers the collection's "refresh" event, unless silenced by passing {silent: true}Date: Thu, 14 Oct 2010 11:13:50 -0400 Subject: [PATCH 05/32] rebuilding annotated source and min.js --- backbone-min.js | 18 ++++----- docs/backbone.html | 94 ++++++++++++++++++++++++---------------------- 2 files changed, 58 insertions(+), 54 deletions(-) diff --git a/backbone-min.js b/backbone-min.js index 55164f8ca..d632e8593 100644 --- a/backbone-min.js +++ b/backbone-min.js @@ -4,12 +4,12 @@ a)this.id=a.id;for(var g in a){d=a[g];if(d==="")d=null;if(!e.isEqual(c[g],d)){c[g]=d;if(!b.silent){this._changed=true;this.trigger("change:"+g,this,d)}}}!b.silent&&this._changed&&this.change();return this},unset:function(a,b){b||(b={});var c=this.attributes[a];delete this.attributes[a];if(!b.silent){this._changed=true;this.trigger("change:"+a,this);this.change()}return c},save:function(a,b){a||(a={});b||(b={});if(!this.set(a,b))return false;var c=this,d=this.isNew()?"create":"update";f.sync(d,this, function(g){if(!c.set(g.model))return false;b.success&&b.success(c,g)},b.error);return this},destroy:function(a){a||(a={});var b=this;f.sync("delete",this,function(c){b.collection&&b.collection.remove(b);a.success&&a.success(b,c)},a.error);return this},url:function(){var a=e.isFunction(this.collection.url)?this.collection.url():this.collection.url;if(this.isNew())return a;return a+"/"+this.id},clone:function(){return new this.constructor(this)},isNew:function(){return!this.id},change:function(){this.trigger("change", this);this._previousAttributes=e.clone(this.attributes);this._changed=false},hasChanged:function(a){if(a)return this._previousAttributes[a]!=this.attributes[a];return this._changed},changedAttributes:function(a){var b=this._previousAttributes;a=a||this.attributes;var c=false,d;for(d in a)if(!e.isEqual(b[d],a[d])){c=c||{};c[d]=a[d]}return c},previous:function(a){if(!a||!this._previousAttributes)return null;return this._previousAttributes[a]},previousAttributes:function(){return e.clone(this._previousAttributes)}}); -f.Collection=function(a,b){b||(b={});if(b.comparator){this.comparator=b.comparator;delete b.comparator}this._boundOnModelEvent=e.bind(this._onModelEvent,this);this._reset();a&&this.refresh(a,{silent:true});this.initialize&&this.initialize(a,b)};e.extend(f.Collection.prototype,f.Events,{model:f.Model,add:function(a,b){if(!e.isArray(a))return this._add(a,b);for(var c=0;c this._reset(); if (models) this.refresh(models, {silent: true}); if (this.initialize) this.initialize(models, options); - };
Define the Collection's inheritable methods.
_.extend(Backbone.Collection.prototype, Backbone.Events, {
-
- model : Backbone.Model,
Add a model, or list of models to the set. Pass silent to avoid + };
Define the Collection's inheritable methods.
_.extend(Backbone.Collection.prototype, Backbone.Events, {
The default model for a collection is just a Backbone.Model. +This should be overridden in most cases.
model : Backbone.Model,
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;
- },
Remove a model, or a list of models from the set. Pass silent to avoid + if (_.isArray(models)) { + for (var i = 0, l = models.length; i < l; i++) { + this._add(models[i], options); + } + } else { + this._add(models, options); + } + return this; + },
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;
- },
Get a model from the set by id.
get : function(id) {
+ if (_.isArray(models)) {
+ for (var i = 0, l = models.length; i < l; i++) {
+ this._remove(models[i], options);
+ }
+ } else {
+ this._remove(models, options);
+ }
+ return this;
+ },
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];
- },
Force the collection to re-sort itself. You don't need to call this under normal + },
Force the collection 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', this);
return this;
- },
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); });
- },
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 refresh
when finished.
refresh : function(models, options) {
+ models || (models = []);
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);
- });
- }
this._reset();
this.add(models, {silent: true});
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 when they arrive.
fetch : function(options) {
options || (options = {});
var collection = this;
@@ -246,7 +249,7 @@
};
Backbone.sync('read', this, success, options.error);
return this;
- },
Create a new instance of a model in this collection. After the model + },
Create a new instance of a model in this collection. After the model has been created on the server, it will be added to the collection.
create : function(model, options) {
options || (options = {});
if (!(model instanceof Backbone.Model)) model = new this.model(model);
@@ -257,15 +260,18 @@
if (options.success) options.success(model, resp);
};
return model.save(null, {success : success, error : options.error});
- },
Reset all internal state. Called when the collection is refreshed.
_reset : function(options) {
+ },
Reset all internal state. Called when the collection is refreshed.
_reset : function(options) {
this.length = 0;
this.models = [];
this._byId = {};
this._byCid = {};
- },
Internal implementation of adding a single model to the set, updating + },
Internal implementation of adding a single model to the set, updating
hash indexes for id
and cid
lookups.
_add : function(model, options) {
options || (options = {});
- var already = this.get(model);
+ if (!(model instanceof Backbone.Model)) {
+ model = new this.model(model);
+ }
+ var already = this.getByCid(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;
@@ -275,11 +281,10 @@
model.bind('all', this._boundOnModelEvent);
this.length++;
if (!options.silent) this.trigger('add', model);
- return model;
- },
Internal implementation of removing a single model from the set, updating + },
Internal implementation of removing a single model from the set, updating
hash indexes for id
and cid
lookups.
_remove : function(model, options) {
options || (options = {});
- model = this.get(model);
+ model = this.getByCid(model);
if (!model) return null;
delete this._byId[model.id];
delete this._byCid[model.cid];
@@ -288,8 +293,7 @@
model.unbind('all', this._boundOnModelEvent);
this.length--;
if (!options.silent) this.trigger('remove', model);
- return model;
- },
Internal method called every time a model in the set fires an event. + },
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':
@@ -304,14 +308,14 @@
}
}
- });
Underscore methods that we want to implement on the Collection.
var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
+ });
Underscore methods that we want to implement on the Collection.
var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
'filter', 'select', 'reject', 'every', 'all', 'some', '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) {
+ 'first', 'rest', 'last', 'without', '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)));
};
- });
Creating a Backbone.View creates its initial element outside of the DOM, + });
Creating a Backbone.View creates its initial element outside of the DOM, if an existing element is not provided...
Backbone.View = function(options) {
this._configure(options || {});
if (this.options.el) {
@@ -323,16 +327,16 @@
this.el = this.make(this.tagName, attrs);
}
if (this.initialize) this.initialize(options);
- };
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 jQueryDelegate = function(selector) {
return $(selector, this.el);
- };
Cached regex to split keys for handleEvents
.
var eventSplitter = /^(\w+)\s*(.*)$/;
Set up all inheritable 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 + };
Cached regex to split keys for handleEvents
.
var eventSplitter = /^(\w+)\s*(.*)$/;
Set up all inheritable 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'));
@@ -341,7 +345,7 @@
if (attributes) $(el).attr(attributes);
if (content) $(el).html(content);
return el;
- },
Set callbacks, where this.callbacks
is a hash of
Set callbacks, where this.callbacks
is a hash of
{"event selector": "callback"}
@@ -370,7 +374,7 @@ } } 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 attached directly to the view.
_configure : function(options) {
if (this.options) options = _.extend({}, this.options, options);
@@ -382,16 +386,16 @@
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;
- };
Map from CRUD to HTTP for our default Backbone.sync
implementation.
var methodMap = {
+ };
Map from CRUD to HTTP for our default Backbone.sync
implementation.
var methodMap = {
'create': 'POST',
'update': 'PUT',
'delete': 'DELETE',
'read' : 'GET'
- };
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:
The server handler for fetch requests should return a JSON list of models, namespaced under "models": {"models": [...]} — - additional information can be returned with the response under different keys. + instead of returning the + array directly, we ask you to namespace your models like this by default, + so that it's possible to send down out-of-band information + for things like pagination or error states.
From 82365e392ea471c504ebcd3787d2d71578ec644b Mon Sep 17 00:00:00 2001 From: Jeremy AshkenasDate: Thu, 14 Oct 2010 13:04:11 -0400 Subject: [PATCH 07/32] internal Collection#_add and Collection#_remove, should return the model, in case they're overridden. --- backbone.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backbone.js b/backbone.js index 20e5fa6b0..cb407ea93 100644 --- a/backbone.js +++ b/backbone.js @@ -441,6 +441,7 @@ model.bind('all', this._boundOnModelEvent); this.length++; if (!options.silent) this.trigger('add', model); + return model; }, // Internal implementation of removing a single model from the set, updating @@ -456,6 +457,7 @@ model.unbind('all', this._boundOnModelEvent); this.length--; if (!options.silent) this.trigger('remove', model); + return model; }, // Internal method called every time a model in the set fires an event. From 7c901e2245f26f8c14bfe35df7ca333ae6874f02 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 13:15:25 -0400 Subject: [PATCH 08/32] Slightly shallower namespaced export for CommonJS. --- backbone.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/backbone.js b/backbone.js index cb407ea93..4d1a6c4bf 100644 --- a/backbone.js +++ b/backbone.js @@ -8,21 +8,24 @@ // Initial Setup // ------------- - // The top-level namespace. - var Backbone = {}; - - // Keep the version here in sync with `package.json`. + // The top-level namespace. All public Backbone classes and modules will + // be attached to this. Exported for both CommonJS and the browser. + var Backbone; + if (typeof exports !== 'undefined') { + Backbone = exports; + } else { + Backbone = this.Backbone = {}; + } + + // Current version of the library. Keep in sync with `package.json`. Backbone.VERSION = '0.1.1'; - // Export for both CommonJS and the browser. - (typeof exports !== 'undefined' ? exports : this).Backbone = Backbone; - // Require Underscore, if we're on the server, and it's not already present. var _ = this._; if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._; // For Backbone's purposes, jQuery owns the `$` variable. - var $ = this.$; + var $ = this.jQuery; // Helper function to correctly set up the prototype chain, for subclasses. // Similar to `goog.inherits`, but uses a hash of prototype properties and From 9c535ca5a55fa2003146270e745258415b6083a4 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 13:31:19 -0400 Subject: [PATCH 09/32] expand inherits helper child constructor creation, for clarity. --- backbone.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backbone.js b/backbone.js index 4d1a6c4bf..064b497bb 100644 --- a/backbone.js +++ b/backbone.js @@ -31,8 +31,12 @@ // Similar to `goog.inherits`, but uses a hash of prototype properties and // class properties to be extended. var inherits = function(parent, protoProps, classProps) { - var child = protoProps.hasOwnProperty('constructor') ? protoProps.constructor : - function(){ return parent.apply(this, arguments); }; + var child; + if (protoProps.hasOwnProperty('constructor')) { + child = protoProps.constructor; + } else { + child = function(){ return parent.apply(this, arguments); }; + } var ctor = function(){}; ctor.prototype = parent.prototype; child.prototype = new ctor(); From 2ae60985eee091504c9bd23d209229805e78ce13 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 13:34:00 -0400 Subject: [PATCH 10/32] Moving all helper functions down to the bottom. --- backbone.js | 56 +++++++++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/backbone.js b/backbone.js index 064b497bb..e9999352a 100644 --- a/backbone.js +++ b/backbone.js @@ -27,31 +27,6 @@ // For Backbone's purposes, jQuery owns the `$` variable. var $ = this.jQuery; - // Helper function to correctly set up the prototype chain, for subclasses. - // Similar to `goog.inherits`, but uses a hash of prototype properties and - // class properties to be extended. - var inherits = function(parent, protoProps, classProps) { - var child; - if (protoProps.hasOwnProperty('constructor')) { - child = protoProps.constructor; - } else { - child = function(){ return parent.apply(this, arguments); }; - } - var ctor = function(){}; - ctor.prototype = parent.prototype; - child.prototype = new ctor(); - _.extend(child.prototype, protoProps); - if (classProps) _.extend(child, classProps); - child.prototype.constructor = child; - return child; - }; - - // Helper function to get a URL from a Model or Collection as a property - // or as a function. - var getUrl = function(object) { - return _.isFunction(object.url) ? object.url() : object.url; - }; - // Backbone.Events // ----------------- @@ -616,6 +591,9 @@ 'read' : 'GET' }; + // Backbone.sync + // ------------- + // 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 @@ -636,4 +614,32 @@ }); }; + // Helpers + // ------- + + // Helper function to correctly set up the prototype chain, for subclasses. + // Similar to `goog.inherits`, but uses a hash of prototype properties and + // class properties to be extended. + var inherits = function(parent, protoProps, classProps) { + var child; + if (protoProps.hasOwnProperty('constructor')) { + child = protoProps.constructor; + } else { + child = function(){ return parent.apply(this, arguments); }; + } + var ctor = function(){}; + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + _.extend(child.prototype, protoProps); + if (classProps) _.extend(child, classProps); + child.prototype.constructor = child; + return child; + }; + + // Helper function to get a URL from a Model or Collection as a property + // or as a function. + var getUrl = function(object) { + return _.isFunction(object.url) ? object.url() : object.url; + }; + })(); From 3560062c11a7919688c861c2b4c5dd86ff3e13c5 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 13:49:01 -0400 Subject: [PATCH 11/32] removing redundant assignment in Events#trigger --- backbone.js | 1 - 1 file changed, 1 deletion(-) diff --git a/backbone.js b/backbone.js index e9999352a..58b6bdd55 100644 --- a/backbone.js +++ b/backbone.js @@ -79,7 +79,6 @@ // Listening for `"all"` passes the true event name as the first argument. trigger : function(ev) { var list, calls, i, l; - var calls = this._callbacks; if (!(calls = this._callbacks)) return this; if (list = calls[ev]) { for (i = 0, l = list.length; i < l; i++) { From e7ce57cc1dbb2b5d3048428333153453baf3817a Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 14:46:53 -0400 Subject: [PATCH 12/32] Adding the beginnings of a speed suite to the Test page. --- backbone.js | 2 +- test/collection.js | 22 +- test/{bindable.js => events.js} | 8 +- test/model.js | 22 +- test/speed.js | 25 ++ test/test.html | 7 +- test/vendor/jslitmus.js | 649 ++++++++++++++++++++++++++++++++ test/view.js | 15 +- 8 files changed, 715 insertions(+), 35 deletions(-) rename test/{bindable.js => events.js} (84%) create mode 100644 test/speed.js create mode 100644 test/vendor/jslitmus.js diff --git a/backbone.js b/backbone.js index 58b6bdd55..d0a1acc93 100644 --- a/backbone.js +++ b/backbone.js @@ -82,7 +82,7 @@ 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)); + list[i].apply(this, Array.prototype.slice.call(arguments, 1)); } } if (list = calls['all']) { diff --git a/test/collection.js b/test/collection.js index fb9cec857..de5c974f3 100644 --- a/test/collection.js +++ b/test/collection.js @@ -1,6 +1,6 @@ $(document).ready(function() { - module("Backbone collections"); + module("Backbone.Collection"); window.lastRequest = null; @@ -15,7 +15,7 @@ $(document).ready(function() { var e = null; var col = window.col = new Backbone.Collection([a,b,c,d]); - test("collections: new and sort", function() { + test("Collection: 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,21 +25,21 @@ $(document).ready(function() { equals(col.length, 4); }); - test("collections: get, getByCid", function() { + test("Collection: get, getByCid", function() { equals(col.get(1), d); equals(col.get(3), b); equals(col.getByCid(col.first().cid), col.first()); }); - test("collections: at", function() { + test("Collection: at", function() { equals(col.at(2), b); }); - test("collections: pluck", function() { + test("Collection: pluck", function() { equals(col.pluck('label').join(' '), 'd c b a'); }); - test("collections: add", function() { + test("Collection: add", function() { var added = null; col.bind('add', function(model){ added = model.get('label'); }); e = new Backbone.Model({id: 0, label : 'e'}); @@ -49,7 +49,7 @@ $(document).ready(function() { equals(col.first(), e); }); - test("collections: remove", function() { + test("Collection: remove", function() { var removed = null; col.bind('remove', function(model){ removed = model.get('label'); }); col.remove(e); @@ -58,13 +58,13 @@ $(document).ready(function() { equals(col.first(), d); }); - test("collections: fetch", function() { + test("Collection: fetch", function() { col.fetch(); equals(lastRequest[0], 'read'); equals(lastRequest[1], col); }); - test("collections: create", function() { + test("Collection: create", function() { var model = col.create({label: 'f'}); equals(lastRequest[0], 'create'); equals(lastRequest[1], model); @@ -82,7 +82,7 @@ $(document).ready(function() { equals(coll.one, 1); }); - test("collections: Underscore methods", function() { + test("Collection: 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); @@ -97,7 +97,7 @@ $(document).ready(function() { equals(col.min(function(model){ return model.id; }).id, 1); }); - test("collections: refresh", function() { + test("Collection: refresh", function() { var refreshed = 0; var models = col.models; col.bind('refresh', function() { refreshed += 1; }); diff --git a/test/bindable.js b/test/events.js similarity index 84% rename from test/bindable.js rename to test/events.js index 1d92bfab7..838084289 100644 --- a/test/bindable.js +++ b/test/events.js @@ -1,8 +1,8 @@ $(document).ready(function() { - module("Backbone bindable"); + module("Backbone.Events"); - test("bindable: bind and trigger", function() { + test("Events: bind and trigger", function() { var obj = { counter: 0 }; _.extend(obj,Backbone.Events); obj.bind('event', function() { obj.counter += 1; }); @@ -15,7 +15,7 @@ $(document).ready(function() { equals(obj.counter, 5, 'counter should be incremented five times.'); }); - test("bindable: bind, then unbind all functions", function() { + test("Events: bind, then unbind all functions", function() { var obj = { counter: 0 }; _.extend(obj,Backbone.Events); var callback = function() { obj.counter += 1; }; @@ -26,7 +26,7 @@ $(document).ready(function() { equals(obj.counter, 1, 'counter should have only been incremented once.'); }); - test("bindable: bind two callbacks, unbind only one", function() { + test("Events: bind two callbacks, unbind only one", function() { var obj = { counterA: 0, counterB: 0 }; _.extend(obj,Backbone.Events); var callback = function() { obj.counterA += 1; }; diff --git a/test/model.js b/test/model.js index f10e9145f..cedb00509 100644 --- a/test/model.js +++ b/test/model.js @@ -1,6 +1,6 @@ $(document).ready(function() { - module("Backbone model"); + module("Backbone.Model"); // Variable to catch the last request. window.lastRequest = null; @@ -26,7 +26,7 @@ $(document).ready(function() { var collection = new klass(); collection.add(doc); - test("model: initialize", function() { + test("Model: initialize", function() { var Model = Backbone.Model.extend({ initialize: function() { this.one = 1; @@ -36,11 +36,11 @@ $(document).ready(function() { equals(model.one, 1); }); - test("model: url", function() { + test("Model: url", function() { equals(doc.url(), '/collection/1-the-tempest'); }); - test("model: clone", function() { + test("Model: clone", function() { attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; a = new Backbone.Model(attrs); b = a.clone(); @@ -55,7 +55,7 @@ $(document).ready(function() { equals(b.get('foo'), 1, "Changing a parent attribute does not change the clone."); }); - test("model: isNew", function() { + test("Model: isNew", function() { attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; a = new Backbone.Model(attrs); ok(a.isNew(), "it should be new"); @@ -63,12 +63,12 @@ $(document).ready(function() { ok(a.isNew(), "any defined ID is legal, negative or positive"); }); - test("model: get", function() { + test("Model: get", function() { equals(doc.get('title'), 'The Tempest'); equals(doc.get('author'), 'Bill Shakespeare'); }); - test("model: set and unset", function() { + test("Model: set and unset", function() { attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; a = new Backbone.Model(attrs); var changeCount = 0; @@ -85,7 +85,7 @@ $(document).ready(function() { ok(changeCount == 2, "Change count should have incremented for unset."); }); - test("model: changed, hasChanged, changedAttributes, previous, previousAttributes", function() { + test("Model: changed, hasChanged, changedAttributes, previous, previousAttributes", function() { var model = new Backbone.Model({name : "Tim", age : 10}); model.bind('change', function() { ok(model.hasChanged('name'), 'name changed'); @@ -99,19 +99,19 @@ $(document).ready(function() { equals(model.get('name'), 'Rob'); }); - test("model: save", function() { + test("Model: save", function() { doc.save({title : "Henry V"}); equals(lastRequest[0], 'update'); ok(_.isEqual(lastRequest[1], doc)); }); - test("model: destroy", function() { + test("Model: destroy", function() { doc.destroy(); equals(lastRequest[0], 'delete'); ok(_.isEqual(lastRequest[1], doc)); }); - test("model: validate", function() { + test("Model: validate", function() { var lastError; var model = new Backbone.Model(); model.validate = function(attrs) { diff --git a/test/speed.js b/test/speed.js new file mode 100644 index 000000000..0f29d47f6 --- /dev/null +++ b/test/speed.js @@ -0,0 +1,25 @@ +(function(){ + + var object = {}; + _.extend(object, Backbone.Events); + var fn = function(){}; + + JSLitmus.test('Events: bind + unbind', function() { + object.bind("event", fn); + object.unbind("event", fn); + }); + + object.bind('test:trigger', fn); + + JSLitmus.test('Events: trigger', function() { + object.trigger('test:trigger'); + }); + + object.bind('test:trigger2', fn); + object.bind('test:trigger2', fn); + + JSLitmus.test('Events: trigger 2 functions, passing 5 arguments', function() { + object.trigger('test:trigger2', 1, 2, 3, 4, 5); + }); + +})(); \ No newline at end of file diff --git a/test/test.html b/test/test.html index 4ecd38630..22d16f04a 100644 --- a/test/test.html +++ b/test/test.html @@ -5,18 +5,23 @@ + - + + Backbone Test Suite
+
+Backbone Speed Suite
+