From 7afd43c0ef110a9aafdcefb12453daf8f7dfde77 Mon Sep 17 00:00:00 2001
From: Damien Holzapfel
Date: Wed, 27 Apr 2011 10:38:13 -0700
Subject: [PATCH 01/19] Reference calls variable in list variable definition in
Backbone.Events.bind
---
backbone.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backbone.js b/backbone.js
index bec4de009..3045a8c45 100644
--- a/backbone.js
+++ b/backbone.js
@@ -70,7 +70,7 @@
// 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] = []);
+ var list = calls[ev] || (calls[ev] = []);
list.push(callback);
return this;
},
From 9d5308b1ab76f8cce34d945a9afb255e720da977 Mon Sep 17 00:00:00 2001
From: Corban Brook
Date: Thu, 5 May 2011 15:15:16 -0400
Subject: [PATCH 02/19] Pass options to the coll.add call in Collection::create
so that it can work silently.
---
backbone.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backbone.js b/backbone.js
index 7ec5d484e..11bfea78a 100644
--- a/backbone.js
+++ b/backbone.js
@@ -536,7 +536,7 @@
}
var success = options.success;
options.success = function(nextModel, resp, xhr) {
- coll.add(nextModel);
+ coll.add(nextModel, options);
if (success) success(nextModel, resp, xhr);
};
model.save(null, options);
From 82c288c91e9c3781e3afeedf1aa30626e6f9a788 Mon Sep 17 00:00:00 2001
From: Francis
Date: Sat, 7 May 2011 22:47:35 -0700
Subject: [PATCH 03/19] Following recommendations from the OWASP
https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet
Should be tested with
http://ha.ckers.org/xss.html
Make sure your pages are utf8!
---
backbone.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backbone.js b/backbone.js
index 7ec5d484e..795b48b33 100644
--- a/backbone.js
+++ b/backbone.js
@@ -1094,7 +1094,7 @@
// Helper function to escape a string for HTML rendering.
var escapeHTML = function(string) {
- return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&').replace(//g, '>').replace(/"/g, '"');
+ return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/');
};
}).call(this);
From cf3215139acc86a54e94f039a0dbf627b49a5e14 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Thu, 26 May 2011 10:54:28 -0400
Subject: [PATCH 04/19] initial draft of a 0.5.0 branch
---
backbone.js | 140 ++++++++++++++++++++----------
examples/todos/todos.js | 2 +-
index.html | 44 ++++++----
package.json | 2 +-
test/collection.js | 32 ++++---
test/{controller.js => router.js} | 53 +++++------
test/test.html | 2 +-
7 files changed, 170 insertions(+), 105 deletions(-)
rename test/{controller.js => router.js} (58%)
diff --git a/backbone.js b/backbone.js
index f769caa79..706c481e0 100644
--- a/backbone.js
+++ b/backbone.js
@@ -1,4 +1,4 @@
-// Backbone.js 0.3.3
+// Backbone.js 0.5.0-pre
// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
@@ -25,7 +25,7 @@
}
// Current version of the library. Keep in sync with `package.json`.
- Backbone.VERSION = '0.3.3';
+ Backbone.VERSION = '0.5.0-pre';
// Require Underscore, if we're on the server, and it's not already present.
var _ = root._;
@@ -416,7 +416,7 @@
}
_.bindAll(this, '_onModelEvent', '_removeReference');
this._reset();
- if (models) this.refresh(models, {silent: true});
+ if (models) this.reset(models, {silent: true});
this.initialize(models, options);
};
@@ -485,7 +485,7 @@
options || (options = {});
if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
this.models = this.sortBy(this.comparator);
- if (!options.silent) this.trigger('refresh', this, options);
+ if (!options.silent) this.trigger('reset', this, options);
return this;
},
@@ -497,13 +497,13 @@
// When you have more items than you want to add or remove individually,
// you can refresh the entire set with a new list of models, without firing
// any `added` or `removed` events. Fires `refresh` when finished.
- refresh : function(models, options) {
+ reset : function(models, options) {
models || (models = []);
options || (options = {});
this.each(this._removeReference);
this._reset();
this.add(models, {silent: true});
- if (!options.silent) this.trigger('refresh', this, options);
+ if (!options.silent) this.trigger('reset', this, options);
return this;
},
@@ -515,7 +515,7 @@
var collection = this;
var success = options.success;
options.success = function(resp, status, xhr) {
- collection[options.add ? 'add' : 'refresh'](collection.parse(resp, xhr), options);
+ collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
if (success) success(collection, resp);
};
options.error = wrapError(options.error, collection, options);
@@ -578,7 +578,8 @@
if (!model.collection) {
model.collection = this;
}
- var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length;
+ var index = this.comparator ? this.sortedIndex(model, this.comparator) :
+ options.at != null ? options.at : this.length;
this.models.splice(index, 0, model);
model.bind('all', this._onModelEvent);
this.length++;
@@ -640,12 +641,12 @@
};
});
- // Backbone.Controller
+ // Backbone.Router
// -------------------
- // Controllers map faux-URLs to actions, and fire events when routes are
+ // Routers map faux-URLs to actions, and fire events when routes are
// matched. Creating a new one sets its `routes` hash, if not set statically.
- Backbone.Controller = function(options) {
+ Backbone.Router = function(options) {
options || (options = {});
if (options.routes) this.routes = options.routes;
this._bindRoutes();
@@ -658,8 +659,8 @@
var splatParam = /\*([\w\d]+)/g;
var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
- // Set up all inheritable **Backbone.Controller** properties and methods.
- _.extend(Backbone.Controller.prototype, Backbone.Events, {
+ // Set up all inheritable **Backbone.Router** properties and methods.
+ _.extend(Backbone.Router.prototype, Backbone.Events, {
// Initialize is an empty function by default. Override it with your own
// initialization logic.
@@ -674,8 +675,8 @@
route : function(route, name, callback) {
Backbone.history || (Backbone.history = new Backbone.History);
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
- Backbone.history.route(route, _.bind(function(hash) {
- var args = this._extractParameters(route, hash);
+ Backbone.history.route(route, _.bind(function(fragment) {
+ var args = this._extractParameters(route, fragment);
callback.apply(this, args);
this.trigger.apply(this, ['route:' + name].concat(args));
}, this));
@@ -683,8 +684,16 @@
// Simple proxy to `Backbone.history` to save a fragment into the history,
// without triggering routes.
- saveLocation : function(hash) {
- Backbone.history.saveLocation(hash);
+ saveLocation : function(fragment) {
+ Backbone.history.saveLocation(fragment);
+ },
+
+ // Simple proxy to `Backbone.history` to both save a fragment into the
+ // history and to then load the route at that fragment. Used in place
+ // of settings `window.location.hash` when using `window.history.pushState`.
+ loadUrl : function(fragment) {
+ Backbone.history.saveLocation(fragment);
+ Backbone.history.loadUrl();
},
// Bind all defined routes to `Backbone.history`. We have to reverse the
@@ -712,8 +721,8 @@
// Given a route, and a URL fragment that it matches, return the array of
// extracted parameters.
- _extractParameters : function(route, hash) {
- return route.exec(hash).slice(1);
+ _extractParameters : function(route, fragment) {
+ return route.exec(fragment).slice(1);
}
});
@@ -721,7 +730,7 @@
// Backbone.History
// ----------------
- // Handles cross-browser history management, based on URL hashes. If the
+ // Handles cross-browser history management, based on URL fragments. If the
// browser does not support `onhashchange`, falls back to polling.
Backbone.History = function() {
this.handlers = [];
@@ -745,32 +754,54 @@
interval: 50,
// Get the cross-browser normalized URL fragment.
- getHash : function(loc) {
- return (loc || window.location).hash.replace(hashStrip, '');
+ getFragment : function(fragment, forcePushState) {
+ if (!fragment) {
+ if (this._hasPushState || forcePushState) {
+ fragment = window.location.pathname;
+ var search = window.location.search;
+ if (search) fragment += search;
+ if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length);
+ } else {
+ fragment = window.location.hash;
+ }
+ }
+ return fragment.replace(hashStrip, '');
},
// Start the hash change handling, returning `true` if the current URL matches
// an existing route, and `false` otherwise.
- start : function() {
+ start : function(options) {
if (historyStarted) throw new Error("Backbone.history has already been started");
- var hash = this.getHash();
+ this.options = _.extend({}, {root: '/'}, options);
+ this._wantsPushState = !!this.options.pushState;
+ this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
+ var fragment = this.getFragment();
var docMode = document.documentMode;
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
if (oldIE) {
this.iframe = $('').hide().appendTo('body')[0].contentWindow;
- this.saveLocation(hash);
+ this.saveLocation(fragment);
}
- if ('onhashchange' in window && !oldIE) {
+ if (this._hasPushState) {
+ $(window).bind('popstate', this.checkUrl);
+ } else if ('onhashchange' in window && !oldIE) {
$(window).bind('hashchange', this.checkUrl);
} else {
setInterval(this.checkUrl, this.interval);
}
- this.hash = hash;
+ this.fragment = fragment;
historyStarted = true;
- return this.loadUrl();
+ var started = this.loadUrl() || this.loadUrl(window.location.hash);
+
+ if (this._wantsPushState && !this._hasPushState && window.location.pathname != this.options.root) {
+ this.fragment = this.getFragment(null, true);
+ window.location.href = this.options.root + '#' + this.fragment;
+ } else {
+ return started;
+ }
},
- // Add a route to be tested when the hash changes. Routes added later may
+ // Add a route to be tested when the fragment changes. Routes added later may
// override previous routes.
route : function(route, callback) {
this.handlers.unshift({route : route, callback : callback});
@@ -778,23 +809,28 @@
// Checks the current URL to see if it has changed, and if it has,
// calls `loadUrl`, normalizing across the hidden iframe.
- checkUrl : function() {
- var hash = this.getHash();
- if (hash == this.hash && this.iframe) hash = this.getHash(this.iframe.location);
- if (hash == this.hash || hash == decodeURIComponent(this.hash)) return false;
- if (this.iframe) this.saveLocation(hash);
- this.hash = hash;
- this.loadUrl();
+ checkUrl : function(e) {
+ var current = this.getFragment();
+ if (current == this.fragment && this.iframe) {
+ current = this.getFragment(this.iframe.location);
+ }
+ if (current == this.fragment ||
+ current == decodeURIComponent(this.fragment)) return false;
+
+ if (this.iframe) {
+ window.location.hash = this.iframe.location.hash = current;
+ }
+ this.loadUrl() || this.loadUrl(window.location.hash);
},
// Attempt to load the current URL fragment. If a route succeeds with a
// match, returns `true`. If no defined routes matches the fragment,
// returns `false`.
- loadUrl : function() {
- var hash = this.hash;
+ loadUrl : function(fragment) {
+ fragment = this.fragment = this.getFragment(fragment);
var matched = _.any(this.handlers, function(handler) {
- if (handler.route.test(hash)) {
- handler.callback(hash);
+ if (handler.route.test(fragment)) {
+ handler.callback(fragment);
return true;
}
});
@@ -804,13 +840,21 @@
// Save a fragment into the hash history. You are responsible for properly
// URL-encoding the fragment in advance. This does not trigger
// a `hashchange` event.
- saveLocation : function(hash) {
- hash = (hash || '').replace(hashStrip, '');
- if (this.hash == hash) return;
- window.location.hash = this.hash = hash;
- if (this.iframe && (hash != this.getHash(this.iframe.location))) {
- this.iframe.document.open().close();
- this.iframe.location.hash = hash;
+ saveLocation : function(fragment) {
+ fragment = (fragment || '').replace(hashStrip, '');
+ if (this.fragment == fragment || this.fragment == decodeURIComponent(fragment)) return;
+
+ if (this._hasPushState) {
+ var loc = window.location;
+ if (fragment.indexOf(this.options.root) != 0) fragment = this.options.root + fragment;
+ this.fragment = fragment;
+ window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + fragment);
+ } else {
+ window.location.hash = this.fragment = fragment;
+ if (this.iframe && (fragment != this.getFragment(this.iframe.location))) {
+ this.iframe.document.open().close();
+ this.iframe.location.hash = fragment;
+ }
}
}
@@ -950,7 +994,7 @@
// Set up inheritance for the model, collection, and view.
Backbone.Model.extend = Backbone.Collection.extend =
- Backbone.Controller.extend = Backbone.View.extend = extend;
+ Backbone.Router.extend = Backbone.View.extend = extend;
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
var methodMap = {
diff --git a/examples/todos/todos.js b/examples/todos/todos.js
index 3e2ef6b4d..1c7c8486a 100644
--- a/examples/todos/todos.js
+++ b/examples/todos/todos.js
@@ -187,7 +187,7 @@ $(function(){
this.input = this.$("#new-todo");
Todos.bind('add', this.addOne);
- Todos.bind('refresh', this.addAll);
+ Todos.bind('reset', this.addAll);
Todos.bind('all', this.render);
Todos.fetch();
diff --git a/index.html b/index.html
index 0f5b24b46..8581d4739 100644
--- a/index.html
+++ b/index.html
@@ -220,7 +220,7 @@
– url
– parse
– fetch
- – refresh
+ – reset
– create
@@ -1027,7 +1027,7 @@ Backbone.Collection
Override this property to specify the model class that the collection
contains. If defined, you can pass raw attributes objects (and arrays) to
add, create,
- and refresh, and the attributes will be
+ and reset, and the attributes will be
converted into a model of the proper type.
@@ -1140,6 +1140,8 @@ Backbone.Collection
event, which you can pass {silent: true} to suppress. If a
model property is defined, you may also pass
raw attributes objects, and have them be vivified as instances of the model.
+ Pass {at: index} to splice the model into the collection at the
+ specified index.
@@ -1240,7 +1242,7 @@ Backbone.Collection
Force a collection to re-sort itself. You don't need to call this under
normal circumstances, as a collection with a comparator function
will maintain itself in proper sort order at all times. Calling sort
- triggers the collection's "refresh" event, unless silenced by passing
+ triggers the collection's "reset" event, unless silenced by passing
{silent: true}
@@ -1314,7 +1316,7 @@ Backbone.Collection
success and error
callbacks which will be passed (collection, response) as arguments.
When the model data returns from the server, the collection will
- refresh.
+ reset.
Delegates to Backbone.sync
under the covers, for custom persistence strategies.
The server handler for fetch requests should return a JSON array of
@@ -1347,24 +1349,25 @@ Backbone.Collection
toggled open and closed.
-
- collection.refresh(models, [options])
+
+ collection.reset(models, [options])
Adding and removing models one at a time is all well and good, but sometimes
you have so many models to change that you'd rather just update the collection
- in bulk. Use refresh to replace a collection with a new list
- of models (or attribute hashes), triggering a single "refresh" event
- at the end. Pass {silent: true} to suppress the "refresh" event.
+ in bulk. Use reset to replace a collection with a new list
+ of models (or attribute hashes), triggering a single "reset" event
+ at the end. Pass {silent: true} to suppress the "reset" event.
+ Using reset with no arguments is useful as a way to empty the collection.
- Here's an example using refresh to bootstrap a collection during initial page load,
+ Here's an example using reset to bootstrap a collection during initial page load,
in a Rails application.
<script>
- Accounts.refresh(<%= @accounts.to_json %>);
+ Accounts.reset(<%= @accounts.to_json %>);
</script>
@@ -2145,7 +2148,7 @@ F.A.Q.
- "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.
+ - "reset" (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.
- "destrooy" (model, collection) — when a model is destroyed.
@@ -2171,7 +2174,7 @@ F.A.Q.
initialize: function() {
this.messages = new Messages;
this.messages.url = '/mailbox/' + this.id + '/messages';
- this.messages.bind("refresh", this.updateCounts);
+ this.messages.bind("reset", this.updateCounts);
},
...
@@ -2192,7 +2195,7 @@ F.A.Q.
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
+ You can then use reset to populate your
collections with the initial data. At DocumentCloud, in the
ERB template for the
workspace, we do something along these lines:
@@ -2200,8 +2203,8 @@ F.A.Q.
<script>
- Accounts.refresh(<%= @accounts.to_json %>);
- Projects.refresh(<%= @projects.to_json(:collaborators => true) %>);
+ Accounts.reset(<%= @accounts.to_json %>);
+ Projects.reset(<%= @projects.to_json(:collaborators => true) %>);
</script>
@@ -2270,7 +2273,7 @@ F.A.Q.
_.bindAll(this, "addMessage", "removeMessage", "render");
var messages = this.collection;
- messages.bind("refresh", this.render);
+ messages.bind("reset", this.render);
messages.bind("add", this.addMessage);
messages.bind("remove", this.removeMessage);
}
@@ -2304,6 +2307,13 @@ F.A.Q.
Change Log
+
+ — FUTURE DATE, 2011
+ Collection.refresh renamed to Collection.reset to emphasize
+ its ability to both refresh the collection with new models, as well as empty
+ out the collection when used with no parameters.
+
+
— Dec 1, 2010
Backbone.js now supports Zepto, alongside
diff --git a/package.json b/package.json
index da66d8777..071d838f4 100644
--- a/package.json
+++ b/package.json
@@ -10,5 +10,5 @@
},
"lib" : ".",
"main" : "backbone.js",
- "version" : "0.3.3"
+ "version" : "0.5.0-pre"
}
diff --git a/test/collection.js b/test/collection.js
index 1d679af77..7e9d77088 100644
--- a/test/collection.js
+++ b/test/collection.js
@@ -83,6 +83,16 @@ $(document).ready(function() {
equals(otherCol.length, 1);
equals(secondAdded, null);
ok(opts.amazing);
+
+ var f = new Backbone.Model({id: 20, label : 'f'});
+ var g = new Backbone.Model({id: 21, label : 'g'});
+ var h = new Backbone.Model({id: 22, label : 'h'});
+ var atCol = new Backbone.Collection([f, g, h]);
+ equals(atCol.length, 3);
+ atCol.add(e, {at: 1});
+ equals(atCol.length, 4);
+ equals(atCol.at(1), e);
+ equals(atCol.last(), h);
});
test("Collection: add model to multiple collections", function() {
@@ -131,7 +141,7 @@ $(document).ready(function() {
emcees.bind('change', function(){ counter++; });
dj.set({name : 'Kool'});
equals(counter, 1);
- emcees.refresh([]);
+ emcees.reset([]);
equals(dj.collection, undefined);
dj.set({name : 'Shadow'});
equals(counter, 1);
@@ -283,20 +293,20 @@ $(document).ready(function() {
[0, 4]);
});
- test("Collection: refresh", function() {
- var refreshed = 0;
+ test("Collection: reset", function() {
+ var resetCount = 0;
var models = col.models;
- col.bind('refresh', function() { refreshed += 1; });
- col.refresh([]);
- equals(refreshed, 1);
+ col.bind('reset', function() { resetCount += 1; });
+ col.reset([]);
+ equals(resetCount, 1);
equals(col.length, 0);
equals(col.last(), null);
- col.refresh(models);
- equals(refreshed, 2);
+ col.reset(models);
+ equals(resetCount, 2);
equals(col.length, 4);
equals(col.last(), a);
- col.refresh(_.map(models, function(m){ return m.attributes; }));
- equals(refreshed, 3);
+ col.reset(_.map(models, function(m){ return m.attributes; }));
+ equals(resetCount, 3);
equals(col.length, 4);
ok(col.last() !== a);
ok(_.isEqual(col.last().attributes, a.attributes));
@@ -304,7 +314,7 @@ $(document).ready(function() {
test("Collection: trigger custom events on models", function() {
var fired = null;
- a.bind("custom", function() { fired = true });
+ a.bind("custom", function() { fired = true; });
a.trigger("custom");
equals(fired, true);
});
diff --git a/test/controller.js b/test/router.js
similarity index 58%
rename from test/controller.js
rename to test/router.js
index 2c02953a7..170daa7ea 100644
--- a/test/controller.js
+++ b/test/router.js
@@ -1,8 +1,8 @@
$(document).ready(function() {
- module("Backbone.Controller");
+ module("Backbone.Router");
- var Controller = Backbone.Controller.extend({
+ var Router = Backbone.Router.extend({
routes: {
"search/:query": "search",
@@ -43,74 +43,75 @@ $(document).ready(function() {
});
- var controller = new Controller({testing: 101});
+ Backbone.history = null;
+ var router = new Router({testing: 101});
Backbone.history.interval = 9;
- Backbone.history.start();
+ Backbone.history.start({pushState: false});
- test("Controller: initialize", function() {
- equals(controller.testing, 101);
+ test("Router: initialize", function() {
+ equals(router.testing, 101);
});
- asyncTest("Controller: routes (simple)", 2, function() {
+ asyncTest("Router: routes (simple)", 2, function() {
window.location.hash = 'search/news';
setTimeout(function() {
- equals(controller.query, 'news');
- equals(controller.page, undefined);
+ equals(router.query, 'news');
+ equals(router.page, undefined);
start();
}, 10);
});
- asyncTest("Controller: routes (two part)", 2, function() {
+ asyncTest("Router: routes (two part)", 2, function() {
window.location.hash = 'search/nyc/p10';
setTimeout(function() {
- equals(controller.query, 'nyc');
- equals(controller.page, '10');
+ equals(router.query, 'nyc');
+ equals(router.page, '10');
start();
}, 10);
});
- asyncTest("Controller: routes (splats)", function() {
+ asyncTest("Router: routes (splats)", function() {
window.location.hash = 'splat/long-list/of/splatted_99args/end';
setTimeout(function() {
- equals(controller.args, 'long-list/of/splatted_99args');
+ equals(router.args, 'long-list/of/splatted_99args');
start();
}, 10);
});
- asyncTest("Controller: routes (complex)", 3, function() {
+ asyncTest("Router: routes (complex)", 3, function() {
window.location.hash = 'one/two/three/complex-part/four/five/six/seven';
setTimeout(function() {
- equals(controller.first, 'one/two/three');
- equals(controller.part, 'part');
- equals(controller.rest, 'four/five/six/seven');
+ equals(router.first, 'one/two/three');
+ equals(router.part, 'part');
+ equals(router.rest, 'four/five/six/seven');
start();
}, 10);
});
- asyncTest("Controller: routes (query)", 2, function() {
+ asyncTest("Router: 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');
+ equals(router.entity, 'mandel');
+ equals(router.queryArgs, 'a=b&c=d');
start();
}, 10);
});
- asyncTest("Controller: routes (anything)", 1, function() {
+ asyncTest("Router: routes (anything)", 1, function() {
window.location.hash = 'doesnt-match-a-route';
setTimeout(function() {
- equals(controller.anything, 'doesnt-match-a-route');
+ equals(router.anything, 'doesnt-match-a-route');
start();
window.location.hash = '';
}, 10);
});
- asyncTest("Controller: routes (hashbang)", 2, function() {
+ asyncTest("Router: routes (hashbang)", 2, function() {
window.location.hash = '!search/news';
setTimeout(function() {
- equals(controller.query, 'news');
- equals(controller.page, undefined);
+ equals(router.query, 'news');
+ equals(router.page, undefined);
start();
}, 10);
});
diff --git a/test/test.html b/test/test.html
index 091d1073a..1d272c4a1 100644
--- a/test/test.html
+++ b/test/test.html
@@ -14,7 +14,7 @@
-
+
From 300c7f0e9c81c16448b6b301caadefe9fd3780a8 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Thu, 26 May 2011 11:04:56 -0400
Subject: [PATCH 05/19] fix the zepto test suite.
---
test/test-zepto.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/test-zepto.html b/test/test-zepto.html
index 2b090e5a4..a03e65c5e 100644
--- a/test/test-zepto.html
+++ b/test/test-zepto.html
@@ -13,7 +13,7 @@
-
+
From 636e7b00a9f9dc3b6829a8e8593114c921639060 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Thu, 26 May 2011 11:54:35 -0400
Subject: [PATCH 06/19] remove an extraneous this.options
---
backbone.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backbone.js b/backbone.js
index 706c481e0..6cfffd31a 100644
--- a/backbone.js
+++ b/backbone.js
@@ -772,7 +772,7 @@
// an existing route, and `false` otherwise.
start : function(options) {
if (historyStarted) throw new Error("Backbone.history has already been started");
- this.options = _.extend({}, {root: '/'}, options);
+ this.options = _.extend({}, {root: '/'}, this.options, options);
this._wantsPushState = !!this.options.pushState;
this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
var fragment = this.getFragment();
From e5ee5503fd388b7fd6efccef829545e0af7b047b Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Thu, 26 May 2011 12:02:12 -0400
Subject: [PATCH 07/19] Adding Backbone.Router.setLocation, alongside
saveLocation, to both save and route at once.
---
backbone.js | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/backbone.js b/backbone.js
index 6cfffd31a..ea8095f7d 100644
--- a/backbone.js
+++ b/backbone.js
@@ -578,8 +578,9 @@
if (!model.collection) {
model.collection = this;
}
- var index = this.comparator ? this.sortedIndex(model, this.comparator) :
- options.at != null ? options.at : this.length;
+ var index = options.at != null ? options.at :
+ this.comparator ? this.sortedIndex(model, this.comparator) :
+ this.length;
this.models.splice(index, 0, model);
model.bind('all', this._onModelEvent);
this.length++;
@@ -689,9 +690,8 @@
},
// Simple proxy to `Backbone.history` to both save a fragment into the
- // history and to then load the route at that fragment. Used in place
- // of settings `window.location.hash` when using `window.history.pushState`.
- loadUrl : function(fragment) {
+ // history and to then load the route at that fragment.
+ setLocation : function(fragment) {
Backbone.history.saveLocation(fragment);
Backbone.history.loadUrl();
},
From e9b38de419affc065ad3f180c882a5e517370fa2 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Thu, 26 May 2011 12:56:08 -0400
Subject: [PATCH 08/19] Backbone.Router formatting, tweaks.
---
backbone.js | 42 ++++++++++++++++++++++--------------------
1 file changed, 22 insertions(+), 20 deletions(-)
diff --git a/backbone.js b/backbone.js
index ea8095f7d..9873110f5 100644
--- a/backbone.js
+++ b/backbone.js
@@ -693,7 +693,7 @@
// history and to then load the route at that fragment.
setLocation : function(fragment) {
Backbone.history.saveLocation(fragment);
- Backbone.history.loadUrl();
+ Backbone.history.loadUrl(fragment);
},
// Bind all defined routes to `Backbone.history`. We have to reverse the
@@ -753,7 +753,8 @@
// twenty times a second.
interval: 50,
- // Get the cross-browser normalized URL fragment.
+ // Get the cross-browser normalized URL fragment, either from the URL,
+ // the hash, or the override.
getFragment : function(fragment, forcePushState) {
if (!fragment) {
if (this._hasPushState || forcePushState) {
@@ -771,17 +772,23 @@
// Start the hash change handling, returning `true` if the current URL matches
// an existing route, and `false` otherwise.
start : function(options) {
+
+ // Figure out the initial configuration. Do we need an iframe?
+ // Is pushState desired ... is it available?
if (historyStarted) throw new Error("Backbone.history has already been started");
this.options = _.extend({}, {root: '/'}, this.options, options);
this._wantsPushState = !!this.options.pushState;
this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
var fragment = this.getFragment();
- var docMode = document.documentMode;
- var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
+ var docMode = document.documentMode;
+ var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
if (oldIE) {
this.iframe = $('').hide().appendTo('body')[0].contentWindow;
this.saveLocation(fragment);
}
+
+ // Depending on whether we're using pushState or hashes, and whether
+ // 'onhashchange' is supported, determine how we check the URL state.
if (this._hasPushState) {
$(window).bind('popstate', this.checkUrl);
} else if ('onhashchange' in window && !oldIE) {
@@ -789,13 +796,15 @@
} else {
setInterval(this.checkUrl, this.interval);
}
+
+ // Determine if we need to change the base url, for a pushState link
+ // opened by a non-pushState browser.
this.fragment = fragment;
historyStarted = true;
var started = this.loadUrl() || this.loadUrl(window.location.hash);
-
if (this._wantsPushState && !this._hasPushState && window.location.pathname != this.options.root) {
this.fragment = this.getFragment(null, true);
- window.location.href = this.options.root + '#' + this.fragment;
+ window.location = this.options.root + '#' + this.fragment;
} else {
return started;
}
@@ -811,23 +820,17 @@
// calls `loadUrl`, normalizing across the hidden iframe.
checkUrl : function(e) {
var current = this.getFragment();
- if (current == this.fragment && this.iframe) {
- current = this.getFragment(this.iframe.location);
- }
- if (current == this.fragment ||
- current == decodeURIComponent(this.fragment)) return false;
-
- if (this.iframe) {
- window.location.hash = this.iframe.location.hash = current;
- }
+ if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location);
+ if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
+ if (this.iframe) this.saveLocation(current);
this.loadUrl() || this.loadUrl(window.location.hash);
},
// Attempt to load the current URL fragment. If a route succeeds with a
// match, returns `true`. If no defined routes matches the fragment,
// returns `false`.
- loadUrl : function(fragment) {
- fragment = this.fragment = this.getFragment(fragment);
+ loadUrl : function(fragmentOverride) {
+ var fragment = this.fragment = this.getFragment(fragmentOverride);
var matched = _.any(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment);
@@ -842,8 +845,7 @@
// a `hashchange` event.
saveLocation : function(fragment) {
fragment = (fragment || '').replace(hashStrip, '');
- if (this.fragment == fragment || this.fragment == decodeURIComponent(fragment)) return;
-
+ if (this.fragment == fragment) return;
if (this._hasPushState) {
var loc = window.location;
if (fragment.indexOf(this.options.root) != 0) fragment = this.options.root + fragment;
@@ -851,7 +853,7 @@
window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + fragment);
} else {
window.location.hash = this.fragment = fragment;
- if (this.iframe && (fragment != this.getFragment(this.iframe.location))) {
+ if (this.iframe && (fragment != this.getFragment(this.iframe.location.hash))) {
this.iframe.document.open().close();
this.iframe.location.hash = fragment;
}
From f742bbe9478f94355ec12a7f3e13ccf0c75574b1 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Thu, 26 May 2011 13:25:06 -0400
Subject: [PATCH 09/19] fixing IE support for 0.5.0
---
backbone.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/backbone.js b/backbone.js
index 9873110f5..570173d88 100644
--- a/backbone.js
+++ b/backbone.js
@@ -756,7 +756,7 @@
// Get the cross-browser normalized URL fragment, either from the URL,
// the hash, or the override.
getFragment : function(fragment, forcePushState) {
- if (!fragment) {
+ if (fragment == null) {
if (this._hasPushState || forcePushState) {
fragment = window.location.pathname;
var search = window.location.search;
@@ -820,7 +820,7 @@
// calls `loadUrl`, normalizing across the hidden iframe.
checkUrl : function(e) {
var current = this.getFragment();
- if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location);
+ if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
if (this.iframe) this.saveLocation(current);
this.loadUrl() || this.loadUrl(window.location.hash);
From e56346c7aec83bdacf8900c59663aec9411b0006 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Thu, 26 May 2011 13:30:29 -0400
Subject: [PATCH 10/19] normalizing for the Safari window.location bug.
---
backbone.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backbone.js b/backbone.js
index 570173d88..5cef2cecd 100644
--- a/backbone.js
+++ b/backbone.js
@@ -845,7 +845,7 @@
// a `hashchange` event.
saveLocation : function(fragment) {
fragment = (fragment || '').replace(hashStrip, '');
- if (this.fragment == fragment) return;
+ if (this.fragment == fragment || this.fragment == decodeURIComponent(fragment)) return;
if (this._hasPushState) {
var loc = window.location;
if (fragment.indexOf(this.options.root) != 0) fragment = this.options.root + fragment;
From 46fbd4cd7f42e51bfc1c17c8fe4d502969ddac94 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Thu, 26 May 2011 13:38:30 -0400
Subject: [PATCH 11/19] Controller -> Router in the docs, and adding docco for
setLocation().
---
index.html | 102 ++++++++++++++++++++++++++++-------------------------
1 file changed, 54 insertions(+), 48 deletions(-)
diff --git a/index.html b/index.html
index 8581d4739..acecf7776 100644
--- a/index.html
+++ b/index.html
@@ -224,15 +224,16 @@
– create
-
- Controller
+
+ Router
@@ -1398,39 +1399,32 @@ Backbone.Collection
});
- Backbone.Controller
+ Backbone.Router
Web applications often choose to change their URL fragment (#fragment)
in order to provide shareable, bookmarkable URLs for an Ajax-heavy application.
- Backbone.Controller provides methods for routing client-side URL
+ Backbone.Router provides methods for routing client-side URL
fragments, and connecting them to actions and events.
-
- Backbone controllers do not yet make use of HTML5 pushState and
- replaceState. Currently, pushState and replaceState
- need special handling on the server-side, cause you to mint duplicate URLs,
- and have an incomplete API. We may start supporting them in the future
- when these issues have been resolved.
-
-
- During page load, after your application has finished creating all of its controllers,
- be sure to call Backbone.history.start() to route the initial URL.
+ During page load, after your application has finished creating all of its routers,
+ be sure to call Backbone.history.start(), or
+ Backbone.history.start({pushState: true}) to route the initial URL.
-
- Backbone.Controller.extend(properties, [classProperties])
+
+ Backbone.Router.extend(properties, [classProperties])
- Get started by creating a custom controller class. You'll
+ Get started by creating a custom router class. You'll
want to define actions that are triggered when certain URL fragments are
- matched, and provide a routes hash
+ matched, and provide a routes hash
that pairs routes to actions.
-var Workspace = Backbone.Controller.extend({
+var Workspace = Backbone.Router.extend({
routes: {
"help": "help", // #help
@@ -1449,10 +1443,10 @@ Backbone.Controller
});
-
- controller.routes
+
+ router.routes
- The routes hash maps URLs with parameters to functions on your controller,
+ The routes hash maps URLs with parameters to functions on your router,
similar to the View's events hash.
Routes can contain parameter parts, :param, which match a single URL
component between slashes; and splat parts *splat, which can match
@@ -1470,9 +1464,9 @@
Backbone.Controller
When the visitor presses the back button, or enters a URL, and a particular
route is matched, the name of the action will be fired as an
- event, so that other objects can listen to the controller,
+ event, so that other objects can listen to the router,
and be notified. In the following example, visiting #help/uploading
- will fire a route:help event from the controller.
+ will fire a route:help event from the router.
@@ -1485,25 +1479,25 @@ Backbone.Controller
-controller.bind("route:help", function(page) {
+router.bind("route:help", function(page) {
...
});
-
- new Controller([options])
+
+ new Router([options])
- When creating a new controller, you may pass its
- routes hash directly as an option, if you
+ When creating a new router, you may pass its
+ routes hash directly as an option, if you
choose. All options will also be passed to your initialize
function, if defined.
-
- controller.route(route, name, callback)
+
+ router.route(route, name, callback)
- Manually create a route for the controller, The route argument may
- be a routing string or regular expression.
+ Manually create a route for the router, The route argument may
+ be a routing string or regular expression.
Each matching capture from the route or regular expression will be passed as
an argument to the callback. The name argument will be triggered as
a "route:name" event whenever the route is matched.
@@ -1521,8 +1515,8 @@
Backbone.Controller
}
-
- controller.saveLocation(fragment)
+
+ router.saveLocation(fragment)
Whenever you reach a point in your application that you'd like to save
as a URL, call saveLocation in order to update the URL fragment
@@ -1535,6 +1529,18 @@
Backbone.Controller
this.document.pages.at(pageNumber).open();
this.saveLocation("page/" + pageNumber);
}
+
+
+
+ router.setLocation(fragment)
+
+ Just like saveLocation, but also triggers
+ your route action at the same time. Useful if you want to transition to a page
+ where no state serialization is necessary, like a simple string.
+
+
+
+app.setLocation("help/troubleshooting");
Backbone.history
@@ -1544,13 +1550,13 @@ Backbone.history
events, match the appropriate route, and trigger callbacks. You shouldn't
ever have to create one of these yourself — you should use the reference
to Backbone.history that will be created for you automatically if you make use
- of Controllers with routes.
+ of Routers with routes.
Backbone.history.start()
- When all of your Controllers have been created,
+ When all of your Routers have been created,
and all of the routes are set up properly, call Backbone.history.start()
to begin monitoring hashchange events, and dispatching routes.
@@ -1563,8 +1569,8 @@ Backbone.history
$(function(){
- new WorkspaceController();
- new HelpPaneController();
+ new WorkspaceRouter();
+ new HelpPaneRouter();
Backbone.history.start();
});
@@ -2103,7 +2109,7 @@ Examples
Tzigla, a collaborative drawing
application where artists make tiles that connect to each other to
create surreal drawings.
- Backbone models help organize the code, controllers provide
+ Backbone models help organize the code, routers provide
bookmarkable deep links,
and the views are rendered with
haml.js and
@@ -2123,7 +2129,7 @@ Examples
Michael Aufreiter is building an open source document authoring and
publishing engine: Substance.
- Substance makes use of Backbone.View and Backbone.Controller, while
+ Substance makes use of Backbone.View and Backbone.Router, while
Backbone plays well together with
Data.js, which is used for
data persistence.
@@ -2153,7 +2159,7 @@
F.A.Q.
"change:[attribute]" (model, collection) — when a specific attribute has been updated.
"destrooy" (model, collection) — when a model is destroyed.
"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.
+ "route:[name]" (router) — when one of a router's routes has matched.
"all" — this special event fires for any triggered event, passing the event name as the first argument.
@@ -2236,7 +2242,7 @@ F.A.Q.
with sorting/filtering/aggregation logic.
- Backbone.Controller – Rails routes.rb + Rails controller
+ Backbone.Router – Rails routes.rb + Rails controller
actions. Maps URLs to functions.
From 9a1deb61c72a7cc015e0f57673578a27021d322f Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Thu, 26 May 2011 13:41:41 -0400
Subject: [PATCH 12/19] mention that Backbone require's jQuery > 1.4.2, if
using jQuery. Issue #367.
---
index.html | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/index.html b/index.html
index acecf7776..2874e8a9b 100644
--- a/index.html
+++ b/index.html
@@ -367,7 +367,8 @@
Backbone.View,
it's highly recommended to include
json2.js, and either
- jQuery or Zepto.
+ jQuery ( > 1.4.2) or
+ Zepto.
Introduction
From 2b1eae4d9c50a77a252fdcfbaa4e8aefa66df8fc Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Thu, 26 May 2011 13:54:22 -0400
Subject: [PATCH 13/19] Issue #365. delete options.comparator.
---
backbone.js | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/backbone.js b/backbone.js
index 5cef2cecd..bef054737 100644
--- a/backbone.js
+++ b/backbone.js
@@ -410,10 +410,7 @@
// 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;
- }
+ if (options.comparator) this.comparator = options.comparator;
_.bindAll(this, '_onModelEvent', '_removeReference');
this._reset();
if (models) this.reset(models, {silent: true});
From 9503627e08f2fa784e503c6da00c943331ae107b Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Thu, 26 May 2011 14:00:45 -0400
Subject: [PATCH 14/19] documenting that fetch options can also be jquery
options.
---
index.html | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/index.html b/index.html
index 2874e8a9b..c1d624380 100644
--- a/index.html
+++ b/index.html
@@ -1341,6 +1341,12 @@ Backbone.Collection
of replacing the collection's contents, pass {add: true} as an
option to fetch.
+
+
+ jQuery.ajax options can also be passed directly as fetch options,
+ so to fetch a specific page of a paginated collection:
+ Documents.fetch({data: {page: 3}})
+
Note that fetch should not be used to populate collections on
From 78212c2b9160c0b01132ed40ed23f01d94fe1e42 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Thu, 26 May 2011 14:05:27 -0400
Subject: [PATCH 15/19] Issue #359 -- removing duplicate model.escape
documentation.
---
index.html | 17 -----------------
1 file changed, 17 deletions(-)
diff --git a/index.html b/index.html
index c1d624380..cb73e2083 100644
--- a/index.html
+++ b/index.html
@@ -581,23 +581,6 @@ Backbone.Model
note.get("title")
-
- model.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.
-
-
-
-var hacker = new Backbone.Model({
- name: "<script>alert('xss')</script>"
-});
-
-alert(hacker.escape('name'));
-
-
model.set(attributes, [options])
From 2f0447365735d864d1035b589db407623b409aa0 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Thu, 26 May 2011 14:13:31 -0400
Subject: [PATCH 16/19] fixing refresh -> reset documentation.
---
backbone.js | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/backbone.js b/backbone.js
index bef054737..7fa68d476 100644
--- a/backbone.js
+++ b/backbone.js
@@ -492,8 +492,8 @@
},
// When you have more items than you want to add or remove individually,
- // you can refresh the entire set with a new list of models, without firing
- // any `added` or `removed` events. Fires `refresh` when finished.
+ // you can reset the entire set with a new list of models, without firing
+ // any `added` or `removed` events. Fires `reset` when finished.
reset : function(models, options) {
models || (models = []);
options || (options = {});
@@ -504,9 +504,9 @@
return this;
},
- // Fetch the default set of models for this collection, refreshing the
+ // Fetch the default set of models for this collection, resetting the
// collection when they arrive. If `add: true` is passed, appends the
- // models to the collection instead of refreshing.
+ // models to the collection instead of resetting.
fetch : function(options) {
options || (options = {});
var collection = this;
@@ -553,7 +553,7 @@
return _(this.models).chain();
},
- // Reset all internal state. Called when the collection is refreshed.
+ // Reset all internal state. Called when the collection is reset.
_reset : function(options) {
this.length = 0;
this.models = [];
From d8d94f1391b3c8007a200c1e7cdc3625cf5ad300 Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Thu, 26 May 2011 14:25:56 -0400
Subject: [PATCH 17/19] add a warning about pass by reference. Issue #356
---
index.html | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/index.html b/index.html
index cb73e2083..569663455 100644
--- a/index.html
+++ b/index.html
@@ -697,6 +697,11 @@ Backbone.Model
alert("Dessert will be " + (new Meal).get('dessert'));
+
+ Remember that in JavaScript, objects are passed by reference, so if you
+ include an object as a default value, it will be shared among all instances.
+
+
model.toJSON()
From 5f7b01d94c4b687d8d5056ce8a21146cb5907bcd Mon Sep 17 00:00:00 2001
From: Jeremy Ashkenas
Date: Thu, 26 May 2011 14:30:37 -0400
Subject: [PATCH 18/19] document collection.reset() with no args.
---
index.html | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/index.html b/index.html
index 569663455..b2f2b07ff 100644
--- a/index.html
+++ b/index.html
@@ -1367,6 +1367,11 @@ Backbone.Collection
</script>
+
+ Calling collection.reset() without passing any models as arguments
+ will empty the entire collection.
+
+
collection.create(attributes, [options])
From d7e3d5bf4ad8ebf8482c873d3742a7b21580a59e Mon Sep 17 00:00:00 2001
From: Paul Uithol
Date: Thu, 26 May 2011 21:34:09 +0200
Subject: [PATCH 19/19] Updated to apply on the 0.5.0 branch
---
backbone.js | 34 ++++++++++++++++++++--------------
1 file changed, 20 insertions(+), 14 deletions(-)
diff --git a/backbone.js b/backbone.js
index 735fc9c72..c2e561892 100644
--- a/backbone.js
+++ b/backbone.js
@@ -521,16 +521,12 @@
// Create a new instance of a model in this collection. After the model
// has been created on the server, it will be added to the collection.
+ // Returns the model, or 'false' if validation on a new model fails.
create : function(model, options) {
var coll = this;
options || (options = {});
- if (!(model instanceof Backbone.Model)) {
- var attrs = model;
- model = new this.model(null, {collection: coll});
- if (!model.set(attrs, options)) return false;
- } else {
- model.collection = coll;
- }
+ model = this._prepareModel(model, options);
+ if (!model) return false;
var success = options.success;
options.success = function(nextModel, resp, xhr) {
coll.add(nextModel, options);
@@ -553,7 +549,7 @@
return _(this.models).chain();
},
- // Reset all internal state. Called when the collection is reset.
+ // Reset all internal state. Called when the collection is refreshed.
_reset : function(options) {
this.length = 0;
this.models = [];
@@ -561,20 +557,30 @@
this._byCid = {};
},
+ // Prepare a model to be added to this collection
+ _prepareModel: function(model, options) {
+ if (!(model instanceof Backbone.Model)) {
+ var attrs = model;
+ model = new this.model(null, {collection: this});
+ if (!model.set(attrs, options)) model = false;
+ }
+ else if (!model.collection) {
+ model.collection = this;
+ }
+ return model;
+ },
+
// Internal implementation of adding a single model to the set, updating
// hash indexes for `id` and `cid` lookups.
+ // Returns the model, or 'false' if validation on a new model fails.
_add : function(model, options) {
options || (options = {});
- if (!(model instanceof Backbone.Model)) {
- model = new this.model(model, {collection: this});
- }
+ model = this._prepareModel(model, options);
+ if (!model) return false;
var already = this.getByCid(model);
if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
this._byId[model.id] = model;
this._byCid[model.cid] = model;
- if (!model.collection) {
- model.collection = this;
- }
var index = options.at != null ? options.at :
this.comparator ? this.sortedIndex(model, this.comparator) :
this.length;