From eb9f54c8fe9efa878093fa24a32755ad7d6fd43d Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 07:36:11 -0400 Subject: [PATCH 01/32] willbailey's patch to use getByCid for internal lookups ... much safer. --- backbone.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backbone.js b/backbone.js index 3a029d08b..bc08d58b8 100644 --- a/backbone.js +++ b/backbone.js @@ -422,7 +422,7 @@ // hash indexes for `id` and `cid` lookups. _add : function(model, options) { options || (options = {}); - var already = this.get(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; @@ -439,7 +439,7 @@ // 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]; From 831090329f21ed2e7e33fda92b232cd244314b6f Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 10:29:18 -0400 Subject: [PATCH 02/32] Issue #8 -- a number of improvements to the documentation. --- backbone.js | 35 ++++++++++++--------- index.html | 77 +++++++++++++++++++++++++++++++++------------- test/collection.js | 33 +++++++++++--------- 3 files changed, 94 insertions(+), 51 deletions(-) diff --git a/backbone.js b/backbone.js index bc08d58b8..832dc8f46 100644 --- a/backbone.js +++ b/backbone.js @@ -322,17 +322,27 @@ // 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; isort
  • pluck
  • url
  • -
  • refresh
  • fetch
  • +
  • refresh
  • create
  • @@ -975,16 +975,6 @@

    Backbone.Collection

    }); -

    - 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. -

    -

    fetchcollection.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.

    @@ -1007,10 +999,10 @@

    Backbone.Collection

    alert(method + ": " + model.url); }; -var accounts = new Backbone.Collection; -accounts.url = '/accounts'; +var Accounts = new Backbone.Collection; +Accounts.url = '/accounts'; -accounts.fetch(); +Accounts.fetch();

    @@ -1020,6 +1012,27 @@

    Backbone.Collection

    for interfaces that are not needed immediately: for example, documents with collections of notes that may be toggled open and closed.

    + +

    + 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 @@

    Backbone.View

    model, collection, el, id, className, and tagName. If the view defines an initialize function, it will be called when - the view is first created. + 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: + new View({el: existingElement})

    @@ -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. +

    +

    makeview.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 Ashkenas Date: 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.

      + +

      + modelcollection.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 / initializenew 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 @@ 

      Backbone.Collection

      }); ships.add([ - new Ship({name: "Flying Dutchman"}), - new Ship({name: "Black Pearl"}) + {name: "Flying Dutchman"}, + {name: "Black Pearl"} ]);
      @@ -1042,9 +1060,8 @@

      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 Ashkenas Date: 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. + +

      +

      sortcollection.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}

      From b2cb44b8f77543c7c60ed3f47cface89ac437e25 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas 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;cthis._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)));
         };
    -  });

    Backbone.View

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

    Backbone.View

    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:

    From a797829c8d6e101d1c8ff28502c5133fdeefc25e Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 11:48:03 -0400 Subject: [PATCH 06/32] a note about namespacing JSON requests. --- index.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 0e71dc9e7..1c52de2cb 100644 --- a/index.html +++ b/index.html @@ -1017,7 +1017,10 @@

    Backbone.Collection

    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 Ashkenas 
    Date: 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

      +
      diff --git a/test/vendor/jslitmus.js b/test/vendor/jslitmus.js new file mode 100644 index 000000000..a4111791f --- /dev/null +++ b/test/vendor/jslitmus.js @@ -0,0 +1,649 @@ +// JSLitmus.js +// +// Copyright (c) 2010, Robert Kieffer, http://broofa.com +// Available under MIT license (http://en.wikipedia.org/wiki/MIT_License) + +(function() { + // Private methods and state + + // Get platform info but don't go crazy trying to recognize everything + // that's out there. This is just for the major platforms and OSes. + var platform = 'unknown platform', ua = navigator.userAgent; + + // Detect OS + var oses = ['Windows','iPhone OS','(Intel |PPC )?Mac OS X','Linux'].join('|'); + var pOS = new RegExp('((' + oses + ') [^ \);]*)').test(ua) ? RegExp.$1 : null; + if (!pOS) pOS = new RegExp('((' + oses + ')[^ \);]*)').test(ua) ? RegExp.$1 : null; + + // Detect browser + var pName = /(Chrome|MSIE|Safari|Opera|Firefox)/.test(ua) ? RegExp.$1 : null; + + // Detect version + var vre = new RegExp('(Version|' + pName + ')[ \/]([^ ;]*)'); + var pVersion = (pName && vre.test(ua)) ? RegExp.$2 : null; + var platform = (pOS && pName && pVersion) ? pName + ' ' + pVersion + ' on ' + pOS : 'unknown platform'; + + /** + * A smattering of methods that are needed to implement the JSLitmus testbed. + */ + var jsl = { + /** + * Enhanced version of escape() + */ + escape: function(s) { + s = s.replace(/,/g, '\\,'); + s = escape(s); + s = s.replace(/\+/g, '%2b'); + s = s.replace(/ /g, '+'); + return s; + }, + + /** + * Get an element by ID. + */ + $: function(id) { + return document.getElementById(id); + }, + + /** + * Null function + */ + F: function() {}, + + /** + * Set the status shown in the UI + */ + status: function(msg) { + var el = jsl.$('jsl_status'); + if (el) el.innerHTML = msg || ''; + }, + + /** + * Convert a number to an abbreviated string like, "15K" or "10M" + */ + toLabel: function(n) { + if (n == Infinity) { + return 'Infinity'; + } else if (n > 1e9) { + n = Math.round(n/1e8); + return n/10 + 'B'; + } else if (n > 1e6) { + n = Math.round(n/1e5); + return n/10 + 'M'; + } else if (n > 1e3) { + n = Math.round(n/1e2); + return n/10 + 'K'; + } + return n; + }, + + /** + * Copy properties from src to dst + */ + extend: function(dst, src) { + for (var k in src) dst[k] = src[k]; return dst; + }, + + /** + * Like Array.join(), but for the key-value pairs in an object + */ + join: function(o, delimit1, delimit2) { + if (o.join) return o.join(delimit1); // If it's an array + var pairs = []; + for (var k in o) pairs.push(k + delimit1 + o[k]); + return pairs.join(delimit2); + }, + + /** + * Array#indexOf isn't supported in IE, so we use this as a cross-browser solution + */ + indexOf: function(arr, o) { + if (arr.indexOf) return arr.indexOf(o); + for (var i = 0; i < this.length; i++) if (arr[i] === o) return i; + return -1; + } + }; + + /** + * Test manages a single test (created with + * JSLitmus.test()) + * + * @private + */ + var Test = function (name, f) { + if (!f) throw new Error('Undefined test function'); + if (!/function[^\(]*\(([^,\)]*)/.test(f.toString())) { + throw new Error('"' + name + '" test: Test is not a valid Function object'); + } + this.loopArg = RegExp.$1; + this.name = name; + this.f = f; + }; + + jsl.extend(Test, /** @lends Test */ { + /** Calibration tests for establishing iteration loop overhead */ + CALIBRATIONS: [ + new Test('calibrating loop', function(count) {while (count--);}), + new Test('calibrating function', jsl.F) + ], + + /** + * Run calibration tests. Returns true if calibrations are not yet + * complete (in which case calling code should run the tests yet again). + * onCalibrated - Callback to invoke when calibrations have finished + */ + calibrate: function(onCalibrated) { + for (var i = 0; i < Test.CALIBRATIONS.length; i++) { + var cal = Test.CALIBRATIONS[i]; + if (cal.running) return true; + if (!cal.count) { + cal.isCalibration = true; + cal.onStop = onCalibrated; + //cal.MIN_TIME = .1; // Do calibrations quickly + cal.run(2e4); + return true; + } + } + return false; + } + }); + + jsl.extend(Test.prototype, {/** @lends Test.prototype */ + /** Initial number of iterations */ + INIT_COUNT: 10, + /** Max iterations allowed (i.e. used to detect bad looping functions) */ + MAX_COUNT: 1e9, + /** Minimum time a test should take to get valid results (secs) */ + MIN_TIME: .5, + + /** Callback invoked when test state changes */ + onChange: jsl.F, + + /** Callback invoked when test is finished */ + onStop: jsl.F, + + /** + * Reset test state + */ + reset: function() { + delete this.count; + delete this.time; + delete this.running; + delete this.error; + }, + + /** + * Run the test (in a timeout). We use a timeout to make sure the browser + * has a chance to finish rendering any UI changes we've made, like + * updating the status message. + */ + run: function(count) { + count = count || this.INIT_COUNT; + jsl.status(this.name + ' x ' + count); + this.running = true; + var me = this; + setTimeout(function() {me._run(count);}, 200); + }, + + /** + * The nuts and bolts code that actually runs a test + */ + _run: function(count) { + var me = this; + + // Make sure calibration tests have run + if (!me.isCalibration && Test.calibrate(function() {me.run(count);})) return; + this.error = null; + + try { + var start, f = this.f, now, i = count; + + // Start the timer + start = new Date(); + + // Now for the money shot. If this is a looping function ... + if (this.loopArg) { + // ... let it do the iteration itself + f(count); + } else { + // ... otherwise do the iteration for it + while (i--) f(); + } + + // Get time test took (in secs) + this.time = Math.max(1,new Date() - start)/1000; + + // Store iteration count and per-operation time taken + this.count = count; + this.period = this.time/count; + + // Do we need to do another run? + this.running = this.time <= this.MIN_TIME; + + // ... if so, compute how many times we should iterate + if (this.running) { + // Bump the count to the nearest power of 2 + var x = this.MIN_TIME/this.time; + var pow = Math.pow(2, Math.max(1, Math.ceil(Math.log(x)/Math.log(2)))); + count *= pow; + if (count > this.MAX_COUNT) { + throw new Error('Max count exceeded. If this test uses a looping function, make sure the iteration loop is working properly.'); + } + } + } catch (e) { + // Exceptions are caught and displayed in the test UI + this.reset(); + this.error = e; + } + + // Figure out what to do next + if (this.running) { + me.run(count); + } else { + jsl.status(''); + me.onStop(me); + } + + // Finish up + this.onChange(this); + }, + + /** + * Get the number of operations per second for this test. + * + * @param normalize if true, iteration loop overhead taken into account + */ + getHz: function(/**Boolean*/ normalize) { + var p = this.period; + + // Adjust period based on the calibration test time + if (normalize && !this.isCalibration) { + var cal = Test.CALIBRATIONS[this.loopArg ? 0 : 1]; + + // If the period is within 20% of the calibration time, then zero the + // it out + p = p < cal.period*1.2 ? 0 : p - cal.period; + } + + return Math.round(1/p); + }, + + /** + * Get a friendly string describing the test + */ + toString: function() { + return this.name + ' - ' + this.time/this.count + ' secs'; + } + }); + + // CSS we need for the UI + var STYLESHEET = ''; + + // HTML markup for the UI + var MARKUP = '
      \ + \ + \ +
      \ +
      \ + Normalize results \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
      ' + platform + '
      TestOps/sec
      \ +
      \ + \ + Powered by JSLitmus \ +
      '; + + /** + * The public API for creating and running tests + */ + window.JSLitmus = { + /** The list of all tests that have been registered with JSLitmus.test */ + _tests: [], + /** The queue of tests that need to be run */ + _queue: [], + + /** + * The parsed query parameters the current page URL. This is provided as a + * convenience for test functions - it's not used by JSLitmus proper + */ + params: {}, + + /** + * Initialize + */ + _init: function() { + // Parse query params into JSLitmus.params[] hash + var match = (location + '').match(/([^?#]*)(#.*)?$/); + if (match) { + var pairs = match[1].split('&'); + for (var i = 0; i < pairs.length; i++) { + var pair = pairs[i].split('='); + if (pair.length > 1) { + var key = pair.shift(); + var value = pair.length > 1 ? pair.join('=') : pair[0]; + this.params[key] = value; + } + } + } + + // Write out the stylesheet. We have to do this here because IE + // doesn't honor sheets written after the document has loaded. + document.write(STYLESHEET); + + // Setup the rest of the UI once the document is loaded + if (window.addEventListener) { + window.addEventListener('load', this._setup, false); + } else if (document.addEventListener) { + document.addEventListener('load', this._setup, false); + } else if (window.attachEvent) { + window.attachEvent('onload', this._setup); + } + + return this; + }, + + /** + * Set up the UI + */ + _setup: function() { + var el = jsl.$('jslitmus_container'); + if (!el) document.body.appendChild(el = document.createElement('div')); + + el.innerHTML = MARKUP; + + // Render the UI for all our tests + for (var i=0; i < JSLitmus._tests.length; i++) + JSLitmus.renderTest(JSLitmus._tests[i]); + }, + + /** + * (Re)render all the test results + */ + renderAll: function() { + for (var i = 0; i < JSLitmus._tests.length; i++) + JSLitmus.renderTest(JSLitmus._tests[i]); + JSLitmus.renderChart(); + }, + + /** + * (Re)render the chart graphics + */ + renderChart: function() { + var url = JSLitmus.chartUrl(); + jsl.$('chart_link').href = url; + jsl.$('chart_image').src = url; + jsl.$('chart').style.display = ''; + + // Update the tiny URL + jsl.$('tiny_url').src = 'http://tinyurl.com/api-create.php?url='+escape(url); + }, + + /** + * (Re)render the results for a specific test + */ + renderTest: function(test) { + // Make a new row if needed + if (!test._row) { + var trow = jsl.$('test_row_template'); + if (!trow) return; + + test._row = trow.cloneNode(true); + test._row.style.display = ''; + test._row.id = ''; + test._row.onclick = function() {JSLitmus._queueTest(test);}; + test._row.title = 'Run ' + test.name + ' test'; + trow.parentNode.appendChild(test._row); + test._row.cells[0].innerHTML = test.name; + } + + var cell = test._row.cells[1]; + var cns = [test.loopArg ? 'test_looping' : 'test_nonlooping']; + + if (test.error) { + cns.push('test_error'); + cell.innerHTML = + '
      ' + test.error + '
      ' + + '
      • ' + + jsl.join(test.error, ': ', '
      • ') + + '
      '; + } else { + if (test.running) { + cns.push('test_running'); + cell.innerHTML = 'running'; + } else if (jsl.indexOf(JSLitmus._queue, test) >= 0) { + cns.push('test_pending'); + cell.innerHTML = 'pending'; + } else if (test.count) { + cns.push('test_done'); + var hz = test.getHz(jsl.$('test_normalize').checked); + cell.innerHTML = hz != Infinity ? hz : '∞'; + cell.title = 'Looped ' + test.count + ' times in ' + test.time + ' seconds'; + } else { + cell.innerHTML = 'ready'; + } + } + cell.className = cns.join(' '); + }, + + /** + * Create a new test + */ + test: function(name, f) { + // Create the Test object + var test = new Test(name, f); + JSLitmus._tests.push(test); + + // Re-render if the test state changes + test.onChange = JSLitmus.renderTest; + + // Run the next test if this one finished + test.onStop = function(test) { + if (JSLitmus.onTestFinish) JSLitmus.onTestFinish(test); + JSLitmus.currentTest = null; + JSLitmus._nextTest(); + }; + + // Render the new test + this.renderTest(test); + }, + + /** + * Add all tests to the run queue + */ + runAll: function(e) { + e = e || window.event; + var reverse = e && e.shiftKey, len = JSLitmus._tests.length; + for (var i = 0; i < len; i++) { + JSLitmus._queueTest(JSLitmus._tests[!reverse ? i : (len - i - 1)]); + } + }, + + /** + * Remove all tests from the run queue. The current test has to finish on + * it's own though + */ + stop: function() { + while (JSLitmus._queue.length) { + var test = JSLitmus._queue.shift(); + JSLitmus.renderTest(test); + } + }, + + /** + * Run the next test in the run queue + */ + _nextTest: function() { + if (!JSLitmus.currentTest) { + var test = JSLitmus._queue.shift(); + if (test) { + jsl.$('stop_button').disabled = false; + JSLitmus.currentTest = test; + test.run(); + JSLitmus.renderTest(test); + if (JSLitmus.onTestStart) JSLitmus.onTestStart(test); + } else { + jsl.$('stop_button').disabled = true; + JSLitmus.renderChart(); + } + } + }, + + /** + * Add a test to the run queue + */ + _queueTest: function(test) { + if (jsl.indexOf(JSLitmus._queue, test) >= 0) return; + JSLitmus._queue.push(test); + JSLitmus.renderTest(test); + JSLitmus._nextTest(); + }, + + /** + * Generate a Google Chart URL that shows the data for all tests + */ + chartUrl: function() { + var n = JSLitmus._tests.length, markers = [], data = []; + var d, min = 0, max = -1e10; + var normalize = jsl.$('test_normalize').checked; + + // Gather test data + for (var i=0; i < JSLitmus._tests.length; i++) { + var test = JSLitmus._tests[i]; + if (test.count) { + var hz = test.getHz(normalize); + var v = hz != Infinity ? hz : 0; + data.push(v); + markers.push('t' + jsl.escape(test.name + '(' + jsl.toLabel(hz)+ ')') + ',000000,0,' + + markers.length + ',10'); + max = Math.max(v, max); + } + } + if (markers.length <= 0) return null; + + // Build chart title + var title = document.getElementsByTagName('title'); + title = (title && title.length) ? title[0].innerHTML : null; + var chart_title = []; + if (title) chart_title.push(title); + chart_title.push('Ops/sec (' + platform + ')'); + + // Build labels + var labels = [jsl.toLabel(min), jsl.toLabel(max)]; + + var w = 250, bw = 15; + var bs = 5; + var h = markers.length*(bw + bs) + 30 + chart_title.length*20; + + var params = { + chtt: escape(chart_title.join('|')), + chts: '000000,10', + cht: 'bhg', // chart type + chd: 't:' + data.join(','), // data set + chds: min + ',' + max, // max/min of data + chxt: 'x', // label axes + chxl: '0:|' + labels.join('|'), // labels + chsp: '0,1', + chm: markers.join('|'), // test names + chbh: [bw, 0, bs].join(','), // bar widths + // chf: 'bg,lg,0,eeeeee,0,eeeeee,.5,ffffff,1', // gradient + chs: w + 'x' + h + }; + return 'http://chart.apis.google.com/chart?' + jsl.join(params, '=', '&'); + } + }; + + JSLitmus._init(); +})(); \ No newline at end of file diff --git a/test/view.js b/test/view.js index e05b2bd66..bf7302c86 100644 --- a/test/view.js +++ b/test/view.js @@ -1,32 +1,33 @@ $(document).ready(function() { - module("Backbone View"); + module("Backbone.View"); var view = new Backbone.View({ id : 'test-view', className : 'test-view' }); - test("view: constructor", function() { + test("View: constructor", function() { equals(view.el.id, 'test-view'); equals(view.el.className, 'test-view'); equals(view.options.id, 'test-view'); equals(view.options.className, 'test-view'); }); - test("view: jQuery", function() { + test("View: jQuery", function() { view.el = document.body; - equals(view.$('#qunit-header').text(), 'Backbone Test Suite'); + equals(view.$('#qunit-header')[0].innerHTML, 'Backbone Test Suite'); + equals(view.$('#qunit-header')[1].innerHTML, 'Backbone Speed Suite'); }); - test("view: make", function() { + test("View: make", function() { var div = view.make('div', {id: 'test-div'}, "one two three"); equals(div.tagName.toLowerCase(), 'div'); equals(div.id, 'test-div'); equals($(div).text(), 'one two three'); }); - test("view: initialize", function() { + test("View: initialize", function() { var View = Backbone.View.extend({ initialize: function() { this.one = 1; @@ -36,7 +37,7 @@ $(document).ready(function() { equals(view.one, 1); }); - test("view: handleEvents", function() { + test("View: handleEvents", function() { var counter = 0; view.el = document.body; view.increment = function() { From 500d66bd58ae472dafe9759658592b9433937109 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 15:11:56 -0400 Subject: [PATCH 13/32] a handful of model speed tests. --- backbone.js | 2 +- test/speed.js | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index d0a1acc93..c219f5169 100644 --- a/backbone.js +++ b/backbone.js @@ -135,7 +135,7 @@ // Extract attributes and options. options || (options = {}); if (!attrs) return this; - attrs = attrs.attributes || attrs; + if (attrs.attributes) attrs = attrs.attributes; var now = this.attributes; // Run validation if `validate` is defined. diff --git a/test/speed.js b/test/speed.js index 0f29d47f6..6cd0b66c0 100644 --- a/test/speed.js +++ b/test/speed.js @@ -22,4 +22,24 @@ object.trigger('test:trigger2', 1, 2, 3, 4, 5); }); + var model = new Backbone.Model; + + JSLitmus.test('Model: set Math.random()', function() { + model.set({number: Math.random()}); + }); + + var eventModel = new Backbone.Model; + eventModel.bind('change', fn); + + JSLitmus.test('Model: set Math.random() with a change event', function() { + eventModel.set({number: Math.random()}); + }); + + var keyModel = new Backbone.Model; + keyModel.bind('change:number', fn); + + JSLitmus.test('Model: set Math.random() with a key-value observer', function() { + keyModel.set({number: Math.random()}); + }); + })(); \ No newline at end of file From 7f4a1bb1788ebd5672aa31659900b019bda7f999 Mon Sep 17 00:00:00 2001 From: Hans Oksendahl Date: Thu, 14 Oct 2010 17:28:33 -0700 Subject: [PATCH 14/32] Added the _method hack from Sinatra for better coverage of PUT and DELETE for non-spec HTTP servers. --- backbone.js | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/backbone.js b/backbone.js index c219f5169..33b2c44cf 100644 --- a/backbone.js +++ b/backbone.js @@ -18,7 +18,7 @@ } // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '0.1.1'; + Backbone.VERSION = '0.1.2'; // Require Underscore, if we're on the server, and it's not already present. var _ = this._; @@ -26,6 +26,9 @@ // For Backbone's purposes, jQuery owns the `$` variable. var $ = this.jQuery; + + // Are we actually sending PUT and DELETE requests + Backbone.USE_METHOD_HACK = true; // Backbone.Events // ----------------- @@ -602,14 +605,27 @@ // * Send up the models as XML instead of JSON. // * Persist models via WebSockets instead of Ajax. // + // The USE_METHOD_HACK setting denotes whether or not Backbone will send + // PUT and DELETE methods which are unsupported by some environments that don't + // follow specs. See Sinatra's documentations for details of the workaround. + // (http://sinatra-book.gittr.com/#the_put_and_delete_methods) Backbone.sync = function(method, model, success, error) { + var type = methodMap[method]; + var data = {model : JSON.stringify(model)}; + + if(Backbone.USE_METHOD_HACK) { + if(/GET|POST/.test(type)) var _method = type; + if(method != 'GET') method = 'POST'; + if(_method) data._method = _method; + } + $.ajax({ - url : getUrl(model), - type : methodMap[method], - data : {model : JSON.stringify(model)}, - dataType : 'json', - success : success, - error : error + url : getUrl(model), + type : type, + data : data, + dataType : 'json', + success : success, + error : error }); }; From 08030e431ec29b542329d87c3dbb2ae3376312ce Mon Sep 17 00:00:00 2001 From: Hans Oksendahl Date: Thu, 14 Oct 2010 17:30:43 -0700 Subject: [PATCH 15/32] fixed indentation --- backbone.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index 33b2c44cf..3f1569737 100644 --- a/backbone.js +++ b/backbone.js @@ -622,7 +622,7 @@ $.ajax({ url : getUrl(model), type : type, - data : data, + data : data, dataType : 'json', success : success, error : error From 196626931e08c98fdfca86766da632b06f38d85e Mon Sep 17 00:00:00 2001 From: Hans Oksendahl Date: Thu, 14 Oct 2010 17:32:23 -0700 Subject: [PATCH 16/32] fixed indentation --- backbone.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index 3f1569737..0fc1cf4fc 100644 --- a/backbone.js +++ b/backbone.js @@ -28,7 +28,7 @@ var $ = this.jQuery; // Are we actually sending PUT and DELETE requests - Backbone.USE_METHOD_HACK = true; + Backbone.USE_METHOD_HACK = false; // Backbone.Events // ----------------- From d8d40149e00bec0ac45080d23de49321fc252185 Mon Sep 17 00:00:00 2001 From: Samuel Clay Date: Fri, 15 Oct 2010 09:49:56 -0400 Subject: [PATCH 17/32] Type found by pbowyer. --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 1c52de2cb..653e8c697 100644 --- a/index.html +++ b/index.html @@ -273,7 +273,7 @@

      Introduction

      - When working on a web application that involved a lot of JavaScript, one + When working on a web application that involves a lot of JavaScript, one of the first things you learn is to stop tying your data to the DOM. It's all too easy to create JavaScript applications that end up as tangled piles of jQuery selectors and callbacks, all trying frantically to keep data in From 7b10698af656ffe137aaf78baba7e3e7156b0778 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Fri, 15 Oct 2010 09:57:31 -0400 Subject: [PATCH 18/32] making the speed test labels fit on the graph --- test/speed.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/speed.js b/test/speed.js index 6cd0b66c0..2c62c16a4 100644 --- a/test/speed.js +++ b/test/speed.js @@ -18,7 +18,7 @@ object.bind('test:trigger2', fn); object.bind('test:trigger2', fn); - JSLitmus.test('Events: trigger 2 functions, passing 5 arguments', function() { + JSLitmus.test('Events: trigger 2, passing 5 args', function() { object.trigger('test:trigger2', 1, 2, 3, 4, 5); }); @@ -31,14 +31,14 @@ var eventModel = new Backbone.Model; eventModel.bind('change', fn); - JSLitmus.test('Model: set Math.random() with a change event', function() { + JSLitmus.test('Model: set rand() with an event', function() { eventModel.set({number: Math.random()}); }); var keyModel = new Backbone.Model; keyModel.bind('change:number', fn); - JSLitmus.test('Model: set Math.random() with a key-value observer', function() { + JSLitmus.test('Model: set rand() with an attribute observer', function() { keyModel.set({number: Math.random()}); }); From 5c5b7a88241f0e7997c5d1d61796133c5a00a6f1 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Sat, 16 Oct 2010 16:05:49 -0400 Subject: [PATCH 19/32] Don't send up model data for GET requests by default, for folks who are re-fetching existing collections. --- backbone.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index c219f5169..d6fc4744e 100644 --- a/backbone.js +++ b/backbone.js @@ -603,10 +603,11 @@ // * Persist models via WebSockets instead of Ajax. // Backbone.sync = function(method, model, success, error) { + var data = method === 'read' ? {} : {model : JSON.stringify(model)}; $.ajax({ url : getUrl(model), type : methodMap[method], - data : {model : JSON.stringify(model)}, + data : data, dataType : 'json', success : success, error : error From 263245b9cfcf6cb38075d7292700956dc07c9115 Mon Sep 17 00:00:00 2001 From: Elijah Insua Date: Sun, 17 Oct 2010 03:22:25 -0400 Subject: [PATCH 20/32] added package.json --- package.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 000000000..b7b898af5 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name" : "backbone", + "description" : "Give your JS App some Backbone with Models, Views, Collections, and Events.", + "url" : "http://documentcloud.github.com/backbone/", + "keywords" : ["util", "functional", "server", "client", "browser"], + "author" : "Jeremy Ashkenas ", + "contributors" : [], + "dependencies" : [], + "lib" : ".", + "main" : "backbone.js", + "version" : "0.1.1" +} \ No newline at end of file From 689998822426ce3160631571085dbc97032cf515 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Sun, 17 Oct 2010 10:17:36 -0400 Subject: [PATCH 21/32] Only send the model on create and update... It's better for destroy not to bother with it. --- backbone.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index d6fc4744e..23501a80f 100644 --- a/backbone.js +++ b/backbone.js @@ -603,7 +603,8 @@ // * Persist models via WebSockets instead of Ajax. // Backbone.sync = function(method, model, success, error) { - var data = method === 'read' ? {} : {model : JSON.stringify(model)}; + var sendModel = method === 'create' || method === 'update'; + var data = sendModel ? {model : JSON.stringify(model)} : {}; $.ajax({ url : getUrl(model), type : methodMap[method], From 83f4748d8c8e84281582eaa07649de5c64fdcd74 Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Sun, 17 Oct 2010 18:24:01 -0700 Subject: [PATCH 22/32] Adding a missing var declaration inside handleEvents --- backbone.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index 33d297266..6017888ee 100644 --- a/backbone.js +++ b/backbone.js @@ -550,7 +550,7 @@ handleEvents : function(events) { $(this.el).unbind(); if (!(events || (events = this.events))) return this; - for (key in events) { + for (var key in events) { var methodName = events[key]; var match = key.match(eventSplitter); var eventName = match[1], selector = match[2]; From 2b539572a038ccc05399bb2b70d42e145e7104d3 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Mon, 18 Oct 2010 21:05:48 -0400 Subject: [PATCH 23/32] Passing JavaScriptLint. Added 'rake lint' task. --- Rakefile | 7 ++++++- backbone.js | 5 ++++- docs/jsl.conf | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 docs/jsl.conf diff --git a/Rakefile b/Rakefile index d5c2b96b8..2b2223791 100644 --- a/Rakefile +++ b/Rakefile @@ -9,5 +9,10 @@ end desc "build the docco documentation" task :doc do - system "docco backbone.js" + system "docco backbone.js" +end + +desc "run JavaScriptLint on the source" +task :lint do + system "jsl -nofilelisting -nologo -conf docs/jsl.conf -process backbone.js" end \ No newline at end of file diff --git a/backbone.js b/backbone.js index 6017888ee..c0217e3e8 100644 --- a/backbone.js +++ b/backbone.js @@ -256,7 +256,9 @@ // view need to be updated and/or what attributes need to be persisted to // the server. changedAttributes : function(now) { - var old = this._previousAttributes, now = now || this.attributes, changed = false; + now || (now = this.attributes); + var old = this._previousAttributes; + var changed = false; for (var attr in now) { if (!_.isEqual(old[attr], now[attr])) { changed = changed || {}; @@ -458,6 +460,7 @@ break; case 'error': this.trigger('error', model, error); + break; } } diff --git a/docs/jsl.conf b/docs/jsl.conf new file mode 100644 index 000000000..fb41a617a --- /dev/null +++ b/docs/jsl.conf @@ -0,0 +1,44 @@ +# JavaScriptLint configuration file for CoffeeScript. + ++no_return_value # function {0} does not always return a value ++duplicate_formal # duplicate formal argument {0} +-equal_as_assign # test for equality (==) mistyped as assignment (=)?{0} ++var_hides_arg # variable {0} hides argument ++redeclared_var # redeclaration of {0} {1} +-anon_no_return_value # anonymous function does not always return a value ++missing_semicolon # missing semicolon ++meaningless_block # meaningless block; curly braces have no impact +-comma_separated_stmts # multiple statements separated by commas (use semicolons?) ++unreachable_code # unreachable code ++missing_break # missing break statement ++missing_break_for_last_case # missing break statement for last case in switch +-comparison_type_conv # comparisons against null, 0, true, false, or an empty string allowing implicit type conversion (use === or !==) +-inc_dec_within_stmt # increment (++) and decrement (--) operators used as part of greater statement ++useless_void # use of the void type may be unnecessary (void is always undefined) ++multiple_plus_minus # unknown order of operations for successive plus (e.g. x+++y) or minus (e.g. x---y) signs ++use_of_label # use of label +-block_without_braces # block statement without curly braces ++leading_decimal_point # leading decimal point may indicate a number or an object member ++trailing_decimal_point # trailing decimal point may indicate a number or an object member ++octal_number # leading zeros make an octal number ++nested_comment # nested comment ++misplaced_regex # regular expressions should be preceded by a left parenthesis, assignment, colon, or comma ++ambiguous_newline # unexpected end of line; it is ambiguous whether these lines are part of the same statement ++empty_statement # empty statement or extra semicolon +-missing_option_explicit # the "option explicit" control comment is missing ++partial_option_explicit # the "option explicit" control comment, if used, must be in the first script tag ++dup_option_explicit # duplicate "option explicit" control comment ++useless_assign # useless assignment ++ambiguous_nested_stmt # block statements containing block statements should use curly braces to resolve ambiguity ++ambiguous_else_stmt # the else statement could be matched with one of multiple if statements (use curly braces to indicate intent) +-missing_default_case # missing default case in switch statement ++duplicate_case_in_switch # duplicate case in switch statements ++default_not_at_end # the default case is not at the end of the switch statement ++legacy_cc_not_understood # couldn't understand control comment using /*@keyword@*/ syntax ++jsl_cc_not_understood # couldn't understand control comment using /*jsl:keyword*/ syntax ++useless_comparison # useless comparison; comparing identical expressions ++with_statement # with statement hides undeclared variables; use temporary variable instead ++trailing_comma_in_array # extra comma is not recommended in array initializers ++assign_to_function_call # assignment to a function call ++parseint_missing_radix # parseInt missing radix parameter ++lambda_assign_requires_semicolon From 17d64e0a51a2b84d8a6accfb67cf00719526d4dc Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Mon, 18 Oct 2010 21:31:27 -0400 Subject: [PATCH 24/32] Adding Backbone.Model#fetch --- backbone.js | 14 ++++++++++++++ test/model.js | 6 ++++++ test/sync.js | 7 +++++++ 3 files changed, 27 insertions(+) diff --git a/backbone.js b/backbone.js index c0217e3e8..d77ba8477 100644 --- a/backbone.js +++ b/backbone.js @@ -186,6 +186,20 @@ return value; }, + // Fetch the model from the server. If the server's representation of the + // model differs from its current attributes, they will be overriden, + // triggering a `"change"` event. + fetch : function(options) { + options || (options = {}); + var model = this; + var success = function(resp) { + if (!model.set(resp.model)) return false; + if (options.success) options.success(model, resp); + }; + Backbone.sync('read', this, success, options.error); + return this; + }, + // Set a hash of model attributes, and sync the model to the server. // If the server returns an attributes hash that differs, the model's // state will be `set` again. diff --git a/test/model.js b/test/model.js index f1c99a460..82a5345f9 100644 --- a/test/model.js +++ b/test/model.js @@ -107,6 +107,12 @@ $(document).ready(function() { ok(_.isEqual(lastRequest[1], doc)); }); + test("Model: fetch", function() { + doc.fetch(); + ok(lastRequest[0], 'read'); + ok(_.isEqual(lastRequest[1], doc)); + }); + test("Model: destroy", function() { doc.destroy(); equals(lastRequest[0], 'delete'); diff --git a/test/sync.js b/test/sync.js index ae1d9948e..36888b197 100644 --- a/test/sync.js +++ b/test/sync.js @@ -68,6 +68,13 @@ $(document).ready(function() { equals(data.length, 123); }); + test("sync: read model", function() { + library.first().fetch(); + equals(lastRequest.url, '/library/2-the-tempest'); + equals(lastRequest.type, 'GET'); + ok(_.isEmpty(lastRequest.data)); + }); + test("sync: destroy", function() { Backbone.emulateHttp = false; library.first().destroy(); From c5c795ed98868d3e62fe9e7b3ad68891a0088b54 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Mon, 18 Oct 2010 21:39:30 -0400 Subject: [PATCH 25/32] adding docs for Model#fetch --- index.html | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/index.html b/index.html index 653e8c697..cc75db21e 100644 --- a/index.html +++ b/index.html @@ -159,6 +159,7 @@

    1. cid
    2. attributes
    3. - toJSON
    4. +
    5. fetch
    6. save
    7. destroy
    8. validate
    9. @@ -565,6 +566,24 @@

      Backbone.Model

      artist.set({birthday: "December 16, 1866"}); alert(JSON.stringify(artist)); +
      + +

      + fetchmodel.fetch([options]) +
      + Refreshes the model's state from the server. Useful if the model has never + been populated with data, or if you'd like to ensure that you have the + latest server state. A "change" event will be triggered if the + server's state differs from the current attributes. Accepts + success and error callbacks in the options hash, which + are passed (model, response) as arguments. +

      + +
      +// Poll every 10 seconds to keep the channel model up-to-date.
      +setInterval(function() {
      +  channel.fetch();
      +}, 10000);
       

      From 402f86634107682dab07eba8b1945b7faf8e8653 Mon Sep 17 00:00:00 2001 From: Elijah Insua Date: Mon, 18 Oct 2010 21:46:03 -0400 Subject: [PATCH 26/32] added underscore dependency --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b7b898af5..ce8a5e5c7 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,10 @@ "keywords" : ["util", "functional", "server", "client", "browser"], "author" : "Jeremy Ashkenas ", "contributors" : [], - "dependencies" : [], + "dependencies" : { + "underscore" : ">=1.1.2" + }, "lib" : ".", "main" : "backbone.js", "version" : "0.1.1" -} \ No newline at end of file +} From b854b28d18d321733797fa4b382298c7cd053043 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 19 Oct 2010 09:41:50 -0400 Subject: [PATCH 27/32] additional documentation ... getting ready for 0.1.2 --- backbone.js | 18 ++++++------- index.html | 76 ++++++++++++++++++++++++++++++++++------------------ package.json | 2 +- 3 files changed, 60 insertions(+), 36 deletions(-) diff --git a/backbone.js b/backbone.js index d77ba8477..272a7e782 100644 --- a/backbone.js +++ b/backbone.js @@ -18,7 +18,7 @@ } // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '0.1.1'; + Backbone.VERSION = '0.1.2'; // Require Underscore, if we're on the server, and it's not already present. var _ = this._; @@ -116,10 +116,10 @@ _.extend(Backbone.Model.prototype, Backbone.Events, { // A snapshot of the model's previous attributes, taken immediately - // after the last `changed` event was fired. + // after the last `"change"` event was fired. _previousAttributes : null, - // Has the item been changed since the last `changed` event? + // Has the item been changed since the last `"change"` event? _changed : false, // Return a copy of the model's `attributes` object. @@ -132,7 +132,7 @@ return this.attributes[attr]; }, - // Set a hash of model attributes on the object, firing `changed` unless you + // Set a hash of model attributes on the object, firing `"change"` unless you // choose to silence it. set : function(attrs, options) { @@ -167,12 +167,12 @@ } } - // Fire the `change` event, if the model has been 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 `"change"` unless you choose to // silence it. unset : function(attr, options) { options || (options = {}); @@ -258,7 +258,7 @@ this._changed = false; }, - // Determine if the model has changed since the last `changed` event. + // Determine if the model has changed since the last `"change"` event. // If you specify an attribute name, determine if that attribute has changed. hasChanged : function(attr) { if (attr) return this._previousAttributes[attr] != this.attributes[attr]; @@ -283,14 +283,14 @@ }, // Get the previous value of an attribute, recorded at the time the last - // `changed` event was fired. + // `"change"` event was fired. previous : function(attr) { if (!attr || !this._previousAttributes) return null; return this._previousAttributes[attr]; }, // Get all of the attributes of the model at the time of the previous - // `changed` event. + // `"change"` event. previousAttributes : function() { return _.clone(this._previousAttributes); } diff --git a/index.html b/index.html index cc75db21e..8f7a35c33 100644 --- a/index.html +++ b/index.html @@ -252,11 +252,11 @@

      - + - +
      Development Version (0.1.1)Development Version (0.1.2) 21kb, Uncompressed with Comments
      Production Version (0.1.1)Production Version (0.1.2) 2kb, Packed and Gzipped
      @@ -439,7 +439,7 @@

      Backbone.Model

       var Note = Backbone.Model.extend({
      -  
      +
         initialize: function() { ... },
       
         author: function() { ... },
      @@ -460,7 +460,7 @@ 

      Backbone.Model

      parent object's implementation, you'll have to explicitly call it, along these lines:

      - +
       var Note = Backbone.Model.extend({
         set: function(attributes, options) {
      @@ -578,7 +578,7 @@ 

      Backbone.Model

      success and error callbacks in the options hash, which are passed (model, response) as arguments.

      - +
       // Poll every 10 seconds to keep the channel model up-to-date.
       setInterval(function() {
      @@ -774,17 +774,17 @@ 

      Backbone.Collection

      providing instance properties, as well as optional classProperties to be attached directly to the collection's constructor function.

      - +

      modelcollection.model
      - Override this property to specify the model class that the collection - contains. If defined, you can pass raw attributes objects (and arrays) to + 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
      @@ -974,7 +974,7 @@ 

      Backbone.Collection


      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. Calling sort + will maintain itself in proper sort order at all times. Calling sort triggers the collection's "refresh" event, unless silenced by passing {silent: true}

      @@ -1036,9 +1036,9 @@

      Backbone.Collection

      The server handler for fetch requests should return a JSON list of models, namespaced under "models": {"models": [...]} — - 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 + 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.

      @@ -1060,7 +1060,7 @@

      Backbone.Collection

      for interfaces that are not needed immediately: for example, documents with collections of notes that may be toggled open and closed.

      - +

      refreshcollection.refresh(models, [options])
      @@ -1070,12 +1070,12 @@

      Backbone.Collection

      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 %>);
      @@ -1138,8 +1138,32 @@ 

      Backbone.sync

      - For example, a Rails handler responding to an "update" call from - Backbone.sync would look like this: (In real code, never use + The default sync handler maps CRUD to REST like so: +

      + +
        +
      • create → POST   /collection
      • +
      • read → GET   /collection[/id]
      • +
      • update → PUT   /collection/id
      • +
      • delete → DELETE   /collection/id
      • +
      + +

      + If your web server makes it difficult to work with real PUT and + DELETE requests, you may choose to emulate them instead, using + HTTP POST, and passing them under the _method parameter + instead, by turning on Backbone.emulateHttp: +

      + +
      +Backbone.emulateHttp = true;
      +
      +model.save();  // Sends a POST to "/collection/id", with "_method=PUT"
      +
      + +

      + As an example, a Rails handler responding to an "update" call from + Backbone.sync might look like this: (In real code, never use update_attributes blindly, and always whitelist the attributes you allow to be changed.)

      @@ -1189,7 +1213,7 @@

      Backbone.View

      "click .button.edit": "openEditDialog", "click .button.delete": "destroy" }, - + initialize: function() { _.bindAll(this, "render"); }, @@ -1209,7 +1233,7 @@

      Backbone.View

      options that, if passed, will be attached directly to the view: model, collection, el, id, className, and tagName. - If the view defines an initialize function, it will be called when + 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: new View({el: existingElement}) @@ -1284,18 +1308,18 @@

      Backbone.View

      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, + choosing a nice JavaScript templating library. + Mustache.js, Haml-js, and - Eco are all fine alternatives. + 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 + 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 @@ -1378,7 +1402,7 @@

      Backbone.View

      Change Log

      - +

      0.1.1Oct 14, 2010
      Added a convention for initialize functions to be called diff --git a/package.json b/package.json index ce8a5e5c7..c2485b865 100644 --- a/package.json +++ b/package.json @@ -10,5 +10,5 @@ }, "lib" : ".", "main" : "backbone.js", - "version" : "0.1.1" + "version" : "0.1.2" } From a09bcbca9d660b09dd010e6fc92c5c6c32f10bbb Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 19 Oct 2010 10:13:50 -0400 Subject: [PATCH 28/32] error events are now always passed the model as the first argument. You may now also pass an error callback to set() and save(), if the callback is passed, it will be called instead of the 'error' event getting fired. --- backbone.js | 26 +++++++++++++++++--------- test/model.js | 24 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/backbone.js b/backbone.js index 272a7e782..501224bce 100644 --- a/backbone.js +++ b/backbone.js @@ -142,11 +142,16 @@ if (attrs.attributes) attrs = attrs.attributes; var now = this.attributes; - // Run validation if `validate` is defined. + // Run validation if `validate` is defined. If a specific `error` callback + // has been passed, call that instead of firing the general `"error"` event. if (this.validate) { var error = this.validate(attrs); if (error) { - this.trigger('error', this, error); + if (options.error) { + options.error(this, error); + } else { + this.trigger('error', this, error); + } return false; } } @@ -193,10 +198,11 @@ options || (options = {}); var model = this; var success = function(resp) { - if (!model.set(resp.model)) return false; + if (!model.set(resp.model, options)) return false; if (options.success) options.success(model, resp); }; - Backbone.sync('read', this, success, options.error); + var error = options.error && _.bind(options.error, null, model); + Backbone.sync('read', this, success, error); return this; }, @@ -209,11 +215,12 @@ if (!this.set(attrs, options)) return false; var model = this; var success = function(resp) { - if (!model.set(resp.model)) return false; + if (!model.set(resp.model, options)) return false; if (options.success) options.success(model, resp); }; + var error = options.error && _.bind(options.error, null, model); var method = this.isNew() ? 'create' : 'update'; - Backbone.sync(method, this, success, options.error); + Backbone.sync(method, this, success, error); return this; }, @@ -226,7 +233,8 @@ if (model.collection) model.collection.remove(model); if (options.success) options.success(model, resp); }; - Backbone.sync('delete', this, success, options.error); + var error = options.error && _.bind(options.error, null, model); + Backbone.sync('delete', this, success, error); return this; }, @@ -399,7 +407,8 @@ collection.refresh(resp.models); if (options.success) options.success(collection, resp); }; - Backbone.sync('read', this, success, options.error); + var error = options.error && _.bind(options.error, null, collection); + Backbone.sync('read', this, success, error); return this; }, @@ -410,7 +419,6 @@ if (!(model instanceof Backbone.Model)) model = new this.model(model); model.collection = this; var success = function(resp) { - if (!model.set(resp.model)) return false; model.collection.add(model); if (options.success) options.success(model, resp); }; diff --git a/test/model.js b/test/model.js index 82a5345f9..1573bb074 100644 --- a/test/model.js +++ b/test/model.js @@ -138,4 +138,28 @@ $(document).ready(function() { equals(lastError, "Can't change admin status."); }); + test("Model: validate with error callback", function() { + var lastError, boundError; + var model = new Backbone.Model(); + model.validate = function(attrs) { + if (attrs.admin) return "Can't change admin status."; + }; + var callback = function(model, error) { + lastError = error; + }; + model.bind('error', function(model, error) { + boundError = true; + }); + var result = model.set({a: 100}, {error: callback}); + equals(result, model); + equals(model.get('a'), 100); + equals(lastError, undefined); + equals(boundError, undefined); + result = model.set({a: 200, admin: true}, {error: callback}); + equals(result, false); + equals(model.get('a'), 100); + equals(lastError, "Can't change admin status."); + equals(boundError, undefined); + }); + }); From f0f7c8d5e3daba3779d1d19566727e16462874f4 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 19 Oct 2010 10:47:40 -0400 Subject: [PATCH 29/32] Adding an error when URLs are left unspecified, and highlighting the importance of the URL property in the docs for persistence to work. --- backbone.js | 1 + index.html | 13 ++++++++++++- test/model.js | 9 +++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index 501224bce..638cc9160 100644 --- a/backbone.js +++ b/backbone.js @@ -678,6 +678,7 @@ // Helper function to get a URL from a Model or Collection as a property // or as a function. var getUrl = function(object) { + if (!(object && object.url)) throw new Error("A 'url' property or function must be specified"); return _.isFunction(object.url) ? object.url() : object.url; }; diff --git a/index.html b/index.html index 8f7a35c33..8af0cd728 100644 --- a/index.html +++ b/index.html @@ -190,7 +190,7 @@

    10. comparator
    11. sort
    12. pluck
    13. -
    14. url
    15. +
    16. url
    17. fetch
    18. refresh
    19. create
    20. @@ -586,6 +586,15 @@

      Backbone.Model

      }, 10000);
      +

      + + Cautionary Note: When fetching or saving a model, make sure that the model is part of + a collection with a url property specified, + or that the model itself has a complete url function + of its own, so that the request knows where to go. + +

      +

      savemodel.save(attributes, [options])
      @@ -678,6 +687,8 @@

      Backbone.Model

      + Delegates to Collection#url to generate the + URL, so make sure that you have it defined. A model with an id of 101, stored in a Backbone.Collection with a url of "/notes", would have this URL: "/notes/101" diff --git a/test/model.js b/test/model.js index 1573bb074..dff019678 100644 --- a/test/model.js +++ b/test/model.js @@ -40,6 +40,15 @@ $(document).ready(function() { test("Model: url", function() { equals(doc.url(), '/collection/1-the-tempest'); + doc.collection = null; + var failed = false; + try { + doc.url(); + } catch (e) { + failed = true; + } + equals(failed, true); + doc.collection = collection; }); test("Model: clone", function() { From 2071b932b99c4747df34ab6545f10e2b90e21061 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 19 Oct 2010 10:51:27 -0400 Subject: [PATCH 30/32] tweak to view docs --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 8af0cd728..b2e49295f 100644 --- a/index.html +++ b/index.html @@ -1197,8 +1197,8 @@

      Backbone.View

      backed by models, each of which can be updated independently when the model changes, without having to redraw the page. Instead of digging into a JSON object, looking up an element in the DOM, and updating the HTML by hand, - it should look more like: - model.bind('change', renderView) — and now everywhere that + you can bind your view's render function to the model's "change" + event — and now everywhere that model data is displayed in the UI, it is always immediately up to date.

      From 3e3c292c48f2159e545750a392c3da1c35743797 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 19 Oct 2010 11:26:16 -0400 Subject: [PATCH 31/32] documenting the 'error' callback and it's overriding behavior. --- index.html | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/index.html b/index.html index b2e49295f..85cdef0d3 100644 --- a/index.html +++ b/index.html @@ -500,17 +500,20 @@

      Backbone.Model

      change the models state, a "change" event will be fired, unless {silent: true} is passed as an option.

      + +
      +note.set({title: "October 12", content: "Lorem Ipsum Dolor Sit Amet..."});
      +

      If the model has a validate method, - it will be validated before the attributes are set, and no changes will - occur if the validation fails. + it will be validated before the attributes are set, no changes will + occur if the validation fails, and set will return false. + You may also pass an error + callback in the options, which will be invoked instead of triggering an + "error" event, should validation fail.

      -
      -note.set({title: "October 12", content: "Lorem Ipsum Dolor Sit Amet..."});
      -
      -

      unsetmodel.unset(attribute, [options])
      @@ -605,7 +608,8 @@

      Backbone.Model

      (HTTP POST), if the model already exists on the server, the save will be an "update" (HTTP PUT). Accepts success and error callbacks in the options hash, which - are passed (model, response) as arguments. + are passed (model, response) as arguments. The error callback will + also be invoked if the model has a validate method, and validation fails.

      @@ -676,6 +680,21 @@

      Backbone.Model

      start: 15, end: 10 }); +
      + +

      + "error" events are useful for providing coarse-grained error + messages at the model or collection level, but if you have a specific view + that can better handle the error, you may override and suppress the event + by passing an error callback directly: +

      + +
      +account.set({access: "unlimited"}, {
      +  error: function(model, error) {
      +    alert(error);
      +  }
      +});
       

      From d2ba3311843e6ac33160b31305c57b68ed162992 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 19 Oct 2010 11:44:07 -0400 Subject: [PATCH 32/32] Backbone.js 0.1.2 --- backbone-min.js | 26 +++--- docs/backbone.html | 215 +++++++++++++++++++++++++++------------------ index.html | 18 +++- 3 files changed, 160 insertions(+), 99 deletions(-) diff --git a/backbone-min.js b/backbone-min.js index d632e8593..1f1aec0cf 100644 --- a/backbone-min.js +++ b/backbone-min.js @@ -1,15 +1,15 @@ -(function(){var f={};f.VERSION="0.1.1";(typeof exports!=="undefined"?exports:this).Backbone=f;var e=this._;if(!e&&typeof require!=="undefined")e=require("underscore")._;var h=this.$,j=function(a,b,c){var d=b.hasOwnProperty("constructor")?b.constructor:function(){return a.apply(this,arguments)},g=function(){};g.prototype=a.prototype;d.prototype=new g;e.extend(d.prototype,b);c&&e.extend(d,c);return d.prototype.constructor=d};f.Events={bind:function(a,b){this._callbacks||(this._callbacks={});(this._callbacks[a]|| -(this._callbacks[a]=[])).push(b);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d=0,g=c.length;d

      (function(){

      Initial Setup

      The top-level namespace.

        var Backbone = {};

      Keep the version here 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.$;

      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 = protoProps.hasOwnProperty('constructor') ? protoProps.constructor :
      -                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

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

      (function(){

      Initial Setup

      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.2';

      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.jQuery;

      Turn on emulateHttp to fake "PUT" and "DELETE" requests via +the _method parameter.

        Backbone.emulateHttp = false;

      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.

      @@ -26,13 +18,13 @@ _.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. +

        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] = []);
             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;
      @@ -53,15 +45,14 @@
               }
             }
             return this;
      -    },

      Trigger an event, firing all bound callbacks. Callbacks are passed the + },

      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;
             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']) {
      @@ -72,29 +63,34 @@
             return this;
           }
       
      -  };

      Backbone.Model

      Create a new model, with defined attributes. A client id (cid) + };

      Backbone.Model

      Create a new model, with defined attributes. A client id (cid) is automatically generated and assigned for you.

        Backbone.Model = function(attributes) {
           this.attributes = {};
           this.cid = _.uniqueId('c');
           this.set(attributes || {}, {silent : true});
           this._previousAttributes = _.clone(this.attributes);
           if (this.initialize) this.initialize(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.

          _previousAttributes : null,

      Has the item been changed since the last changed event?

          _changed : false,

      Return a copy of the model's attributes object.

          toJSON : function() {
      +  };

      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 "change" event was fired.

          _previousAttributes : null,

      Has the item been changed since the last "change" event?

          _changed : false,

      Return a copy of the model's attributes object.

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

      Get the value of an attribute.

          get : function(attr) {
      +    },

      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) {

      Extract attributes and options.

            options || (options = {});
      +    },

      Set a hash of model attributes on the object, firing "change" 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;

      Run validation if validate is defined.

            if (this.validate) {
      +      if (attrs.attributes) attrs = attrs.attributes;
      +      var now = this.attributes;

      Run validation if validate is defined. If a specific error callback +has been passed, call that instead of firing the general "error" event.

            if (this.validate) {
               var error = this.validate(attrs);
               if (error) {
      -          this.trigger('error', this, error);
      +          if (options.error) {
      +            options.error(this, error);
      +          } else {
      +            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) {
      +      }

      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)) {
      @@ -104,9 +100,9 @@
                   this.trigger('change:' + attr, this, val);
                 }
               }
      -      }

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

            if (!options.silent && this._changed) this.change();
      +      }

      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 "change" unless you choose to silence it.

          unset : function(attr, options) {
             options || (options = {});
             var value = this.attributes[attr];
      @@ -117,7 +113,19 @@
               this.change();
             }
             return value;
      -    },

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

      Fetch the model from the server. If the server's representation of the +model differs from its current attributes, they will be overriden, +triggering a "change" event.

          fetch : function(options) {
      +      options || (options = {});
      +      var model = this;
      +      var success = function(resp) {
      +        if (!model.set(resp.model, options)) return false;
      +        if (options.success) options.success(model, resp);
      +      };
      +      var error = options.error && _.bind(options.error, null, model);
      +      Backbone.sync('read', this, success, error);
      +      return this;
      +    },

      Set a hash of model attributes, and sync the model to the server. If the server returns an attributes hash that differs, the model's state will be set again.

          save : function(attrs, options) {
             attrs   || (attrs = {});
      @@ -125,13 +133,14 @@
             if (!this.set(attrs, options)) return false;
             var model = this;
             var success = function(resp) {
      -        if (!model.set(resp.model)) return false;
      +        if (!model.set(resp.model, options)) return false;
               if (options.success) options.success(model, resp);
             };
      +      var error = options.error && _.bind(options.error, null, model);
             var method = this.isNew() ? 'create' : 'update';
      -      Backbone.sync(method, this, success, options.error);
      +      Backbone.sync(method, this, success, error);
             return this;
      -    },

      Destroy this model on the server. Upon success, the model is removed + },

      Destroy this model on the server. Upon success, the model is removed from its collection, if it has one.

          destroy : function(options) {
             options || (options = {});
             var model = this;
      @@ -139,33 +148,36 @@
               if (model.collection) model.collection.remove(model);
               if (options.success) options.success(model, resp);
             };
      -      Backbone.sync('delete', this, success, options.error);
      +      var error = options.error && _.bind(options.error, null, model);
      +      Backbone.sync('delete', this, success, error);
             return this;
      -    },

      Default URL for the model's representation on the server -- if you're + },

      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 = getUrl(this.collection);
             if (this.isNew()) return base;
             return base + '/' + this.id;
      -    },

      Create a new model with identical attributes to this one.

          clone : function() {
      +    },

      Create a new model with identical attributes to this one.

          clone : function() {
             return new this.constructor(this);
      -    },

      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 change 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.

          change : function() {
             this.trigger('change', this);
             this._previousAttributes = _.clone(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 "change" event. If you specify an attribute name, determine if that attribute has changed.

          hasChanged : function(attr) {
             if (attr) return this._previousAttributes[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) {
      -      var old = this._previousAttributes, now = now || this.attributes, changed = false;
      +      now || (now = this.attributes);
      +      var old = this._previousAttributes;
      +      var changed = false;
             for (var attr in now) {
               if (!_.isEqual(old[attr], now[attr])) {
                 changed = changed || {};
      @@ -173,16 +185,16 @@
               }
             }
             return changed;
      -    },

      Get the previous value of an attribute, recorded at the time the last -changed event was fired.

          previous : function(attr) {
      +    },

      Get the previous value of an attribute, recorded at the time the last +"change" event was fired.

          previous : function(attr) {
             if (!attr || !this._previousAttributes) return null;
             return this._previousAttributes[attr];
      -    },

      Get all of the attributes of the model at the time of the previous -changed event.

          previousAttributes : function() {
      +    },

      Get all of the attributes of the model at the time of the previous +"change" event.

          previousAttributes : function() {
             return _.clone(this._previousAttributes);
           }
       
      -  });

      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 = {});
      @@ -194,8 +206,8 @@
           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, {

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

      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)) {
               for (var i = 0, l = models.length; i < l; i++) {
      @@ -205,7 +217,7 @@
               this._add(models, options);
             }
             return this;
      -    },

      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)) {
               for (var i = 0, l = models.length; i < l; i++) {
      @@ -215,22 +227,22 @@
               this._remove(models, options);
             }
             return this;
      -    },

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

      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 = []);
      @@ -239,7 +251,7 @@
             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;
      @@ -247,25 +259,25 @@
               collection.refresh(resp.models);
               if (options.success) options.success(collection, resp);
             };
      -      Backbone.sync('read', this, success, options.error);
      +      var error = options.error && _.bind(options.error, null, collection);
      +      Backbone.sync('read', this, success, 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);
             model.collection = this;
             var success = function(resp) {
      -        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});
      -    },

      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 = {});
             if (!(model instanceof Backbone.Model)) {
      @@ -281,7 +293,8 @@
             model.bind('all', this._boundOnModelEvent);
             this.length++;
             if (!options.silent) this.trigger('add', model);
      -    },

      Internal implementation of removing a single model from the set, updating + return model; + },

      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.getByCid(model);
      @@ -293,7 +306,8 @@
             model.unbind('all', this._boundOnModelEvent);
             this.length--;
             if (!options.silent) this.trigger('remove', model);
      -    },

      Internal method called every time a model in the set fires an event. + return 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':
      @@ -305,17 +319,18 @@
                 break;
               case 'error':
                 this.trigger('error', model, error);
      +          break;
             }
           }
       
      -  });

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

      Backbone.View

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

      Backbone.View

      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) {
      @@ -327,16 +342,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'));
      @@ -345,7 +360,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"}

      @@ -362,7 +377,7 @@ bubble change events at all.

          handleEvents : function(events) {
             $(this.el).unbind();
             if (!(events || (events = this.events))) return this;
      -      for (key in events) {
      +      for (var key in events) {
               var methodName = events[key];
               var match = key.match(eventSplitter);
               var eventName = match[1], selector = match[2];
      @@ -374,7 +389,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);
      @@ -386,16 +401,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 + };

      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 to the model's url(). Some possible customizations could be:

      @@ -404,15 +419,47 @@
    21. Use setTimeout to batch rapid-fire updates into a single request.
    22. Send up the models as XML instead of JSON.
    23. Persist models via WebSockets instead of Ajax.
    24. -
        Backbone.sync = function(method, model, success, error) {
      +
      +
      +

      Turn on Backbone.emulateHttp in order to send PUT and DELETE requests +as POST, with an _method parameter containing the true HTTP method. +Useful when interfacing with server-side languages like PHP that make +it difficult to read the body of PUT requests.

        Backbone.sync = function(method, model, success, error) {
      +    var sendModel = method === 'create' || method === 'update';
      +    var data = sendModel ? {model : JSON.stringify(model)} : {};
      +    var type = methodMap[method];
      +    if (Backbone.emulateHttp && (type === 'PUT' || type === 'DELETE')) {
      +      data._method = type;
      +      type = 'POST';
      +    }
           $.ajax({
             url       : getUrl(model),
      -      type      : methodMap[method],
      -      data      : {model : JSON.stringify(model)},
      +      type      : type,
      +      data      : data,
             dataType  : 'json',
             success   : success,
             error     : error
           });
      +  };

      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) {
      +    if (!(object && object.url)) throw new Error("A 'url' property or function must be specified");
      +    return _.isFunction(object.url) ? object.url() : object.url;
         };
       
       })();
      diff --git a/index.html b/index.html
      index 85cdef0d3..141bbbdd1 100644
      --- a/index.html
      +++ b/index.html
      @@ -253,11 +253,11 @@ 

      - + - +
      Development Version (0.1.2)21kb, Uncompressed with Comments23.8kb, Uncompressed with Comments
      Production Version (0.1.2)2kb, Packed and Gzipped2.6kb, Packed and Gzipped
      @@ -1432,6 +1432,20 @@

      Backbone.View

      Change Log

      + +

      + 0.1.2Oct 19, 2010
      + Added a Model#fetch method for refreshing the + attributes of single model from the server. + An error callback may now be passed to set and save + as an option, which will be invoked if validation fails, overriding the + "error" event. + You can now tell backbone to use the _method hack instead of HTTP + methods by setting Backbone.emulateHttp = true. + Existing Model and Collection data is no longer sent up unnecessarily with + GET and DELETE requests. Added a rake lint task. + Backbone is now published as an NPM module. +

      0.1.1Oct 14, 2010