- 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 comparison. SproutCore and Cappuccino provide rich UI widgets, vast
- core libraries, and determine the structure of your HTML for you.
- Both frameworks measure in the hundreds of kilobytes when packed and
- gzipped, and megabytes of JavaScript, CSS, and images when loaded in the browser
- — there's a lot of room underneath for libraries of a more moderate scope.
- Backbone is a 4 kilobyte include that provides
- just the core concepts of models, events, collections, views, controllers,
- and persistence.
-
-
Many of the examples that follow are runnable. Click the play button
to execute them.
@@ -550,16 +540,16 @@
Backbone.Model
Get the current value of an attribute from the model. For example:
note.get("title")
-
+
escapemodel.escape(attribute)
Similar to get, but returns the HTML-escaped version
of a model's attribute. If you're interpolating data from the model into
- HTML, using escape to retrieve attributes will prevent
+ HTML, using escape to retrieve attributes will prevent
XSS attacks.
-
+
var hacker = new Backbone.Model({
name: "<script>alert('xss')</script>"
@@ -1833,6 +1823,127 @@
Examples
+
F.A.Q.
+
+
+ Catalog of Events
+
+ Here's a list of all of the built-in events that Backbone.js can fire.
+ You're also free to trigger your own events on Models and Views as you
+ see fit.
+
+
+
+
"add" (model, collection) — when a model is added to a collection.
+
"remove" (model, collection) — when a model is removed from a collection.
+
"refresh" (collection) — when the collection's entire contents have been replaced.
+
"change" (model, collection) — when a model's attributes have changed.
+
"change:[attribute]" (model, collection) — when a specific attribute has been updated.
+
"error" (model, collection) — when a model's validation fails, or a save call fails on the server.
+
"route:[name]" (controller) — when one of a controller's routes has matched.
+
"all" — this special event fires for any triggered event, passing the event name as the first argument.
+
+
+
+ Nested Models & Collections
+
+ It's common to nest collections inside of models with Backbone. For example,
+ consider a Mailbox model that contains many Message models.
+ One nice pattern for handling this is have a this.messages collection
+ for each mailbox, enabling the lazy-loading of messages, when the mailbox
+ is first opened ... perhaps with MessageList views listening for
+ "add" and "remove" events.
+
+
+
+var Mailbox = Backbone.Model.extend({
+
+ initialize: function() {
+ this.messages = new Messages;
+ this.messages.url = '/mailbox/' + this.id + '/messages';
+ this.messages.bind("refresh", this.updateCounts);
+ },
+
+ ...
+
+});
+
+var Inbox = new Mailbox;
+
+// And then, when the Inbox is opened:
+
+Inbox.messages.fetch();
+
+
+
+ How does Backbone relate to "traditional" MVC?
+
+ Different implementations of the
+ Model-View-Controller
+ pattern tend to disagree about the definition of a controller. If it helps any, in
+ Backbone, the View class can also be thought of as a
+ kind of controller, dispatching events that originate from the UI, with
+ the HTML template serving as the true view. We call it a View because it
+ represents a logical chunk of UI, responsible for the contents of a single
+ DOM element.
+
+
+
+ Binding "this"
+
+ Perhaps the single most common JavaScript "gotcha" is the fact that when
+ you pass a function as a callback, it's value for this is lost. With
+ Backbone, when dealing with events and callbacks,
+ you'll often find it useful to rely on
+ _.bind and
+ _.bindAll
+ from Underscore.js. _.bind takes a function and an object to be
+ used as this, any time the function is called in the future.
+ _.bindAll takes an object and a list of method names: each method
+ in the list will be bound to the object, so that it's this may
+ not change. For example, in a View that listens for
+ changes to a collection...
+
+
+ How is Backbone 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 comparison. SproutCore and Cappuccino provide rich UI widgets, vast
+ core libraries, and determine the structure of your HTML for you.
+ Both frameworks measure in the hundreds of kilobytes when packed and
+ gzipped, and megabytes of JavaScript, CSS, and images when loaded in the browser
+ — there's a lot of room underneath for libraries of a more moderate scope.
+ Backbone is a 4 kilobyte include that provides
+ just the core concepts of models, events, collections, views, controllers,
+ and persistence.
+
+
Change Log
@@ -1841,7 +1952,7 @@
Change Log
jQuery, as a framework for DOM manipulation and Ajax support.
Implemented Model#escape, to efficiently handle
attributes intended for HTML interpolation. When trying to persist a model,
- failed requests will now trigger an "error" event. The
+ failed requests will now trigger an "error" event. The
ubiquitous options argument is now passed as the final argument
to all "change" events.
From f194f2d53c0fd362b872573a922b1833c9f287cf Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Mon, 6 Dec 2010 11:33:42 -0500
Subject: [PATCH 008/167] adding an FAQ section.
---
index.html | 161 ++++++++++++++++++++++++++++++++++++++++++++---------
1 file changed, 136 insertions(+), 25 deletions(-)
diff --git a/index.html b/index.html
index 25a9462b3..df6000f33 100644
--- a/index.html
+++ b/index.html
@@ -263,6 +263,17 @@
Examples
+
+ F.A.Q.
+
+
- 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 comparison. SproutCore and Cappuccino provide rich UI widgets, vast
- core libraries, and determine the structure of your HTML for you.
- Both frameworks measure in the hundreds of kilobytes when packed and
- gzipped, and megabytes of JavaScript, CSS, and images when loaded in the browser
- — there's a lot of room underneath for libraries of a more moderate scope.
- Backbone is a 4 kilobyte include that provides
- just the core concepts of models, events, collections, views, controllers,
- and persistence.
-
-
Many of the examples that follow are runnable. Click the play button
to execute them.
@@ -550,16 +540,16 @@
Backbone.Model
Get the current value of an attribute from the model. For example:
note.get("title")
-
+
escapemodel.escape(attribute)
Similar to get, but returns the HTML-escaped version
of a model's attribute. If you're interpolating data from the model into
- HTML, using escape to retrieve attributes will prevent
+ HTML, using escape to retrieve attributes will prevent
XSS attacks.
-
+
var hacker = new Backbone.Model({
name: "<script>alert('xss')</script>"
@@ -1833,6 +1823,127 @@
Examples
+
F.A.Q.
+
+
+ Catalog of Events
+
+ Here's a list of all of the built-in events that Backbone.js can fire.
+ You're also free to trigger your own events on Models and Views as you
+ see fit.
+
+
+
+
"add" (model, collection) — when a model is added to a collection.
+
"remove" (model, collection) — when a model is removed from a collection.
+
"refresh" (collection) — when the collection's entire contents have been replaced.
+
"change" (model, collection) — when a model's attributes have changed.
+
"change:[attribute]" (model, collection) — when a specific attribute has been updated.
+
"error" (model, collection) — when a model's validation fails, or a save call fails on the server.
+
"route:[name]" (controller) — when one of a controller's routes has matched.
+
"all" — this special event fires for any triggered event, passing the event name as the first argument.
+
+
+
+ Nested Models & Collections
+
+ It's common to nest collections inside of models with Backbone. For example,
+ consider a Mailbox model that contains many Message models.
+ One nice pattern for handling this is have a this.messages collection
+ for each mailbox, enabling the lazy-loading of messages, when the mailbox
+ is first opened ... perhaps with MessageList views listening for
+ "add" and "remove" events.
+
+
+
+var Mailbox = Backbone.Model.extend({
+
+ initialize: function() {
+ this.messages = new Messages;
+ this.messages.url = '/mailbox/' + this.id + '/messages';
+ this.messages.bind("refresh", this.updateCounts);
+ },
+
+ ...
+
+});
+
+var Inbox = new Mailbox;
+
+// And then, when the Inbox is opened:
+
+Inbox.messages.fetch();
+
+
+
+ How does Backbone relate to "traditional" MVC?
+
+ Different implementations of the
+ Model-View-Controller
+ pattern tend to disagree about the definition of a controller. If it helps any, in
+ Backbone, the View class can also be thought of as a
+ kind of controller, dispatching events that originate from the UI, with
+ the HTML template serving as the true view. We call it a View because it
+ represents a logical chunk of UI, responsible for the contents of a single
+ DOM element.
+
+
+
+ Binding "this"
+
+ Perhaps the single most common JavaScript "gotcha" is the fact that when
+ you pass a function as a callback, it's value for this is lost. With
+ Backbone, when dealing with events and callbacks,
+ you'll often find it useful to rely on
+ _.bind and
+ _.bindAll
+ from Underscore.js. _.bind takes a function and an object to be
+ used as this, any time the function is called in the future.
+ _.bindAll takes an object and a list of method names: each method
+ in the list will be bound to the object, so that it's this may
+ not change. For example, in a View that listens for
+ changes to a collection...
+
+
+ How is Backbone 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 comparison. SproutCore and Cappuccino provide rich UI widgets, vast
+ core libraries, and determine the structure of your HTML for you.
+ Both frameworks measure in the hundreds of kilobytes when packed and
+ gzipped, and megabytes of JavaScript, CSS, and images when loaded in the browser
+ — there's a lot of room underneath for libraries of a more moderate scope.
+ Backbone is a 4 kilobyte include that provides
+ just the core concepts of models, events, collections, views, controllers,
+ and persistence.
+
+
Change Log
@@ -1841,7 +1952,7 @@
Change Log
jQuery, as a framework for DOM manipulation and Ajax support.
Implemented Model#escape, to efficiently handle
attributes intended for HTML interpolation. When trying to persist a model,
- failed requests will now trigger an "error" event. The
+ failed requests will now trigger an "error" event. The
ubiquitous options argument is now passed as the final argument
to all "change" events.
From 21a3675db926831999096c3e368e1be5a78738a4 Mon Sep 17 00:00:00 2001
From: Matt Todd
Date: Tue, 7 Dec 2010 02:39:39 -0500
Subject: [PATCH 009/167] Implement model.is() for attr test, with docs, tests
---
backbone.js | 5 +++++
index.html | 8 ++++++++
test/model.js | 15 +++++++++++++++
3 files changed, 28 insertions(+)
diff --git a/backbone.js b/backbone.js
index 4217e6254..60f06d21f 100644
--- a/backbone.js
+++ b/backbone.js
@@ -158,6 +158,11 @@
return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : val);
},
+ // Returns true if the attribute evalutates to a truthy value. False otherwise.
+ is : function(attr) {
+ return !!this.attributes[attr] === true;
+ },
+
// Set a hash of model attributes on the object, firing `"change"` unless you
// choose to silence it.
set : function(attrs, options) {
diff --git a/index.html b/index.html
index df6000f33..b42423fe0 100644
--- a/index.html
+++ b/index.html
@@ -168,6 +168,7 @@
// And then, when the Inbox is opened:
Inbox.messages.fetch();
+
+
+
+ Loading Bootstrapped Models
+
+ When your app first loads, it's common to have a set of initial models that
+ you know you're going to need, in order to render the page. Instead of
+ firing an extra AJAX request to fetch them,
+ a nicer pattern is to have their data already bootstrapped into the page.
+ You can then use refresh to populate your
+ collections with the initial data. At DocumentCloud, in the
+ ERB template for the
+ workspace, we do something along these lines:
+
// And then, when the Inbox is opened:
Inbox.messages.fetch();
+
+
+
+ Loading Bootstrapped Models
+
+ When your app first loads, it's common to have a set of initial models that
+ you know you're going to need, in order to render the page. Instead of
+ firing an extra AJAX request to fetch them,
+ a nicer pattern is to have their data already bootstrapped into the page.
+ You can then use refresh to populate your
+ collections with the initial data. At DocumentCloud, in the
+ ERB template for the
+ workspace, we do something along these lines:
+
- escapemodel.escape(attribute)
-
- Similar to get, but returns the HTML-escaped version
- of a model's attribute. If you're interpolating data from the model into
- HTML, using escape to retrieve attributes will prevent
- XSS attacks.
-
callback in the options, which will be invoked instead of triggering an
"error" event, should validation fail.
+
+
+ escapemodel.escape(attribute)
+
+ Similar to get, but returns the HTML-escaped version
+ of a model's attribute. If you're interpolating data from the model into
+ HTML, using escape to retrieve attributes will prevent
+ XSS attacks.
+
- 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.
-
Returns the relative URL where the model's resource would be located on
the server. If your models are located somewhere else, override this method
- with the correct logic. Generates URLs of the form: "/[collection]/[id]".
+ with the correct logic. Generates URLs of the form: "/[collection.url]/[id]",
+ falling back to "/[urlRoot]/id" if the model is not part of a collection.
Delegates to Collection#url to generate the
- URL, so make sure that you have it defined.
+ URL, so make sure that you have it defined, or a urlRoot
+ property, if all models of this class share a common root URL.
A model with an id of 101, stored in a
- Backbone.Collection with a url of "/notes",
- would have this URL: "/notes/101"
+ Backbone.Collection with a url of "/documents/7/notes",
+ would have this URL: "/documents/7/notes/101"
+
+
+
+ urlRootmodel.urlRoot
+
+ Specify a urlRoot if you're using a model outside of a collection,
+ to enable the default url function to generate
+ URLs based on the model id. "/[urlRoot]/id"
+
+
+var Book = Backbone.Model.extend({urlRoot : '/books'});
+
+var solaris = new Book({id: "1083-lem-solaris"});
+
+alert(solaris.url());
+
parsemodel.parse(response)
From 53ae5b5cdfaae0dc8ccfd6cf60d477cfb0561bbc Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Fri, 10 Dec 2010 11:58:41 -0500
Subject: [PATCH 017/167] Issue #132 ... initial _changed after new with
attributes.
---
backbone.js | 1 +
test/model.js | 9 +++++++++
2 files changed, 10 insertions(+)
diff --git a/backbone.js b/backbone.js
index 15d888cc5..ec1ad2dda 100644
--- a/backbone.js
+++ b/backbone.js
@@ -121,6 +121,7 @@
this._escapedAttributes = {};
this.cid = _.uniqueId('c');
this.set(attributes, {silent : true});
+ this._changed = false;
this._previousAttributes = _.clone(this.attributes);
if (options && options.collection) this.collection = options.collection;
this.initialize(attributes, options);
diff --git a/test/model.js b/test/model.js
index b6a189c52..b90f77d54 100644
--- a/test/model.js
+++ b/test/model.js
@@ -204,6 +204,15 @@ $(document).ready(function() {
equals(value, 'Ms. Sue');
});
+ test("Model: change after initialize", function () {
+ var changed = 0;
+ var attrs = {id: 1, label: 'c'};
+ var obj = new Backbone.Model(attrs);
+ obj.bind('change', function() { changed += 1; });
+ obj.set(attrs);
+ equals(changed, 0);
+ });
+
test("Model: save within change event", function () {
var model = new Backbone.Model({firstName : "Taylor", lastName: "Swift"});
model.bind('change', function () {
From 34d1d0ac936b23a04f921abc70f71ddcb90ca7d3 Mon Sep 17 00:00:00 2001
From: Sam Stephenson
Date: Sat, 11 Dec 2010 23:16:53 -0600
Subject: [PATCH 018/167] Multiple views may listen for events on the same
element
---
backbone.js | 5 +++--
test/view.js | 23 +++++++++++++++++++++++
2 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/backbone.js b/backbone.js
index ec1ad2dda..44310f69a 100644
--- a/backbone.js
+++ b/backbone.js
@@ -772,6 +772,7 @@
// Creating a Backbone.View creates its initial element outside of the DOM,
// if an existing element is not provided...
Backbone.View = function(options) {
+ this.vid = _.uniqueId('v');
this._configure(options || {});
this._ensureElement();
this.delegateEvents();
@@ -843,13 +844,13 @@
// not `change`, `submit`, and `reset` in Internet Explorer.
delegateEvents : function(events) {
if (!(events || (events = this.events))) return;
- $(this.el).unbind('.delegateEvents');
+ $(this.el).unbind('.delegateEvents' + this.vid);
for (var key in events) {
var methodName = events[key];
var match = key.match(eventSplitter);
var eventName = match[1], selector = match[2];
var method = _.bind(this[methodName], this);
- eventName += '.delegateEvents';
+ eventName += '.delegateEvents' + this.vid;
if (selector === '') {
$(this.el).bind(eventName, method);
} else {
diff --git a/test/view.js b/test/view.js
index 1fdfb71e4..60be9b887 100644
--- a/test/view.js
+++ b/test/view.js
@@ -64,4 +64,27 @@ $(document).ready(function() {
equals(view.el, document.body);
});
+ test("View: multiple views per element", function() {
+ var count = 0, ViewClass = Backbone.View.extend({
+ el: $("body"),
+ events: {
+ "click": "click"
+ },
+ click: function() {
+ count++;
+ }
+ });
+
+ var view1 = new ViewClass;
+ $("body").trigger("click");
+ equals(1, count);
+
+ var view2 = new ViewClass;
+ $("body").trigger("click");
+ equals(3, count);
+
+ view1.delegateEvents();
+ $("body").trigger("click");
+ equals(5, count);
+ });
});
From 5886fe4051df366e1e15971fc8df817c57014926 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Mon, 13 Dec 2010 09:32:04 -0500
Subject: [PATCH 019/167] Issue #134. json2.js source link.
---
index.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/index.html b/index.html
index fcaa50c62..41827d3c3 100644
--- a/index.html
+++ b/index.html
@@ -340,7 +340,7 @@
For RESTful persistence, and DOM manipulation with
Backbone.View,
it's highly recommended to include
- json2.js, and either
+ json2.js, and either
jQuery or Zepto.
From d01b1364993f7c940a16e1377c658d8b31b292eb Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Mon, 13 Dec 2010 09:40:54 -0500
Subject: [PATCH 020/167] Merging Issue #135. Multiple views per DOM element.
---
backbone.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/backbone.js b/backbone.js
index 44310f69a..c422b6d11 100644
--- a/backbone.js
+++ b/backbone.js
@@ -772,7 +772,7 @@
// Creating a Backbone.View creates its initial element outside of the DOM,
// if an existing element is not provided...
Backbone.View = function(options) {
- this.vid = _.uniqueId('v');
+ this.cid = _.uniqueId('view');
this._configure(options || {});
this._ensureElement();
this.delegateEvents();
@@ -844,13 +844,13 @@
// not `change`, `submit`, and `reset` in Internet Explorer.
delegateEvents : function(events) {
if (!(events || (events = this.events))) return;
- $(this.el).unbind('.delegateEvents' + this.vid);
+ $(this.el).unbind('.delegateEvents' + this.cid);
for (var key in events) {
var methodName = events[key];
var match = key.match(eventSplitter);
var eventName = match[1], selector = match[2];
var method = _.bind(this[methodName], this);
- eventName += '.delegateEvents' + this.vid;
+ eventName += '.delegateEvents' + this.cid;
if (selector === '') {
$(this.el).bind(eventName, method);
} else {
From 3d8fe92f1e5447acf4d76d33dd62bf368ee4b523 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Mon, 13 Dec 2010 10:15:09 -0500
Subject: [PATCH 021/167] Issue #78. Changes the Backbone.sync API to enable
passing through of options ... like {data} in fetch()
---
backbone.js | 53 ++++++++++++++++---------------
examples/backbone-localstorage.js | 6 ++--
examples/todos/todos.js | 9 ++++--
test/sync.js | 8 ++++-
4 files changed, 44 insertions(+), 32 deletions(-)
diff --git a/backbone.js b/backbone.js
index c422b6d11..beb490497 100644
--- a/backbone.js
+++ b/backbone.js
@@ -248,12 +248,13 @@
fetch : function(options) {
options || (options = {});
var model = this;
- var success = function(resp) {
+ var success = options.success;
+ options.success = function(resp) {
if (!model.set(model.parse(resp), options)) return false;
- if (options.success) options.success(model, resp);
+ if (success) success(model, resp);
};
- var error = wrapError(options.error, model, options);
- (this.sync || Backbone.sync)('read', this, success, error);
+ options.error = wrapError(options.error, model, options);
+ (this.sync || Backbone.sync)('read', this, options);
return this;
},
@@ -264,13 +265,14 @@
options || (options = {});
if (attrs && !this.set(attrs, options)) return false;
var model = this;
- var success = function(resp) {
+ var success = options.success;
+ options.success = function(resp) {
if (!model.set(model.parse(resp), options)) return false;
- if (options.success) options.success(model, resp);
+ if (success) success(model, resp);
};
- var error = wrapError(options.error, model, options);
+ options.error = wrapError(options.error, model, options);
var method = this.isNew() ? 'create' : 'update';
- (this.sync || Backbone.sync)(method, this, success, error);
+ (this.sync || Backbone.sync)(method, this, options);
return this;
},
@@ -279,12 +281,13 @@
destroy : function(options) {
options || (options = {});
var model = this;
- var success = function(resp) {
+ var success = options.success;
+ options.success = function(resp) {
if (model.collection) model.collection.remove(model);
- if (options.success) options.success(model, resp);
+ if (success) success(model, resp);
};
- var error = wrapError(options.error, model, options);
- (this.sync || Backbone.sync)('delete', this, success, error);
+ options.error = wrapError(options.error, model, options);
+ (this.sync || Backbone.sync)('delete', this, options);
return this;
},
@@ -488,12 +491,13 @@
fetch : function(options) {
options || (options = {});
var collection = this;
- var success = function(resp) {
+ var success = options.success;
+ options.success = function(resp) {
collection[options.add ? 'add' : 'refresh'](collection.parse(resp));
- if (options.success) options.success(collection, resp);
+ if (success) success(collection, resp);
};
- var error = wrapError(options.error, collection, options);
- (this.sync || Backbone.sync)('read', this, success, error);
+ options.error = wrapError(options.error, collection, options);
+ (this.sync || Backbone.sync)('read', this, options);
return this;
},
@@ -507,11 +511,12 @@
} else {
model.collection = coll;
}
- var success = function(nextModel, resp) {
+ var success = options.success;
+ options.success = function(nextModel, resp) {
coll.add(nextModel);
- if (options.success) options.success(nextModel, resp);
+ if (success) success(nextModel, resp);
};
- return model.save(null, {success : success, error : options.error});
+ return model.save(null, options);
},
// **parse** converts a response into a list of models to be added to the
@@ -921,22 +926,20 @@
// `application/json` with the model in a param named `model`.
// 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) {
+ Backbone.sync = function(method, model, options) {
var type = methodMap[method];
var modelJSON = (method === 'create' || method === 'update') ?
JSON.stringify(model.toJSON()) : null;
// Default JSON-request options.
- var params = {
+ var params = _.extend({
url: getUrl(model) || urlError(),
type: type,
contentType: 'application/json',
data: modelJSON,
dataType: 'json',
- processData: false,
- success: success,
- error: error
- };
+ processData: false
+ }, options);
// For older servers, emulate JSON by encoding the request into an HTML-form.
if (Backbone.emulateJSON) {
diff --git a/examples/backbone-localstorage.js b/examples/backbone-localstorage.js
index add3cf705..091d7f368 100644
--- a/examples/backbone-localstorage.js
+++ b/examples/backbone-localstorage.js
@@ -64,7 +64,7 @@ _.extend(Store.prototype, {
// Override `Backbone.sync` to use delegate to the model or collection's
// *localStorage* property, which should be an instance of `Store`.
-Backbone.sync = function(method, model, success, error) {
+Backbone.sync = function(method, model, options) {
var resp;
var store = model.localStorage || model.collection.localStorage;
@@ -77,8 +77,8 @@ Backbone.sync = function(method, model, success, error) {
}
if (resp) {
- success(resp);
+ options.success(resp);
} else {
- error("Record not found");
+ options.error("Record not found");
}
};
\ No newline at end of file
diff --git a/examples/todos/todos.js b/examples/todos/todos.js
index e4a7fa4c0..3e2ef6b4d 100644
--- a/examples/todos/todos.js
+++ b/examples/todos/todos.js
@@ -12,13 +12,16 @@ $(function(){
// Our basic **Todo** model has `content`, `order`, and `done` attributes.
window.Todo = Backbone.Model.extend({
- // If you don't provide a todo, one will be provided for you.
- EMPTY: "empty todo...",
+ // Default attributes for the todo.
+ defaults: {
+ content: "empty todo...",
+ done: false
+ },
// Ensure that each todo created has `content`.
initialize: function() {
if (!this.get("content")) {
- this.set({"content": this.EMPTY});
+ this.set({"content": this.defaults.content});
}
},
diff --git a/test/sync.js b/test/sync.js
index 0d21b23b8..02921e925 100644
--- a/test/sync.js
+++ b/test/sync.js
@@ -31,6 +31,13 @@ $(document).ready(function() {
ok(_.isEmpty(lastRequest.data));
});
+ test("sync: passing data", function() {
+ library.fetch({data: {a: 'a', one: 1}});
+ equals(lastRequest.url, '/library');
+ equals(lastRequest.data.a, 'a');
+ equals(lastRequest.data.one, 1);
+ });
+
test("sync: create", function() {
library.add(library.create(attrs));
equals(lastRequest.url, '/library');
@@ -42,7 +49,6 @@ $(document).ready(function() {
equals(data.length, 123);
});
-
test("sync: update", function() {
library.first().save({id: '1-the-tempest', author: 'William Shakespeare'});
equals(lastRequest.url, '/library/1-the-tempest');
From ad2ae8e162106c174ddf1b798d39d352511b853b Mon Sep 17 00:00:00 2001
From: Joshua Peek
Date: Thu, 16 Dec 2010 20:13:15 -0600
Subject: [PATCH 022/167] Pass Collection#fetch options along to refresh
Matches the behavior of Model#fetch
---
backbone.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backbone.js b/backbone.js
index beb490497..f24ff3042 100644
--- a/backbone.js
+++ b/backbone.js
@@ -493,7 +493,7 @@
var collection = this;
var success = options.success;
options.success = function(resp) {
- collection[options.add ? 'add' : 'refresh'](collection.parse(resp));
+ collection[options.add ? 'add' : 'refresh'](collection.parse(resp), options);
if (success) success(collection, resp);
};
options.error = wrapError(options.error, collection, options);
From 43176d96f96445edb2cb179cfc651cbb7e3ff7ad Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Fri, 17 Dec 2010 13:00:37 -0500
Subject: [PATCH 023/167] Expanding View#el documentation.
---
index.html | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/index.html b/index.html
index 41827d3c3..af0341b8f 100644
--- a/index.html
+++ b/index.html
@@ -1662,6 +1662,10 @@
Backbone.View
this.el is created from the view's tagName, className,
and id properties, if specified. If not, el is an empty div.
+ You may assign el directly in your view's
+ initialize function, if the view is being
+ created for an element that already exists in the DOM. Make sure to assign
+ a real DOM element, and not a jQuery object.
From 71c98381f0d6066030a9a37b7802ee3d13e9a4d4 Mon Sep 17 00:00:00 2001
From: Sam Stephenson
Date: Fri, 17 Dec 2010 12:33:12 -0600
Subject: [PATCH 024/167] If Backbone.View#el is a string, pass it through
$(...).get(0) in _ensureElement
---
backbone.js | 18 +++++++++++++-----
test/view.js | 24 +++++++++++++++++++++++-
2 files changed, 36 insertions(+), 6 deletions(-)
diff --git a/backbone.js b/backbone.js
index f24ff3042..a3b288edc 100644
--- a/backbone.js
+++ b/backbone.js
@@ -879,12 +879,20 @@
},
// Ensure that the View has a DOM element to render into.
+ // If `this.el` is a string, pass it through `$()`, take the first
+ // matching element, and re-assign it to `el`. Otherwise, create
+ // an element from the `id`, `className` and `tagName` proeprties.
_ensureElement : function() {
- if (this.el) return;
- var attrs = {};
- if (this.id) attrs.id = this.id;
- if (this.className) attrs["class"] = this.className;
- this.el = this.make(this.tagName, attrs);
+ if (_.isString(this.el)) {
+ this.el = $(this.el).get(0);
+ }
+
+ if (!this.el) {
+ var attrs = {};
+ if (this.id) attrs.id = this.id;
+ if (this.className) attrs["class"] = this.className;
+ this.el = this.make(this.tagName, attrs);
+ }
}
});
diff --git a/test/view.js b/test/view.js
index 60be9b887..f67df4160 100644
--- a/test/view.js
+++ b/test/view.js
@@ -56,7 +56,7 @@ $(document).ready(function() {
equals(counter2, 3);
});
- test("View: _ensureElement", function() {
+ test("View: _ensureElement with DOM node el", function() {
var ViewClass = Backbone.View.extend({
el: document.body
});
@@ -64,6 +64,28 @@ $(document).ready(function() {
equals(view.el, document.body);
});
+ test("View: _ensureElement with string el", function() {
+ var ViewClass = Backbone.View.extend({
+ el: "body"
+ });
+ var view = new ViewClass;
+ equals(view.el, document.body);
+
+ ViewClass = Backbone.View.extend({
+ el: "body > h2"
+ });
+ view = new ViewClass;
+ equals(view.el, $("#qunit-banner").get(0));
+
+ ViewClass = Backbone.View.extend({
+ el: "#nonexistent"
+ });
+ view = new ViewClass;
+ ok(view.el);
+ equals(view.el.tagName.toLowerCase(), "div");
+ equals(view.el.parentNode, null);
+ });
+
test("View: multiple views per element", function() {
var count = 0, ViewClass = Backbone.View.extend({
el: $("body"),
From 2c29387e70077739e56ae77ce29898adde18026d Mon Sep 17 00:00:00 2001
From: Sam Stephenson
Date: Fri, 17 Dec 2010 12:44:24 -0600
Subject: [PATCH 025/167] Pass along the current value of `this` to Backbone's
closure wrapper
---
backbone.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backbone.js b/backbone.js
index f24ff3042..00f9b9f52 100644
--- a/backbone.js
+++ b/backbone.js
@@ -1034,4 +1034,4 @@
return string.replace(/&(?!\w+;)/g, '&').replace(//g, '>').replace(/"/g, '"');
};
-})();
+}).call(this);
From 12f7ae91370dc4247e0abb3771bf80049835e4a2 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Fri, 17 Dec 2010 13:56:19 -0500
Subject: [PATCH 026/167] Merging Issue #149. View#el can be a string.
---
backbone.js | 6 ++----
index.html | 28 +++++++++++++++++++++-------
test/view.js | 4 +---
3 files changed, 24 insertions(+), 14 deletions(-)
diff --git a/backbone.js b/backbone.js
index a3b288edc..4e7e423bf 100644
--- a/backbone.js
+++ b/backbone.js
@@ -883,15 +883,13 @@
// matching element, and re-assign it to `el`. Otherwise, create
// an element from the `id`, `className` and `tagName` proeprties.
_ensureElement : function() {
- if (_.isString(this.el)) {
- this.el = $(this.el).get(0);
- }
-
if (!this.el) {
var attrs = {};
if (this.id) attrs.id = this.id;
if (this.className) attrs["class"] = this.className;
this.el = this.make(this.tagName, attrs);
+ } else if (_.isString(this.el)) {
+ this.el = $(this.el).get(0);
}
}
diff --git a/index.html b/index.html
index af0341b8f..c8b1d2244 100644
--- a/index.html
+++ b/index.html
@@ -1656,17 +1656,31 @@
Backbone.View
whether they've already been inserted into the page or not. In this
fashion, views can be rendered at any time, and inserted into the DOM all
at once, in order to get high-performance UI rendering with as few
- reflows and repaints as possible.
+ reflows and repaints as possible. this.el is created from the
+ view's tagName, className, and id properties,
+ if specified. If not, el is an empty div.
- this.el is created from the view's tagName, className,
- and id properties, if specified. If not, el is an empty div.
- You may assign el directly in your view's
- initialize function, if the view is being
- created for an element that already exists in the DOM. Make sure to assign
- a real DOM element, and not a jQuery object.
+ You may assign el directly if the view is being
+ created for an element that already exists in the DOM. Use either a
+ reference to a real DOM element, or a css selector string.
- Collections may also listen for changes to specific attributes in their
- models, for example: Documents.bind("change:selected", ...)
+ Any event that is triggered on a model in a collection will also be
+ triggered on the collection directly, for convenience.
+ This allows you to listen for changes to specific attributes in any
+ model in a collection, for example:
+ Documents.bind("change:selected", ...)
From 3098321989a47a2cd78d6aec8ea4840e4a408f50 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Mon, 20 Dec 2010 12:16:34 -0500
Subject: [PATCH 029/167] Issue #143, properly escaping regex characters in
literal routes.
---
backbone.js | 9 ++++++---
test/controller.js | 15 ++++++++++++++-
2 files changed, 20 insertions(+), 4 deletions(-)
diff --git a/backbone.js b/backbone.js
index bcf8c5965..d903c6e8a 100644
--- a/backbone.js
+++ b/backbone.js
@@ -623,8 +623,9 @@
// Cached regular expressions for matching named param parts and splatted
// parts of route strings.
- var namedParam = /:([\w\d]+)/g;
- var splatParam = /\*([\w\d]+)/g;
+ var namedParam = /:([\w\d]+)/g;
+ var splatParam = /\*([\w\d]+)/g;
+ var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
// Set up all inheritable **Backbone.Controller** properties and methods.
_.extend(Backbone.Controller.prototype, Backbone.Events, {
@@ -667,7 +668,9 @@
// Convert a route string into a regular expression, suitable for matching
// against the current location fragment.
_routeToRegExp : function(route) {
- route = route.replace(namedParam, "([^\/]*)").replace(splatParam, "(.*?)");
+ route = route.replace(escapeRegExp, "\\$&")
+ .replace(namedParam, "([^\/]*)")
+ .replace(splatParam, "(.*?)");
return new RegExp('^' + route + '$');
},
diff --git a/test/controller.js b/test/controller.js
index 316806b32..144e1ec4d 100644
--- a/test/controller.js
+++ b/test/controller.js
@@ -8,7 +8,8 @@ $(document).ready(function() {
"search/:query": "search",
"search/:query/p:page": "search",
"splat/*args/end": "splat",
- "*first/complex-:part/*rest": "complex"
+ "*first/complex-:part/*rest": "complex",
+ "query?*args": "query"
},
initialize : function(options) {
@@ -28,6 +29,10 @@ $(document).ready(function() {
this.first = first;
this.part = part;
this.rest = rest;
+ },
+
+ query : function(args) {
+ this.queryArgs = args;
}
});
@@ -74,6 +79,14 @@ $(document).ready(function() {
equals(controller.part, 'part');
equals(controller.rest, 'four/five/six/seven');
start();
+ }, 10);
+ });
+
+ asyncTest("Controller: routes (query)", function() {
+ window.location.hash = 'query?a=b&c=d';
+ setTimeout(function() {
+ equals(controller.queryArgs, 'a=b&c=d');
+ start();
window.location.hash = '';
}, 10);
});
From 4d6d494f2edba46f8daafe1bd6d94c6c6dda5cd6 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Mon, 20 Dec 2010 12:23:52 -0500
Subject: [PATCH 030/167] adding options to the onError direct callback.
---
backbone.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backbone.js b/backbone.js
index d903c6e8a..8067adbca 100644
--- a/backbone.js
+++ b/backbone.js
@@ -1031,7 +1031,7 @@
var wrapError = function(onError, model, options) {
return function(resp) {
if (onError) {
- onError(model, resp);
+ onError(model, resp, options);
} else {
model.trigger('error', model, resp, options);
}
From e3aa5751ac3af91b0890d3b37b39512b0db0bb92 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Mon, 20 Dec 2010 12:31:58 -0500
Subject: [PATCH 031/167] adding a bit more explanation to Model#save onError
---
index.html | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/index.html b/index.html
index eb68b91ba..8b1353dc2 100644
--- a/index.html
+++ b/index.html
@@ -708,10 +708,7 @@
Backbone.Model
method, and validation fails, the model will not be saved. If the model
isNew, the save will be a "create"
(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. The error callback will
- also be invoked if the model has a validate method, and validation fails.
+ exists on the server, the save will be an "update" (HTTP PUT).
@@ -732,6 +729,15 @@
Backbone.Model
book.save();
+
+ save accepts success and error callbacks in the
+ options hash, which are passed (model, response) as arguments.
+ The error callback will also be invoked if the model has a
+ validate method, and validation fails. If a server-side
+ validation fails, return a non-200 HTTP response code, along with
+ an error response in text or JSON.
+
+
destroymodel.destroy([options])
From c0c8cb27637b7084978221a23697b069a94cd9c1 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Mon, 20 Dec 2010 12:34:52 -0500
Subject: [PATCH 032/167] Revising controller test as per dvv's suggestion.
---
test/controller.js | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/test/controller.js b/test/controller.js
index 144e1ec4d..7f43fb6c2 100644
--- a/test/controller.js
+++ b/test/controller.js
@@ -9,7 +9,7 @@ $(document).ready(function() {
"search/:query/p:page": "search",
"splat/*args/end": "splat",
"*first/complex-:part/*rest": "complex",
- "query?*args": "query"
+ ":entity?*args": "query"
},
initialize : function(options) {
@@ -31,7 +31,8 @@ $(document).ready(function() {
this.rest = rest;
},
- query : function(args) {
+ query : function(entity, args) {
+ this.entity = entity;
this.queryArgs = args;
}
@@ -82,9 +83,10 @@ $(document).ready(function() {
}, 10);
});
- asyncTest("Controller: routes (query)", function() {
- window.location.hash = 'query?a=b&c=d';
+ asyncTest("Controller: routes (query)", 2, function() {
+ window.location.hash = 'mandel?a=b&c=d';
setTimeout(function() {
+ equals(controller.entity, 'mandel');
equals(controller.queryArgs, 'a=b&c=d');
start();
window.location.hash = '';
From 331cb8bedea21e61128056608714d7dbd117db86 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Mon, 20 Dec 2010 23:00:51 -0500
Subject: [PATCH 033/167] Allowing Model#defaults to be a function as well as a
hash.
---
backbone.js | 6 +++++-
index.html | 8 ++++----
test/model.js | 11 +++++++++++
3 files changed, 20 insertions(+), 5 deletions(-)
diff --git a/backbone.js b/backbone.js
index 8067adbca..849440082 100644
--- a/backbone.js
+++ b/backbone.js
@@ -115,8 +115,12 @@
// Create a new model, with defined attributes. A client id (`cid`)
// is automatically generated and assigned for you.
Backbone.Model = function(attributes, options) {
+ var defaults;
attributes || (attributes = {});
- if (this.defaults) attributes = _.extend({}, this.defaults, attributes);
+ if (defaults = this.defaults) {
+ if (_.isFunction(defaults)) defaults = defaults();
+ attributes = _.extend({}, defaults, attributes);
+ }
this.attributes = {};
this._escapedAttributes = {};
this.cid = _.uniqueId('c');
diff --git a/index.html b/index.html
index 8b1353dc2..9316a3dfb 100644
--- a/index.html
+++ b/index.html
@@ -642,11 +642,11 @@
Backbone.Model
- defaultsmodel.defaults
+ defaultsmodel.defaults or model.defaults()
- The defaults hash can be used to specify the default attributes
- for your model. When creating an instance of the model, any unspecified
- attributes will be set to their default value.
+ The defaults hash (or function) can be used to specify the default
+ attributes for your model. When creating an instance of the model,
+ any unspecified attributes will be set to their default value.
diff --git a/test/model.js b/test/model.js
index b90f77d54..5a313dc9a 100644
--- a/test/model.js
+++ b/test/model.js
@@ -172,6 +172,17 @@ $(document).ready(function() {
var model = new Defaulted({two: null});
equals(model.get('one'), 1);
equals(model.get('two'), null);
+ Defaulted = Backbone.Model.extend({
+ defaults: function() {
+ return {
+ "one": 3,
+ "two": 4
+ };
+ }
+ });
+ var model = new Defaulted({two: null});
+ equals(model.get('one'), 3);
+ equals(model.get('two'), null);
});
test("Model: change, hasChanged, changedAttributes, previous, previousAttributes", function() {
From 67dd2ee8b3f94aaf85aeb3fa5478e505f632e96f Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Tue, 21 Dec 2010 12:11:39 -0500
Subject: [PATCH 034/167] prefer single quotes.
---
backbone.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/backbone.js b/backbone.js
index 849440082..01801e17b 100644
--- a/backbone.js
+++ b/backbone.js
@@ -23,7 +23,7 @@
// Require Underscore, if we're on the server, and it's not already present.
var _ = this._;
- if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._;
+ if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._;
// For Backbone's purposes, either jQuery or Zepto owns the `$` variable.
var $ = this.jQuery || this.Zepto;
@@ -893,7 +893,7 @@
if (!this.el) {
var attrs = {};
if (this.id) attrs.id = this.id;
- if (this.className) attrs["class"] = this.className;
+ if (this.className) attrs['class'] = this.className;
this.el = this.make(this.tagName, attrs);
} else if (_.isString(this.el)) {
this.el = $(this.el).get(0);
@@ -968,7 +968,7 @@
if (Backbone.emulateJSON) params.data._method = type;
params.type = 'POST';
params.beforeSend = function(xhr) {
- xhr.setRequestHeader("X-HTTP-Method-Override", type);
+ xhr.setRequestHeader('X-HTTP-Method-Override', type);
};
}
}
From a0ace0d9d6a7b5c0221322c6198d0d3dd13d4fbf Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Tue, 21 Dec 2010 12:15:56 -0500
Subject: [PATCH 035/167] Taking some of dvv's suggestions.
---
backbone.js | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/backbone.js b/backbone.js
index 01801e17b..797dac118 100644
--- a/backbone.js
+++ b/backbone.js
@@ -946,7 +946,7 @@
// Default JSON-request options.
var params = _.extend({
- url: getUrl(model) || urlError(),
+ url: getUrl(model),
type: type,
contentType: 'application/json',
data: modelJSON,
@@ -954,11 +954,14 @@
processData: false
}, options);
+ // Ensure that we have a URL.
+ if (!params.url) urlError();
+
// For older servers, emulate JSON by encoding the request into an HTML-form.
if (Backbone.emulateJSON) {
params.contentType = 'application/x-www-form-urlencoded';
params.processData = true;
- params.data = modelJSON ? {model : modelJSON} : {};
+ params.data = params.data ? {model : params.data} : {};
}
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
From 65e434096d81f89cfad923d389956fd6f198f0cd Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Wed, 22 Dec 2010 16:44:24 -0800
Subject: [PATCH 036/167] Updating Model-save documentation.
---
index.html | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/index.html b/index.html
index 9316a3dfb..adf5b6af5 100644
--- a/index.html
+++ b/index.html
@@ -701,10 +701,13 @@
Backbone.Model
- savemodel.save(attributes, [options])
+ savemodel.save([attributes], [options])
Save a model to your database (or alternative persistence layer),
- by delegating to Backbone.sync. If the model has a validate
+ by delegating to Backbone.sync. The attributes
+ hash (as in set) should contain the attributes
+ you'd like to change -- keys that aren't mentioned won't be altered.
+ If the model has a validate
method, and validation fails, the model will not be saved. If the model
isNew, the save will be a "create"
(HTTP POST), if the model already
@@ -737,6 +740,10 @@
Backbone.Model
validation fails, return a non-200 HTTP response code, along with
an error response in text or JSON.