├── .gitignore ├── README.md ├── app.js ├── public ├── js │ ├── backbone.js │ ├── jquery-1.4.4.min.js │ ├── jquery.tmpl.min.js │ └── underscore-1.1.0.js └── stylesheets │ └── style.less ├── test └── app.test.js └── views ├── examples ├── click-counter.ejs ├── hello-world.ejs └── simple-list.ejs ├── index.ejs └── layout.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.bak 3 | .DS_Store 4 | *.swo 5 | *.swp 6 | .*.swp 7 | .*.swo 8 | .*.bak 9 | !.gitignore 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backbone Examples from Knockout 2 | 3 | This project contains [Knockout](http://knockoutjs.com) examples ported to 4 | [Backbone](https://documentcloud.github.com/backbone/). The motivation 5 | is to learn enough about each to determine which framework best suits my 6 | style. 7 | 8 | ## Opinion 9 | 10 | My initial impression is Knockout is the more elegant 11 | framework as of this writing. However, almost everything [jashkenas](https://github.com/jashkenas), 12 | the author of Backbone, has created has been excellent. Backbone's 13 | markup is cleaner, which facilitates integrating creative 14 | assets from designers. Backbone's' synchronization with RESTful services 15 | could also be a plus. We'll see. 16 | 17 | Knockout's examples have too much inline javascript in data attributes. Perhaps that 18 | is intentional to keep the examples concise. Not sure I like that. Who knows, I'm un-learning 19 | a lot of things and that may be one of those compromises which makes code simpler at 20 | the expense of *architectural* correctness. 21 | 22 | ## Pre-requisites 23 | 24 | * [express](https://github.com/visionmedia/express) - awesome web framework 25 | * [ejs](https://github.com/kof/node-jqtpl) 26 | 27 | Install both via [npm](https://github.com/isaacs/npm) 28 | 29 | ## Examples Ported 30 | 31 | * Hello World 32 | * Click Counter 33 | * Simple List 34 | 35 | ## Run It 36 | 37 | node app.js 38 | 39 | ## TODOS 40 | 41 | * Use Docco 42 | * Create a Pretty Examples Site 43 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var express = require('express'); 7 | var app = module.exports = express.createServer(); 8 | 9 | 10 | // Configuration 11 | 12 | app.configure(function(){ 13 | app.set('views', __dirname + '/views'); 14 | app.set('view engine', 'ejs'); 15 | app.use(express.bodyDecoder()); 16 | app.use(express.methodOverride()); 17 | app.use(express.compiler({ src: __dirname + '/public', enable: ['less'] })); 18 | app.use(app.router); 19 | app.use(express.staticProvider(__dirname + '/public')); 20 | }); 21 | 22 | app.configure('development', function(){ 23 | app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 24 | }); 25 | 26 | app.configure('production', function(){ 27 | app.use(express.errorHandler()); 28 | }); 29 | 30 | 31 | // Routes 32 | 33 | 34 | app.get('/:example', function(req, res){ 35 | res.render('examples/' + req.params.example, { 36 | locals: { 37 | title: req.params.example 38 | } 39 | }); 40 | }); 41 | 42 | app.get('/', function(req, res){ 43 | res.render('index', { 44 | locals: { 45 | title: 'Backbone Examples Ported From Knockout' 46 | } 47 | }); 48 | }); 49 | 50 | 51 | // Only listen on $ node app.js 52 | 53 | if (!module.parent) { 54 | app.listen(3000); 55 | console.log("Express server listening on port %d", app.address().port); 56 | } 57 | -------------------------------------------------------------------------------- /public/js/backbone.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 0.3.1 2 | // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Backbone may be freely distributed under the MIT license. 4 | // For all details and documentation: 5 | // http://documentcloud.github.com/backbone 6 | 7 | (function(){ 8 | 9 | // Initial Setup 10 | // ------------- 11 | 12 | // The top-level namespace. All public Backbone classes and modules will 13 | // be attached to this. Exported for both CommonJS and the browser. 14 | var Backbone; 15 | if (typeof exports !== 'undefined') { 16 | Backbone = exports; 17 | } else { 18 | Backbone = this.Backbone = {}; 19 | } 20 | 21 | // Current version of the library. Keep in sync with `package.json`. 22 | Backbone.VERSION = '0.3.1'; 23 | 24 | // Require Underscore, if we're on the server, and it's not already present. 25 | var _ = this._; 26 | if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._; 27 | 28 | // For Backbone's purposes, jQuery owns the `$` variable. 29 | var $ = this.jQuery; 30 | 31 | // Turn on `emulateHTTP` to use support legacy HTTP servers. Setting this option will 32 | // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a 33 | // `X-Http-Method-Override` header. 34 | Backbone.emulateHTTP = false; 35 | 36 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 37 | // `application/json` requests ... will encode the body as 38 | // `application/x-www-form-urlencoded` instead and will send the model in a 39 | // form param named `model`. 40 | Backbone.emulateJSON = false; 41 | 42 | // Backbone.Events 43 | // ----------------- 44 | 45 | // A module that can be mixed in to *any object* in order to provide it with 46 | // custom events. You may `bind` or `unbind` a callback function to an event; 47 | // `trigger`-ing an event fires all callbacks in succession. 48 | // 49 | // var object = {}; 50 | // _.extend(object, Backbone.Events); 51 | // object.bind('expand', function(){ alert('expanded'); }); 52 | // object.trigger('expand'); 53 | // 54 | Backbone.Events = { 55 | 56 | // Bind an event, specified by a string name, `ev`, to a `callback` function. 57 | // Passing `"all"` will bind the callback to all events fired. 58 | bind : function(ev, callback) { 59 | var calls = this._callbacks || (this._callbacks = {}); 60 | var list = this._callbacks[ev] || (this._callbacks[ev] = []); 61 | list.push(callback); 62 | return this; 63 | }, 64 | 65 | // Remove one or many callbacks. If `callback` is null, removes all 66 | // callbacks for the event. If `ev` is null, removes all bound callbacks 67 | // for all events. 68 | unbind : function(ev, callback) { 69 | var calls; 70 | if (!ev) { 71 | this._callbacks = {}; 72 | } else if (calls = this._callbacks) { 73 | if (!callback) { 74 | calls[ev] = []; 75 | } else { 76 | var list = calls[ev]; 77 | if (!list) return this; 78 | for (var i = 0, l = list.length; i < l; i++) { 79 | if (callback === list[i]) { 80 | list.splice(i, 1); 81 | break; 82 | } 83 | } 84 | } 85 | } 86 | return this; 87 | }, 88 | 89 | // Trigger an event, firing all bound callbacks. Callbacks are passed the 90 | // same arguments as `trigger` is, apart from the event name. 91 | // Listening for `"all"` passes the true event name as the first argument. 92 | trigger : function(ev) { 93 | var list, calls, i, l; 94 | if (!(calls = this._callbacks)) return this; 95 | if (list = calls[ev]) { 96 | for (i = 0, l = list.length; i < l; i++) { 97 | list[i].apply(this, Array.prototype.slice.call(arguments, 1)); 98 | } 99 | } 100 | if (list = calls['all']) { 101 | for (i = 0, l = list.length; i < l; i++) { 102 | list[i].apply(this, arguments); 103 | } 104 | } 105 | return this; 106 | } 107 | 108 | }; 109 | 110 | // Backbone.Model 111 | // -------------- 112 | 113 | // Create a new model, with defined attributes. A client id (`cid`) 114 | // is automatically generated and assigned for you. 115 | Backbone.Model = function(attributes, options) { 116 | this.attributes = {}; 117 | this.cid = _.uniqueId('c'); 118 | this.set(attributes || {}, {silent : true}); 119 | this._previousAttributes = _.clone(this.attributes); 120 | if (options && options.collection) this.collection = options.collection; 121 | this.initialize(attributes, options); 122 | }; 123 | 124 | // Attach all inheritable methods to the Model prototype. 125 | _.extend(Backbone.Model.prototype, Backbone.Events, { 126 | 127 | // A snapshot of the model's previous attributes, taken immediately 128 | // after the last `"change"` event was fired. 129 | _previousAttributes : null, 130 | 131 | // Has the item been changed since the last `"change"` event? 132 | _changed : false, 133 | 134 | // Initialize is an empty function by default. Override it with your own 135 | // initialization logic. 136 | initialize : function(){}, 137 | 138 | // Return a copy of the model's `attributes` object. 139 | toJSON : function() { 140 | return _.clone(this.attributes); 141 | }, 142 | 143 | // Get the value of an attribute. 144 | get : function(attr) { 145 | return this.attributes[attr]; 146 | }, 147 | 148 | // Set a hash of model attributes on the object, firing `"change"` unless you 149 | // choose to silence it. 150 | set : function(attrs, options) { 151 | 152 | // Extract attributes and options. 153 | options || (options = {}); 154 | if (!attrs) return this; 155 | if (attrs.attributes) attrs = attrs.attributes; 156 | var now = this.attributes; 157 | 158 | // Run validation. 159 | if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false; 160 | 161 | // Check for changes of `id`. 162 | if ('id' in attrs) this.id = attrs.id; 163 | 164 | // Update attributes. 165 | for (var attr in attrs) { 166 | var val = attrs[attr]; 167 | if (!_.isEqual(now[attr], val)) { 168 | now[attr] = val; 169 | if (!options.silent) { 170 | this._changed = true; 171 | this.trigger('change:' + attr, this, val); 172 | } 173 | } 174 | } 175 | 176 | // Fire the `"change"` event, if the model has been changed. 177 | if (!options.silent && this._changed) this.change(); 178 | return this; 179 | }, 180 | 181 | // Remove an attribute from the model, firing `"change"` unless you choose 182 | // to silence it. 183 | unset : function(attr, options) { 184 | options || (options = {}); 185 | var value = this.attributes[attr]; 186 | 187 | // Run validation. 188 | var validObj = {}; 189 | validObj[attr] = void 0; 190 | if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; 191 | 192 | // Remove the attribute. 193 | delete this.attributes[attr]; 194 | if (!options.silent) { 195 | this._changed = true; 196 | this.trigger('change:' + attr, this); 197 | this.change(); 198 | } 199 | return this; 200 | }, 201 | 202 | // Clear all attributes on the model, firing `"change"` unless you choose 203 | // to silence it. 204 | clear : function(options) { 205 | options || (options = {}); 206 | var old = this.attributes; 207 | 208 | // Run validation. 209 | var validObj = {}; 210 | for (attr in old) validObj[attr] = void 0; 211 | if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; 212 | 213 | this.attributes = {}; 214 | if (!options.silent) { 215 | this._changed = true; 216 | for (attr in old) { 217 | this.trigger('change:' + attr, this); 218 | } 219 | this.change(); 220 | } 221 | return this; 222 | }, 223 | 224 | // Fetch the model from the server. If the server's representation of the 225 | // model differs from its current attributes, they will be overriden, 226 | // triggering a `"change"` event. 227 | fetch : function(options) { 228 | options || (options = {}); 229 | var model = this; 230 | var success = function(resp) { 231 | if (!model.set(model.parse(resp), options)) return false; 232 | if (options.success) options.success(model, resp); 233 | }; 234 | var error = options.error && _.bind(options.error, null, model); 235 | Backbone.sync('read', this, success, error); 236 | return this; 237 | }, 238 | 239 | // Set a hash of model attributes, and sync the model to the server. 240 | // If the server returns an attributes hash that differs, the model's 241 | // state will be `set` again. 242 | save : function(attrs, options) { 243 | attrs || (attrs = {}); 244 | options || (options = {}); 245 | if (!this.set(attrs, options)) return false; 246 | var model = this; 247 | var success = function(resp) { 248 | if (!model.set(model.parse(resp), options)) return false; 249 | if (options.success) options.success(model, resp); 250 | }; 251 | var error = options.error && _.bind(options.error, null, model); 252 | var method = this.isNew() ? 'create' : 'update'; 253 | Backbone.sync(method, this, success, error); 254 | return this; 255 | }, 256 | 257 | // Destroy this model on the server. Upon success, the model is removed 258 | // from its collection, if it has one. 259 | destroy : function(options) { 260 | options || (options = {}); 261 | var model = this; 262 | var success = function(resp) { 263 | if (model.collection) model.collection.remove(model); 264 | if (options.success) options.success(model, resp); 265 | }; 266 | var error = options.error && _.bind(options.error, null, model); 267 | Backbone.sync('delete', this, success, error); 268 | return this; 269 | }, 270 | 271 | // Default URL for the model's representation on the server -- if you're 272 | // using Backbone's restful methods, override this to change the endpoint 273 | // that will be called. 274 | url : function() { 275 | var base = getUrl(this.collection); 276 | if (this.isNew()) return base; 277 | return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + this.id; 278 | }, 279 | 280 | // **parse** converts a response into the hash of attributes to be `set` on 281 | // the model. The default implementation is just to pass the response along. 282 | parse : function(resp) { 283 | return resp; 284 | }, 285 | 286 | // Create a new model with identical attributes to this one. 287 | clone : function() { 288 | return new this.constructor(this); 289 | }, 290 | 291 | // A model is new if it has never been saved to the server, and has a negative 292 | // ID. 293 | isNew : function() { 294 | return !this.id; 295 | }, 296 | 297 | // Call this method to manually fire a `change` event for this model. 298 | // Calling this will cause all objects observing the model to update. 299 | change : function() { 300 | this.trigger('change', this); 301 | this._previousAttributes = _.clone(this.attributes); 302 | this._changed = false; 303 | }, 304 | 305 | // Determine if the model has changed since the last `"change"` event. 306 | // If you specify an attribute name, determine if that attribute has changed. 307 | hasChanged : function(attr) { 308 | if (attr) return this._previousAttributes[attr] != this.attributes[attr]; 309 | return this._changed; 310 | }, 311 | 312 | // Return an object containing all the attributes that have changed, or false 313 | // if there are no changed attributes. Useful for determining what parts of a 314 | // view need to be updated and/or what attributes need to be persisted to 315 | // the server. 316 | changedAttributes : function(now) { 317 | now || (now = this.attributes); 318 | var old = this._previousAttributes; 319 | var changed = false; 320 | for (var attr in now) { 321 | if (!_.isEqual(old[attr], now[attr])) { 322 | changed = changed || {}; 323 | changed[attr] = now[attr]; 324 | } 325 | } 326 | return changed; 327 | }, 328 | 329 | // Get the previous value of an attribute, recorded at the time the last 330 | // `"change"` event was fired. 331 | previous : function(attr) { 332 | if (!attr || !this._previousAttributes) return null; 333 | return this._previousAttributes[attr]; 334 | }, 335 | 336 | // Get all of the attributes of the model at the time of the previous 337 | // `"change"` event. 338 | previousAttributes : function() { 339 | return _.clone(this._previousAttributes); 340 | }, 341 | 342 | // Run validation against a set of incoming attributes, returning `true` 343 | // if all is well. If a specific `error` callback has been passed, 344 | // call that instead of firing the general `"error"` event. 345 | _performValidation : function(attrs, options) { 346 | var error = this.validate(attrs); 347 | if (error) { 348 | if (options.error) { 349 | options.error(this, error); 350 | } else { 351 | this.trigger('error', this, error); 352 | } 353 | return false; 354 | } 355 | return true; 356 | } 357 | 358 | }); 359 | 360 | // Backbone.Collection 361 | // ------------------- 362 | 363 | // Provides a standard collection class for our sets of models, ordered 364 | // or unordered. If a `comparator` is specified, the Collection will maintain 365 | // its models in sort order, as they're added and removed. 366 | Backbone.Collection = function(models, options) { 367 | options || (options = {}); 368 | if (options.comparator) { 369 | this.comparator = options.comparator; 370 | delete options.comparator; 371 | } 372 | this._boundOnModelEvent = _.bind(this._onModelEvent, this); 373 | this._reset(); 374 | if (models) this.refresh(models, {silent: true}); 375 | this.initialize(models, options); 376 | }; 377 | 378 | // Define the Collection's inheritable methods. 379 | _.extend(Backbone.Collection.prototype, Backbone.Events, { 380 | 381 | // The default model for a collection is just a **Backbone.Model**. 382 | // This should be overridden in most cases. 383 | model : Backbone.Model, 384 | 385 | // Initialize is an empty function by default. Override it with your own 386 | // initialization logic. 387 | initialize : function(){}, 388 | 389 | // The JSON representation of a Collection is an array of the 390 | // models' attributes. 391 | toJSON : function() { 392 | return this.map(function(model){ return model.toJSON(); }); 393 | }, 394 | 395 | // Add a model, or list of models to the set. Pass **silent** to avoid 396 | // firing the `added` event for every new model. 397 | add : function(models, options) { 398 | if (_.isArray(models)) { 399 | for (var i = 0, l = models.length; i < l; i++) { 400 | this._add(models[i], options); 401 | } 402 | } else { 403 | this._add(models, options); 404 | } 405 | return this; 406 | }, 407 | 408 | // Remove a model, or a list of models from the set. Pass silent to avoid 409 | // firing the `removed` event for every model removed. 410 | remove : function(models, options) { 411 | if (_.isArray(models)) { 412 | for (var i = 0, l = models.length; i < l; i++) { 413 | this._remove(models[i], options); 414 | } 415 | } else { 416 | this._remove(models, options); 417 | } 418 | return this; 419 | }, 420 | 421 | // Get a model from the set by id. 422 | get : function(id) { 423 | if (id == null) return null; 424 | return this._byId[id.id != null ? id.id : id]; 425 | }, 426 | 427 | // Get a model from the set by client id. 428 | getByCid : function(cid) { 429 | return cid && this._byCid[cid.cid || cid]; 430 | }, 431 | 432 | // Get the model at the given index. 433 | at: function(index) { 434 | return this.models[index]; 435 | }, 436 | 437 | // Force the collection to re-sort itself. You don't need to call this under normal 438 | // circumstances, as the set will maintain sort order as each item is added. 439 | sort : function(options) { 440 | options || (options = {}); 441 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 442 | this.models = this.sortBy(this.comparator); 443 | if (!options.silent) this.trigger('refresh', this); 444 | return this; 445 | }, 446 | 447 | // Pluck an attribute from each model in the collection. 448 | pluck : function(attr) { 449 | return _.map(this.models, function(model){ return model.get(attr); }); 450 | }, 451 | 452 | // When you have more items than you want to add or remove individually, 453 | // you can refresh the entire set with a new list of models, without firing 454 | // any `added` or `removed` events. Fires `refresh` when finished. 455 | refresh : function(models, options) { 456 | models || (models = []); 457 | options || (options = {}); 458 | this._reset(); 459 | this.add(models, {silent: true}); 460 | if (!options.silent) this.trigger('refresh', this); 461 | return this; 462 | }, 463 | 464 | // Fetch the default set of models for this collection, refreshing the 465 | // collection when they arrive. 466 | fetch : function(options) { 467 | options || (options = {}); 468 | var collection = this; 469 | var success = function(resp) { 470 | collection.refresh(collection.parse(resp)); 471 | if (options.success) options.success(collection, resp); 472 | }; 473 | var error = options.error && _.bind(options.error, null, collection); 474 | Backbone.sync('read', this, success, error); 475 | return this; 476 | }, 477 | 478 | // Create a new instance of a model in this collection. After the model 479 | // has been created on the server, it will be added to the collection. 480 | create : function(model, options) { 481 | var coll = this; 482 | options || (options = {}); 483 | if (!(model instanceof Backbone.Model)) { 484 | model = new this.model(model, {collection: coll}); 485 | } else { 486 | model.collection = coll; 487 | } 488 | var success = function(nextModel, resp) { 489 | coll.add(nextModel); 490 | if (options.success) options.success(nextModel, resp); 491 | }; 492 | return model.save(null, {success : success, error : options.error}); 493 | }, 494 | 495 | // **parse** converts a response into a list of models to be added to the 496 | // collection. The default implementation is just to pass it through. 497 | parse : function(resp) { 498 | return resp; 499 | }, 500 | 501 | // Proxy to _'s chain. Can't be proxied the same way the rest of the 502 | // underscore methods are proxied because it relies on the underscore 503 | // constructor. 504 | chain: function () { 505 | return _(this.models).chain(); 506 | }, 507 | 508 | // Reset all internal state. Called when the collection is refreshed. 509 | _reset : function(options) { 510 | this.length = 0; 511 | this.models = []; 512 | this._byId = {}; 513 | this._byCid = {}; 514 | }, 515 | 516 | // Internal implementation of adding a single model to the set, updating 517 | // hash indexes for `id` and `cid` lookups. 518 | _add : function(model, options) { 519 | options || (options = {}); 520 | if (!(model instanceof Backbone.Model)) { 521 | model = new this.model(model, {collection: this}); 522 | } 523 | var already = this.getByCid(model); 524 | if (already) throw new Error(["Can't add the same model to a set twice", already.id]); 525 | this._byId[model.id] = model; 526 | this._byCid[model.cid] = model; 527 | model.collection = this; 528 | var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length; 529 | this.models.splice(index, 0, model); 530 | model.bind('all', this._boundOnModelEvent); 531 | this.length++; 532 | if (!options.silent) model.trigger('add', model, this); 533 | return model; 534 | }, 535 | 536 | // Internal implementation of removing a single model from the set, updating 537 | // hash indexes for `id` and `cid` lookups. 538 | _remove : function(model, options) { 539 | options || (options = {}); 540 | model = this.getByCid(model) || this.get(model); 541 | if (!model) return null; 542 | delete this._byId[model.id]; 543 | delete this._byCid[model.cid]; 544 | delete model.collection; 545 | this.models.splice(this.indexOf(model), 1); 546 | this.length--; 547 | if (!options.silent) model.trigger('remove', model, this); 548 | model.unbind('all', this._boundOnModelEvent); 549 | return model; 550 | }, 551 | 552 | // Internal method called every time a model in the set fires an event. 553 | // Sets need to update their indexes when models change ids. All other 554 | // events simply proxy through. 555 | _onModelEvent : function(ev, model) { 556 | if (ev === 'change:id') { 557 | delete this._byId[model.previous('id')]; 558 | this._byId[model.id] = model; 559 | } 560 | this.trigger.apply(this, arguments); 561 | } 562 | 563 | }); 564 | 565 | // Underscore methods that we want to implement on the Collection. 566 | var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', 567 | 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 568 | 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', 569 | 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty']; 570 | 571 | // Mix in each Underscore method as a proxy to `Collection#models`. 572 | _.each(methods, function(method) { 573 | Backbone.Collection.prototype[method] = function() { 574 | return _[method].apply(_, [this.models].concat(_.toArray(arguments))); 575 | }; 576 | }); 577 | 578 | // Backbone.Controller 579 | // ------------------- 580 | 581 | // Controllers map faux-URLs to actions, and fire events when routes are 582 | // matched. Creating a new one sets its `routes` hash, if not set statically. 583 | Backbone.Controller = function(options) { 584 | options || (options = {}); 585 | if (options.routes) this.routes = options.routes; 586 | this._bindRoutes(); 587 | this.initialize(options); 588 | }; 589 | 590 | // Cached regular expressions for matching named param parts and splatted 591 | // parts of route strings. 592 | var namedParam = /:([\w\d]+)/g; 593 | var splatParam = /\*([\w\d]+)/g; 594 | 595 | // Set up all inheritable **Backbone.Controller** properties and methods. 596 | _.extend(Backbone.Controller.prototype, Backbone.Events, { 597 | 598 | // Initialize is an empty function by default. Override it with your own 599 | // initialization logic. 600 | initialize : function(){}, 601 | 602 | // Manually bind a single named route to a callback. For example: 603 | // 604 | // this.route('search/:query/p:num', 'search', function(query, num) { 605 | // ... 606 | // }); 607 | // 608 | route : function(route, name, callback) { 609 | Backbone.history || (Backbone.history = new Backbone.History); 610 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 611 | Backbone.history.route(route, _.bind(function(fragment) { 612 | var args = this._extractParameters(route, fragment); 613 | callback.apply(this, args); 614 | this.trigger.apply(this, ['route:' + name].concat(args)); 615 | }, this)); 616 | }, 617 | 618 | // Simple proxy to `Backbone.history` to save a fragment into the history, 619 | // without triggering routes. 620 | saveLocation : function(fragment) { 621 | Backbone.history.saveLocation(fragment); 622 | }, 623 | 624 | // Bind all defined routes to `Backbone.history`. 625 | _bindRoutes : function() { 626 | if (!this.routes) return; 627 | for (var route in this.routes) { 628 | var name = this.routes[route]; 629 | this.route(route, name, this[name]); 630 | } 631 | }, 632 | 633 | // Convert a route string into a regular expression, suitable for matching 634 | // against the current location fragment. 635 | _routeToRegExp : function(route) { 636 | route = route.replace(namedParam, "([^\/]*)").replace(splatParam, "(.*?)"); 637 | return new RegExp('^' + route + '$'); 638 | }, 639 | 640 | // Given a route, and a URL fragment that it matches, return the array of 641 | // extracted parameters. 642 | _extractParameters : function(route, fragment) { 643 | return route.exec(fragment).slice(1); 644 | } 645 | 646 | }); 647 | 648 | // Backbone.History 649 | // ---------------- 650 | 651 | // Handles cross-browser history management, based on URL hashes. If the 652 | // browser does not support `onhashchange`, falls back to polling. 653 | Backbone.History = function() { 654 | this.handlers = []; 655 | this.fragment = this.getFragment(); 656 | _.bindAll(this, 'checkUrl'); 657 | }; 658 | 659 | // Cached regex for cleaning hashes. 660 | var hashStrip = /^#*/; 661 | 662 | // Set up all inheritable **Backbone.History** properties and methods. 663 | _.extend(Backbone.History.prototype, { 664 | 665 | // The default interval to poll for hash changes, if necessary, is 666 | // twenty times a second. 667 | interval: 50, 668 | 669 | // Get the cross-browser normalized URL fragment. 670 | getFragment : function(loc) { 671 | return (loc || window.location).hash.replace(hashStrip, ''); 672 | }, 673 | 674 | // Start the hash change handling, returning `true` if the current URL matches 675 | // an existing route, and `false` otherwise. 676 | start : function() { 677 | var docMode = document.documentMode; 678 | var oldIE = ($.browser.msie && docMode < 7); 679 | if (oldIE) { 680 | this.iframe = $('