diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..48ecc30c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +raw \ No newline at end of file diff --git a/backbone.js b/backbone.js index 2ac3f1e0..5dcfcbe1 100644 --- a/backbone.js +++ b/backbone.js @@ -1,8 +1,7 @@ -// Backbone.js -// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. -// Backbone may be freely distributed under the terms of the MIT license. -// For all details and documentation: -// http://documentcloud.github.com/backbone +// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://documentcloud.github.com/backbone (function(){ @@ -19,12 +18,11 @@ (typeof exports !== 'undefined' ? exports : this).Backbone = Backbone; // Helper function to correctly set up the prototype chain, for subclasses. + // Similar to `goog.inherits`, but uses a hash of prototype properties and + // static properties to be extended. var inherits = function(parent, protoProps, classProps) { - if (protoProps.hasOwnProperty('constructor')) { - child = protoProps.constructor; - } else { - child = function(){ return parent.apply(this, arguments); }; - } + var child = protoProps.hasOwnProperty('constructor') ? protoProps.constructor : + function(){ return parent.apply(this, arguments); }; var ctor = function(){}; ctor.prototype = parent.prototype; child.prototype = new ctor(); @@ -34,12 +32,19 @@ return child; }; - // Backbone.Bindable + // Backbone.Events // ----------------- - // A module that can be mixed in to any object in order to provide it with - // custom events. - Backbone.Bindable = { + // A module that can be mixed in to *any object* in order to provide it with + // custom events. You may `bind` or `unbind` a callback function to an event; + // `trigger`-ing an event fires all callbacks in succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.bind('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + Backbone.Events = { // Bind an event, specified by a string name, `ev`, to a `callback` function. // Passing `"all"` will bind the callback to all events fired. @@ -62,6 +67,7 @@ calls[ev] = []; } else { var list = calls[ev]; + if (!list) return this; for (var i = 0, l = list.length; i < l; i++) { if (callback === list[i]) { list.splice(i, 1); @@ -73,14 +79,21 @@ return this; }, - // Trigger an event, firing all bound callbacks + // Trigger an event, firing all bound callbacks. Callbacks are passed the + // same arguments as `trigger` is, apart from the event name. + // Listening for `"all"` passes the true event name as the first argument. trigger : function(ev) { + var list, calls, i, l; var calls = this._callbacks; - for (var i = 0; i < 2; i++) { - var list = calls && calls[i ? 'all' : ev]; - if (!list) continue; - for (var j = 0, l = list.length; j < l; j++) { - list[j].apply(this, arguments); + if (!(calls = this._callbacks)) return this; + if (list = calls[ev]) { + for (i = 0, l = list.length; i < l; i++) { + list[i].apply(this, _.rest(arguments)); + } + } + if (list = calls['all']) { + for (i = 0, l = list.length; i < l; i++) { + list[i].apply(this, arguments); } } return this; @@ -95,36 +108,13 @@ // If you do not specify the id, a negative id will be assigned for you. Backbone.Model = function(attributes) { this._attributes = {}; - attributes = attributes || {}; - this.set(attributes, true); this.cid = _.uniqueId('c'); + this.set(attributes || {}, {silent : true}); this._formerAttributes = this.attributes(); }; - // Create a model on the server and add it to the set. - // When the server returns a JSON representation of the model, we update it - // on the client. - Backbone.Model.create = function(attributes, options) { - options || (options = {}); - var model = new this(attributes); - $.ajax({ - url : model.set.resource, - type : 'POST', - data : {model : JSON.stringify(model.attributes())}, - dataType : 'json', - success : function(resp) { - model.set(resp.model); - if (options.success) return options.success(model, resp); - }, - error : function(resp) { - if (options.error) options.error(model, resp); - } - }); - return model; - }; - // Attach all inheritable methods to the Model prototype. - _.extend(Backbone.Model.prototype, Backbone.Bindable, { + _.extend(Backbone.Model.prototype, Backbone.Events, { // A snapshot of the model's previous attributes, taken immediately // after the last `changed` event was fired. @@ -133,6 +123,26 @@ // Has the item been changed since the last `changed` event? _changed : false, + // Return a copy of the model's `attributes` object. + attributes : function() { + return _.clone(this._attributes); + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url : function() { + var base = this.collection.url(); + if (this.isNew()) return base; + return base + '/' + this.id; + }, + + // String representation of the model. Override this to provide a nice way + // to print models to the console. + toString : function() { + return 'Model ' + this.id; + }, + // Create a new model with identical attributes to this one. clone : function() { return new (this.constructor)(this.attributes()); @@ -149,9 +159,68 @@ return !this.id; }, - // Call this method to fire manually fire a `changed` event for this model. + // Get the value of an attribute. + get : function(attr) { + return this._attributes[attr]; + }, + + // Set a hash of model attributes on the object, firing `changed` unless you + // choose to silence it. + set : function(attrs, options) { + + // 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) { + var error = this.validate(attrs); + if (error) { + this.trigger('error', this, error); + return false; + } + } + + // Check for changes of `id`. + if ('id' in attrs) this.id = attrs.id; + + // Update attributes. + for (var attr in attrs) { + var val = attrs[attr]; + if (val === '') val = null; + if (!_.isEqual(now[attr], val)) { + now[attr] = val; + if (!options.silent) { + this._changed = true; + this.trigger('change:' + attr, this); + } + } + } + + // 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 + // silence it. + unset : function(attr, options) { + options || (options = {}); + var value = this._attributes[attr]; + delete this._attributes[attr]; + if (!options.silent) { + this._changed = true; + this.trigger('change:' + attr, this); + this.change(); + } + return value; + }, + + // Call this method to fire manually fire a `change` event for this model. // Calling this will cause all objects observing the model to update. - changed : function() { + change : function() { this.trigger('change', this); this._formerAttributes = this.attributes(); this._changed = false; @@ -164,19 +233,6 @@ return this._changed; }, - // Get the previous value of an attribute, recorded at the time the last - // `changed` event was fired. - formerValue : function(attr) { - if (!attr || !this._formerAttributes) return null; - return this._formerAttributes[attr]; - }, - - // Get all of the attributes of the model at the time of the previous - // `changed` event. - formerAttributes : function() { - return this._formerAttributes; - }, - // Return an object containing all the attributes that have changed, or false // if there are no changed attributes. Useful for determining what parts of a // view need to be updated and/or what attributes need to be persisted to @@ -192,80 +248,43 @@ return changed; }, - // Set a hash of model attributes on the object, firing `changed` unless you - // choose to silence it. - set : function(attrs, options) { - options || (options = {}); - if (!attrs) return this; - attrs = attrs._attributes || attrs; - var now = this._attributes; - if (attrs.id) { - this.id = attrs.id; - if (this.collection) this.resource = this.collection.resource + '/' + this.id; - } - for (var attr in attrs) { - var val = attrs[attr]; - if (val === '') val = null; - if (!_.isEqual(now[attr], val)) { - if (!options.silent) this._changed = true; - now[attr] = val; - } - } - if (!options.silent && this._changed) this.changed(); - return this; + // Get the previous value of an attribute, recorded at the time the last + // `changed` event was fired. + formerValue : function(attr) { + if (!attr || !this._formerAttributes) return null; + return this._formerAttributes[attr]; }, - // Get the value of an attribute. - get : function(attr) { - return this._attributes[attr]; - }, - - // Remove an attribute from the model, firing `changed` unless you choose to - // silence it. - unset : function(attr, options) { - options || (options = {}); - var value = this._attributes[attr]; - delete this._attributes[attr]; - if (!options.silent) this.changed(); - return value; - }, - - // Return a copy of the model's attributes. - attributes : function() { - return _.clone(this._attributes); - }, - - // Bind all methods in the list to the model. - bindAll : function() { - _.bindAll.apply(_, [this].concat(arguments)); - }, - - toString : function() { - return 'Model ' + this.id; - }, - - // Return the URL used to {save,delete} - url : function() { - if (!this.id) throw new Error(this.toString() + " has no id."); - return this.collection.url() + '/' + this.id; + // Get all of the attributes of the model at the time of the previous + // `changed` event. + formerAttributes : function() { + return this._formerAttributes; }, // Set a hash of model attributes, and sync the model to the server. save : function(attrs, options) { + attrs || (attrs = {}); options || (options = {}); - this.set(attrs, options); + if (!this.set(attrs, options)) return false; var model = this; var success = function(resp) { - model.set(resp.model); + if (!model.set(resp.model)) return false; if (options.success) options.success(model, resp); }; - Backbone.request('PUT', this, success, options.error); + var method = this.isNew() ? 'POST' : 'PUT'; + Backbone.request(method, this, success, options.error); return this; }, // Destroy this model on the server. destroy : function(options) { - Backbone.request('DELETE', this, options.success, options.error); + options || (options = {}); + var model = this; + var success = function(resp) { + if (model.collection) model.collection.remove(model); + if (options.success) options.success(model, resp); + }; + Backbone.request('DELETE', this, success, options.error); return this; } @@ -277,26 +296,30 @@ // Provides a standard collection class for our sets of models, ordered // or unordered. If a `comparator` is specified, the Collection will maintain // its models in sort order, as they're added and removed. - Backbone.Collection = function(options) { + Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.comparator) { + this.comparator = options.comparator; + delete options.comparator; + } this._boundOnModelEvent = _.bind(this._onModelEvent, this); this._initialize(); + if (models) this.refresh(models,true); }; // Define the Collection's inheritable methods. - _.extend(Backbone.Collection.prototype, Backbone.Bindable, { + _.extend(Backbone.Collection.prototype, Backbone.Events, { - // Initialize or re-initialize all internal state. Called when the - // collection is refreshed. - _initialize : function() { - this.length = 0; - this.models = []; - this._byId = {}; - this._byCid = {}; + model : Backbone.Model, + + // Override this function to get convenient logging in the console. + toString : function() { + return 'Collection (' + this.length + " models)"; }, // Get a model from the set by id. get : function(id) { - return id && this._byId[id.id || id]; + return id && this._byId[id.id != null ? id.id : id]; }, // Get a model from the set by client id. @@ -304,6 +327,11 @@ return cid && this._byCid[cid.cid || cid]; }, + // Get the model at the given index. + at: function(index) { + return this.models[index]; + }, + // What are the ids for every model in the set? getIds : function() { return _.keys(this._byId); @@ -314,100 +342,142 @@ return _.keys(this._byCid); }, - // Get the model at the given index. - at: function(index) { - return this.models[index]; + // Pluck an attribute from each model in the collection. + pluck : function(attr) { + return _.map(this.models, function(model){ return model.get(attr); }); }, - // Add a model, or list of models to the set. Pass silent to avoid firing - // the `added` event for every new model. - add : function(models, silent) { - if (!_.isArray(models)) return this._add(models, silent); - for (var i=0; i backbone.js

backbone.js

#

Backbone.js -(c) 2010 Jeremy Ashkenas, DocumentCloud Inc. -Backbone may be freely distributed under the terms of the MIT license. + backbone.js

backbone.js

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

(function(){
#

Initial Setup

#

The top-level namespace.

  var Backbone = {};
#

Keep the version in sync with package.json.

  Backbone.VERSION = '0.1.0';
#

Export for both CommonJS and the Browser.

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

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

  var inherits = function(parent, protoProps, classProps) {
-    if (protoProps.hasOwnProperty('constructor')) {
-      child = protoProps.constructor;
-    } else {
-      child = function(){ return parent.apply(this, arguments); };
-    }
+http://documentcloud.github.com/backbone
+
(function(){

Initial Setup

The top-level namespace.

  var Backbone = {};

Keep the version in sync with package.json.

  Backbone.VERSION = '0.1.0';

Export for both CommonJS and the Browser.

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

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

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

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

Backbone.Bindable

#

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

  Backbone.Bindable = {
#

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

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

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

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

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

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

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

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

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

} } return this; - },
#

Trigger an event, firing all bound callbacks

    trigger : function(ev) {
+    },

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

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

Backbone.Model

#

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

Backbone.Model

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

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

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

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

Attach all inheritable methods to the Model prototype.

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

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

    _formerAttributes : null,
#

Has the item been changed since the last changed event?

    _changed : false,
#

Create a new model with identical attributes to this one.

    clone : function() {
+  };

Attach all inheritable methods to the Model prototype.

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

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

    _formerAttributes : null,

Has the item been changed since the last changed event?

    _changed : false,

Return a copy of the model's attributes object.

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

Default URL for the model's representation on the server -- if you're +using Backbone's restful methods, override this to change the endpoint +that will be called.

    url : function() {
+      var base = this.collection.url();
+      if (this.isNew()) return base;
+      return base + '/' + this.id;
+    },

String representation of the model. Override this to provide a nice way +to print models to the console.

    toString : function() {
+      return 'Model ' + this.id;
+    },

Create a new model with identical attributes to this one.

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

Are this model's attributes identical to another model?

    isEqual : function(other) {
+    },

Are this model's attributes identical to another model?

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

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

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

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

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

    changed : function() {
+    },

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 = {});
+      if (!attrs) return this;
+      attrs = attrs._attributes || attrs;
+      var now = this._attributes;

Run validation if validate is defined.

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

Check for changes of id.

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

Update attributes.

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

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

    unset : function(attr, options) {
+      options || (options = {});
+      var value = this._attributes[attr];
+      delete this._attributes[attr];
+      if (!options.silent) {
+        this._changed = true;
+        this.trigger('change:' + attr, this);
+        this.change();
+      }
+      return value;
+    },

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

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

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

Determine if the model has changed since the last changed event. If you specify an attribute name, determine if that attribute has changed.

    hasChanged : function(attr) {
       if (attr) return this._formerAttributes[attr] != this._attributes[attr];
       return this._changed;
-    },
#

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

    formerValue : function(attr) {
-      if (!attr || !this._formerAttributes) return null;
-      return this._formerAttributes[attr];
-    },
#

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

    formerAttributes : function() {
-      return this._formerAttributes;
-    },
#

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

Return an object containing all the attributes that have changed, or false if there are no changed attributes. Useful for determining what parts of a view need to be updated and/or what attributes need to be persisted to the server.

    changedAttributes : function(now) {
@@ -116,206 +149,222 @@ the server.

} } return changed; - },
#

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

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

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

    formerValue : function(attr) {
+      if (!attr || !this._formerAttributes) return null;
+      return this._formerAttributes[attr];
+    },

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

    formerAttributes : function() {
+      return this._formerAttributes;
+    },

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

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

Get the value of an attribute.

    get : function(attr) {
-      return this._attributes[attr];
-    },
#

Remove an attribute from the model, firing changed unless you choose to -silence it.

    unset : function(attr, options) {
-      options || (options = {});
-      var value = this._attributes[attr];
-      delete this._attributes[attr];
-      if (!options.silent) this.changed();
-      return value;
-    },
#

Return a copy of the model's attributes.

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

Bind all methods in the list to the model.

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

Return the URL used to {save,delete}

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

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

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

Destroy this model on the server.

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

Destroy this model on the server.

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

Backbone.Collection

#

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

Backbone.Collection

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

  Backbone.Collection = function(options) {
+its models in sort order, as they're added and removed.

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

Define the Collection's inheritable methods.

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

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

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

Get a model from the set by id.

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

Get a model from the set by client id.

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

Define the Collection's inheritable methods.

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

Override this function to get convenient logging in the console.

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

Get a model from the set by id.

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

Get a model from the set by client id.

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

What are the ids for every model in the set?

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

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

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

Get the model at the given index.

    at: function(index) {
+    },

Get the model at the given index.

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

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

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

What are the ids for every model in the set?

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

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

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

Pluck an attribute from each model in the collection.

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

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

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

Internal implementation of adding a single model to the set.

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

Internal implementation of adding a single model to the set.

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

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

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

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

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

Internal implementation of removing a single model from the set.

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

Internal implementation of removing a single model from the set.

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

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

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

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

    sort : function(silent) {
+    },

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

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

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

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

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

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

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

Create a new instance of a model in this collection.

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

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

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

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

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

Inspect.

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

Underscore methods that we want to implement on the Collection.

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

Underscore methods that we want to implement on the Collection.

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

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

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

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

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

Backbone.View

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

Backbone.View

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

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

Set up all interitable view properties and methods.

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

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

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

Set up all interitable Backbone.View properties and methods.

  _.extend(Backbone.View.prototype, {

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

    tagName : 'div',

Attach the jQuery function as the $ and jQuery properties.

    $       : 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;
-    },
#

jQuery lookup, scoped to the current view.

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

Quick-create a dom element with attributes.

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

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

+ +
var el = this.make('li', {'class': 'row'}, this.model.get('title'));
+
    make : function(tagName, attributes, content) {
       var el = document.createElement(tagName);
       if (attributes) $(el).attr(attributes);
       if (content) $(el).html(content);
       return el;
-    },
#

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

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

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

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

    setCallbacks : function(callbacks) {
+    },

Set callbacks, where this.callbacks is a hash of

+ +

{selector.event_name: callback_name}

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

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

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

} } return this; + },

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

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

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

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

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

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

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

- Backbone.request = function(type, model, success, error) { - +
    +
  • Use setTimeout to batch rapid-fire updates into a single request.
  • +
  • Send up the models as XML instead of JSON.
  • +
  • Persist models via WebSockets instead of Ajax.
  • +
  Backbone.request = function(type, model, success, error) {
+    var data = model.attributes ? {model : JSON.stringify(model.attributes())} : {};
     $.ajax({
       url       : model.url(),
       type      : type,
-      data      : {model : JSON.stringify(model.attributes())},
+      data      : data,
       dataType  : 'json',
       success   : success,
       error     : error
     });
-  }
+  };
 
 })();
 
diff --git a/docs/backbone.png b/docs/backbone.png
new file mode 100644
index 00000000..b69682c8
Binary files /dev/null and b/docs/backbone.png differ
diff --git a/docs/background.png b/docs/background.png
new file mode 100644
index 00000000..fda0500f
Binary files /dev/null and b/docs/background.png differ
diff --git a/docs/docco.css b/docs/docco.css
index 1ddec891..b2e60197 100644
--- a/docs/docco.css
+++ b/docs/docco.css
@@ -1,8 +1,8 @@
 /*--------------------- Layout and Typography ----------------------------*/
 body {
   font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif;
-  font-size: 16px;
-  line-height: 24px;
+  font-size: 15px;
+  line-height: 22px;
   color: #252519;
   margin: 0; padding: 0;
 }
@@ -72,10 +72,11 @@ table td {
   outline: 0;
 }
   td.docs, th.docs {
-    max-width: 500px;
-    min-width: 500px;
+    max-width: 450px;
+    min-width: 450px;
     min-height: 5px;
     padding: 10px 25px 1px 50px;
+    overflow-x: hidden;
     vertical-align: top;
     text-align: left;
   }
@@ -89,10 +90,10 @@ table td {
       font-size: 12px;
       padding: 0 0.2em;
     }
-    .octowrap {
+    .pilwrap {
       position: relative;
     }
-      .octothorpe {
+      .pilcrow {
         font: 12px Arial;
         text-decoration: none;
         color: #454545;
@@ -102,11 +103,11 @@ table td {
         opacity: 0;
         -webkit-transition: opacity 0.2s linear;
       }
-        td.docs:hover .octothorpe {
+        td.docs:hover .pilcrow {
           opacity: 1;
         }
   td.code, th.code {
-    padding: 14px 15px 16px 50px;
+    padding: 14px 15px 16px 25px;
     width: 100%;
     vertical-align: top;
     background: #f5f5ff;
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..ecda7ff4
--- /dev/null
+++ b/index.html
@@ -0,0 +1,311 @@
+
+
+
+  
+  
+  Backbone: ...
+  
+
+
+
+  
+ + Introduction + + + Events + + + + Model + + + + Collection + + + + Request + + + + View + + +
+ +
+ +

+ Backbone.js +

+ +

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

+ +

+ The project is hosted on GitHub, + and the annotated source code is available, + as well as an online test suite. +

+ +

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

+ +

+ Downloads & Dependencies + (Right-click, and use "Save As") +

+ + + + + + + + + + +
Development Version (0.1.0)22kb, Uncompressed with Comments
Production Version (0.1.0)2kb, Packed and Gzipped
+ +

+ Backbone's only hard dependency is + Underscore.js. + For RESTful persistence, and DOM manipulation with + Backbone.View, + it's highly recommended to include jQuery, + and json2.js + (which you probably already have). +

+ +

Introduction

+ +

+ The core idea behind Backbone is to avoid tying your data to the DOM. It's + too easy to create JavaScript applications that end up as tangled piles of + jQuery selectors and callbacks, all trying frantically to keep data in + sync between the UI, your JavaScript logic, and the database on your + server. For rich client-side applications, a more structured approach + is helpful. +

+ +

+ With Backbone, you represent your data as + Models, which can be created, validated, destroyed, + and saved to the server. Whenever a UI action causes an attribute of + a model to change, the model triggers a change event, and all + the Views that are displaying the model's data are + notified, causing them to re-render. You don't have to write the glue + code that looks into the DOM to find an element with a specific id, + and update the HTML contents + — when the model changes, the views simply update themselves. +

+ +

+ How is this different than + SproutCore or + Cappuccino? + +

+ +

+ This question is frequently asked, and all three projects apply general + Model-View-Controller + principles to JavaScript applications. However, there isn't much basis + for comparsion. SproutCore and Cappuccino provide rich UI widgets, vast + core libraries, and determine the structure of your HTML for you. + Loading the "Hello World" of SproutCore includes 2.5 megabytes of JavaScript on the + page; the "Hello World" of Cappuccino includes 1.7 megabytes of JS and images. + Backbone is a mere 2 kilobyte include that provides the core concepts of + models, events (key-value observing), collections, views, and persistence. +

+ +

Backbone.Events

+ +

+ Events is a module that can be mixed in to any object, giving the + object the ability to bind and trigger custom events. For example: +

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

Change Log

+ +

+ 0.1.0
+ Initial Backbone release. +

+ +

+
+ + A DocumentCloud Project + +

+ +
+ +
+ + + diff --git a/test/bindable.js b/test/bindable.js index 465a6767..1d92bfab 100644 --- a/test/bindable.js +++ b/test/bindable.js @@ -2,41 +2,41 @@ $(document).ready(function() { module("Backbone bindable"); - test("bindable: bind and trigger", function() { - var obj = { counter: 0 } - _.extend(obj,Backbone.Bindable); - obj.bind('foo',function() { obj.counter += 1; }); - obj.trigger('foo'); - equals(obj.counter,1,'counter should be incremented.'); - obj.trigger('foo'); - obj.trigger('foo'); - obj.trigger('foo'); - obj.trigger('foo'); - equals(obj.counter,5,'counter should be incremented five times.'); - }); - - test("bindable: bind, then unbind all functions", function() { - var obj = { counter: 0 } - _.extend(obj,Backbone.Bindable); - var callback = function() { obj.counter += 1; } - obj.bind('foo', callback); - obj.trigger('foo'); - obj.unbind('foo'); - obj.trigger('foo'); - equals(obj.counter,1,'counter should have only been incremented once.') - }); + test("bindable: bind and trigger", function() { + var obj = { counter: 0 }; + _.extend(obj,Backbone.Events); + obj.bind('event', function() { obj.counter += 1; }); + obj.trigger('event'); + equals(obj.counter,1,'counter should be incremented.'); + obj.trigger('event'); + obj.trigger('event'); + obj.trigger('event'); + obj.trigger('event'); + equals(obj.counter, 5, 'counter should be incremented five times.'); + }); - test("bindable: bind two callbacks, unbind only one", function() { - var obj = { counterA: 0, counterB: 0 } - _.extend(obj,Backbone.Bindable); - var callback = function() { obj.counterA += 1; }; - obj.bind('foo', callback); - obj.bind('foo', function() { obj.counterB += 1 }); - obj.trigger('foo'); - obj.unbind('foo', callback); - obj.trigger('foo'); - equals(obj.counterA,1,'counterA should have only been incremented once.') - equals(obj.counterB,2,'counterB should have been incremented twice.') - }); + test("bindable: bind, then unbind all functions", function() { + var obj = { counter: 0 }; + _.extend(obj,Backbone.Events); + var callback = function() { obj.counter += 1; }; + obj.bind('event', callback); + obj.trigger('event'); + obj.unbind('event'); + obj.trigger('event'); + equals(obj.counter, 1, 'counter should have only been incremented once.'); + }); + + test("bindable: bind two callbacks, unbind only one", function() { + var obj = { counterA: 0, counterB: 0 }; + _.extend(obj,Backbone.Events); + var callback = function() { obj.counterA += 1; }; + obj.bind('event', callback); + obj.bind('event', function() { obj.counterB += 1; }); + obj.trigger('event'); + obj.unbind('event', callback); + obj.trigger('event'); + equals(obj.counterA, 1, 'counterA should have only been incremented once.'); + equals(obj.counterB, 2, 'counterB should have been incremented twice.'); + }); }); \ No newline at end of file diff --git a/test/collection.js b/test/collection.js index e69de29b..cfd27a66 100644 --- a/test/collection.js +++ b/test/collection.js @@ -0,0 +1,113 @@ +$(document).ready(function() { + + module("Backbone collections"); + + window.lastRequest = null; + + Backbone.request = function() { + lastRequest = _.toArray(arguments); + }; + + var a = new Backbone.Model({id: 4, label: 'a'}); + var b = new Backbone.Model({id: 3, label: 'b'}); + var c = new Backbone.Model({id: 2, label: 'c'}); + var d = new Backbone.Model({id: 1, label: 'd'}); + var e = null; + var col = window.col = new Backbone.Collection([a,b,c,d]); + + test("collections: new and sort", function() { + equals(col.first(), a, "a should be first"); + equals(col.last(), d, "d should be last"); + col.comparator = function(model) { return model.id; }; + col.sort(); + equals(col.first(), d, "d should be first"); + equals(col.last(), a, "a should be last"); + equals(col.length, 4); + }); + + test("collections: toString", function() { + equals(col.toString(), 'Collection (4 models)'); + }); + + test("collections: get, getByCid", function() { + equals(col.get(1), d); + equals(col.get(3), b); + equals(col.getByCid(col.first().cid), col.first()); + }); + + test("collections: getIds, getCids", function() { + equals(col.getIds().sort().join(' '), '1 2 3 4'); + equals(col.getCids().sort().join(' '), 'c1 c2 c3 c4'); + }); + + test("collections: at", function() { + equals(col.at(2), b); + }); + + test("collections: pluck", function() { + equals(col.pluck('label').join(' '), 'd c b a'); + }); + + test("collections: add", function() { + var added = null; + col.bind('add', function(model){ added = model.get('label'); }); + e = new Backbone.Model({id: 0, label : 'e'}); + col.add(e); + equals(added, 'e'); + equals(col.length, 5); + equals(col.first(), e); + }); + + test("collections: remove", function() { + var removed = null; + col.bind('remove', function(model){ removed = model.get('label'); }); + col.remove(e); + equals(removed, 'e'); + equals(col.length, 4); + equals(col.first(), d); + }); + + test("collections: refresh", function() { + var refreshed = 0; + var models = col.models; + col.bind('refresh', function() { refreshed += 1; }); + col.refresh([]); + equals(refreshed, 1); + equals(col.length, 0); + equals(col.last(), null); + col.refresh(models); + equals(refreshed, 2); + equals(col.length, 4); + equals(col.last(), a); + }); + + test("collections: fetch", function() { + col.fetch(); + equals(lastRequest[0], 'GET'); + equals(lastRequest[1], col); + }); + + test("collections: create", function() { + var model = col.create({label: 'f'}); + equals(lastRequest[0], 'POST'); + equals(lastRequest[1], model); + equals(model.get('label'), 'f'); + equals(model.collection, col); + }); + + test("collections: Underscore methods", function() { + equals(col.map(function(model){ return model.get('label'); }).join(' '), 'd c b a'); + equals(col.any(function(model){ return model.id === 100; }), false); + equals(col.any(function(model){ return model.id === 1; }), true); + equals(col.indexOf(b), 2); + equals(col.size(), 4); + equals(col.rest().length, 3); + ok(!_.include(col.rest()), a); + ok(!_.include(col.rest()), d); + ok(!col.isEmpty()); + ok(!_.include(col.without(d)), d); + equals(col.max(function(model){ return model.id; }).id, 4); + equals(col.min(function(model){ return model.id; }).id, 1); + }); + +}); diff --git a/test/model.js b/test/model.js index a836bc95..c2797666 100644 --- a/test/model.js +++ b/test/model.js @@ -2,43 +2,140 @@ $(document).ready(function() { module("Backbone model"); + // Variable to catch the last request. + window.lastRequest = null; + + // Stub out Backbone.request... + Backbone.request = function() { + lastRequest = _.toArray(arguments); + }; + + var attrs = { + id : '1-the-tempest', + title : "The Tempest", + author : "Bill Shakespeare", + length : 123 + }; + + var doc = new Backbone.Model(attrs); + + var klass = Backbone.Collection.extend({ + url : function() { return '/collection'; } + }); + + var collection = new klass(); + collection.add(doc); + + test("model: attributes", function() { + ok(doc.attributes() !== attrs, "Attributes are different objects."); + ok(_.isEqual(doc.attributes(), attrs), "but with identical contents."); + }); + + test("model: url", function() { + equals(doc.url(), '/collection/1-the-tempest'); + }); + + test("model: toString", function() { + equals(doc.toString(), 'Model 1-the-tempest'); + }); + test("model: clone", function() { - attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; - a = new Backbone.Model(attrs); - b = a.clone(); - equals(a.get('foo'),1); - equals(a.get('bar'),2); - equals(a.get('baz'),3); - equals(b.get('foo'),a.get('foo'),"Foo should be the same on the clone."); - equals(b.get('bar'),a.get('bar'),"Bar should be the same on the clone."); - equals(b.get('baz'),a.get('baz'),"Baz should be the same on the clone."); + attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; + a = new Backbone.Model(attrs); + b = a.clone(); + equals(a.get('foo'), 1); + equals(a.get('bar'), 2); + equals(a.get('baz'), 3); + equals(b.get('foo'), a.get('foo'), "Foo should be the same on the clone."); + equals(b.get('bar'), a.get('bar'), "Bar should be the same on the clone."); + equals(b.get('baz'), a.get('baz'), "Baz should be the same on the clone."); + a.set({foo : 100}); + equals(a.get('foo'), 100); + equals(b.get('foo'), 1, "Changing a parent attribute does not change the clone."); }); test("model: isEqual", function() { - attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; - a = new Backbone.Model(attrs); - b = new Backbone.Model(attrs); - ok(a.isEqual(b),"a should equal b"); - c = new Backbone.Model({ 'foo': 1, 'bar': 2, 'baz': 3, 'qux': 4}); - ok(!a.isEqual(c),"a should not equal c"); - - }) - - test("model: isNew", function() { - attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; - a = new Backbone.Model(attrs); - ok(a.isNew()); - }) - - test("model: set", function() { - attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; - a = new Backbone.Model(attrs); - var changeCount = 0; - a.bind("change", function() { changeCount += 1}); - a.set({'foo': 2}); - ok(a.get('foo')==2, "Foo should have changed."); - ok(changeCount == 1, "Change count should have incremented."); - + attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; + a = new Backbone.Model(attrs); + b = new Backbone.Model(attrs); + ok(a.isEqual(b), "a should equal b"); + c = new Backbone.Model({ 'foo': 1, 'bar': 2, 'baz': 3, 'qux': 4}); + ok(!a.isEqual(c), "a should not equal c"); }); -}); \ No newline at end of file + test("model: isNew", function() { + attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; + a = new Backbone.Model(attrs); + ok(a.isNew(), "it should be new"); + attrs = { 'foo': 1, 'bar': 2, 'baz': 3, 'id': -5 }; + ok(a.isNew(), "any defined ID is legal, negative or positive"); + }); + + test("model: get", function() { + equals(doc.get('title'), 'The Tempest'); + equals(doc.get('author'), 'Bill Shakespeare'); + }); + + test("model: set and unset", function() { + attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; + a = new Backbone.Model(attrs); + var changeCount = 0; + a.bind("change:foo", function() { changeCount += 1; }); + a.set({'foo': 2}); + ok(a.get('foo')== 2, "Foo should have changed."); + ok(changeCount == 1, "Change count should have incremented."); + a.set({'foo': 2}); // set with value that is not new shouldn't fire change event + ok(a.get('foo')== 2, "Foo should NOT have changed, still 2"); + ok(changeCount == 1, "Change count should NOT have incremented."); + + a.unset('foo'); + ok(a.get('foo')== null, "Foo should have changed"); + ok(changeCount == 2, "Change count should have incremented for unset."); + }); + + test("model: changed, hasChanged, changedAttributes, formerValue, formerAttributes", function() { + var model = new Backbone.Model({name : "Tim", age : 10}); + model.bind('change', function() { + ok(model.hasChanged('name'), 'name changed'); + ok(!model.hasChanged('age'), 'age did not'); + ok(_.isEqual(model.changedAttributes(), {name : 'Rob'}), 'changedAttributes returns the changed attrs'); + equals(model.formerValue('name'), 'Tim'); + ok(_.isEqual(model.formerAttributes(), {name : "Tim", age : 10}), 'formerAttributes is correct'); + }); + model.set({name : 'Rob'}, {silent : true}); + model.change(); + equals(model.get('name'), 'Rob'); + }); + + test("model: save", function() { + doc.save({title : "Henry V"}); + equals(lastRequest[0], 'PUT'); + ok(_.isEqual(lastRequest[1], doc)); + }); + + test("model: destroy", function() { + doc.destroy(); + equals(lastRequest[0], 'DELETE'); + ok(_.isEqual(lastRequest[1], doc)); + }); + + test("model: validate", function() { + var lastError; + var model = new Backbone.Model(); + model.validate = function(attrs) { + if (attrs.admin) return "Can't change admin status."; + }; + model.bind('error', function(model, error) { + lastError = error; + }); + var result = model.set({a: 100}); + equals(result, model); + equals(model.get('a'), 100); + equals(lastError, undefined); + result = model.set({a: 200, admin: true}); + equals(result, false); + equals(model.get('a'), 100); + equals(lastError, "Can't change admin status."); + }); + +});