PageRenderTime 27ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/backbone.js

https://github.com/olleolleolle/backbone
JavaScript | 645 lines | 405 code | 78 blank | 162 comment | 111 complexity | 605814c61f2071481000a49edd9ea583 MD5 | raw file
  1. // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
  2. // Backbone may be freely distributed under the MIT license.
  3. // For all details and documentation:
  4. // http://documentcloud.github.com/backbone
  5. (function(){
  6. // Initial Setup
  7. // -------------
  8. // The top-level namespace. All public Backbone classes and modules will
  9. // be attached to this. Exported for both CommonJS and the browser.
  10. var Backbone;
  11. if (typeof exports !== 'undefined') {
  12. Backbone = exports;
  13. } else {
  14. Backbone = this.Backbone = {};
  15. }
  16. // Current version of the library. Keep in sync with `package.json`.
  17. Backbone.VERSION = '0.1.1';
  18. // Require Underscore, if we're on the server, and it's not already present.
  19. var _ = this._;
  20. if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._;
  21. // For Backbone's purposes, jQuery owns the `$` variable.
  22. var $ = this.jQuery;
  23. // Backbone.Events
  24. // -----------------
  25. // A module that can be mixed in to *any object* in order to provide it with
  26. // custom events. You may `bind` or `unbind` a callback function to an event;
  27. // `trigger`-ing an event fires all callbacks in succession.
  28. //
  29. // var object = {};
  30. // _.extend(object, Backbone.Events);
  31. // object.bind('expand', function(){ alert('expanded'); });
  32. // object.trigger('expand');
  33. //
  34. Backbone.Events = {
  35. // Bind an event, specified by a string name, `ev`, to a `callback` function.
  36. // Passing `"all"` will bind the callback to all events fired.
  37. bind : function(ev, callback) {
  38. var calls = this._callbacks || (this._callbacks = {});
  39. var list = this._callbacks[ev] || (this._callbacks[ev] = []);
  40. list.push(callback);
  41. return this;
  42. },
  43. // Remove one or many callbacks. If `callback` is null, removes all
  44. // callbacks for the event. If `ev` is null, removes all bound callbacks
  45. // for all events.
  46. unbind : function(ev, callback) {
  47. var calls;
  48. if (!ev) {
  49. this._callbacks = {};
  50. } else if (calls = this._callbacks) {
  51. if (!callback) {
  52. calls[ev] = [];
  53. } else {
  54. var list = calls[ev];
  55. if (!list) return this;
  56. for (var i = 0, l = list.length; i < l; i++) {
  57. if (callback === list[i]) {
  58. list.splice(i, 1);
  59. break;
  60. }
  61. }
  62. }
  63. }
  64. return this;
  65. },
  66. // Trigger an event, firing all bound callbacks. Callbacks are passed the
  67. // same arguments as `trigger` is, apart from the event name.
  68. // Listening for `"all"` passes the true event name as the first argument.
  69. trigger : function(ev) {
  70. var list, calls, i, l;
  71. if (!(calls = this._callbacks)) return this;
  72. if (list = calls[ev]) {
  73. for (i = 0, l = list.length; i < l; i++) {
  74. list[i].apply(this, Array.prototype.slice.call(arguments, 1));
  75. }
  76. }
  77. if (list = calls['all']) {
  78. for (i = 0, l = list.length; i < l; i++) {
  79. list[i].apply(this, arguments);
  80. }
  81. }
  82. return this;
  83. }
  84. };
  85. // Backbone.Model
  86. // --------------
  87. // Create a new model, with defined attributes. A client id (`cid`)
  88. // is automatically generated and assigned for you.
  89. Backbone.Model = function(attributes) {
  90. this.attributes = {};
  91. this.cid = _.uniqueId('c');
  92. this.set(attributes || {}, {silent : true});
  93. this._previousAttributes = _.clone(this.attributes);
  94. if (this.initialize) this.initialize(attributes);
  95. };
  96. // Attach all inheritable methods to the Model prototype.
  97. _.extend(Backbone.Model.prototype, Backbone.Events, {
  98. // A snapshot of the model's previous attributes, taken immediately
  99. // after the last `changed` event was fired.
  100. _previousAttributes : null,
  101. // Has the item been changed since the last `changed` event?
  102. _changed : false,
  103. // Return a copy of the model's `attributes` object.
  104. toJSON : function() {
  105. return _.clone(this.attributes);
  106. },
  107. // Get the value of an attribute.
  108. get : function(attr) {
  109. return this.attributes[attr];
  110. },
  111. // Set a hash of model attributes on the object, firing `changed` unless you
  112. // choose to silence it.
  113. set : function(attrs, options) {
  114. // Extract attributes and options.
  115. options || (options = {});
  116. if (!attrs) return this;
  117. if (attrs.attributes) attrs = attrs.attributes;
  118. var now = this.attributes;
  119. // Run validation if `validate` is defined.
  120. if (this.validate) {
  121. var error = this.validate(attrs);
  122. if (error) {
  123. this.trigger('error', this, error);
  124. return false;
  125. }
  126. }
  127. // Check for changes of `id`.
  128. if ('id' in attrs) this.id = attrs.id;
  129. // Update attributes.
  130. for (var attr in attrs) {
  131. var val = attrs[attr];
  132. if (val === '') val = null;
  133. if (!_.isEqual(now[attr], val)) {
  134. now[attr] = val;
  135. if (!options.silent) {
  136. this._changed = true;
  137. this.trigger('change:' + attr, this, val);
  138. }
  139. }
  140. }
  141. // Fire the `change` event, if the model has been changed.
  142. if (!options.silent && this._changed) this.change();
  143. return this;
  144. },
  145. // Remove an attribute from the model, firing `changed` unless you choose to
  146. // silence it.
  147. unset : function(attr, options) {
  148. options || (options = {});
  149. var value = this.attributes[attr];
  150. delete this.attributes[attr];
  151. if (!options.silent) {
  152. this._changed = true;
  153. this.trigger('change:' + attr, this);
  154. this.change();
  155. }
  156. return value;
  157. },
  158. // Set a hash of model attributes, and sync the model to the server.
  159. // If the server returns an attributes hash that differs, the model's
  160. // state will be `set` again.
  161. save : function(attrs, options) {
  162. attrs || (attrs = {});
  163. options || (options = {});
  164. if (!this.set(attrs, options)) return false;
  165. var model = this;
  166. var success = function(resp) {
  167. if (!model.set(resp.model)) return false;
  168. if (options.success) options.success(model, resp);
  169. };
  170. var method = this.isNew() ? 'create' : 'update';
  171. Backbone.sync(method, this, success, options.error);
  172. return this;
  173. },
  174. // Destroy this model on the server. Upon success, the model is removed
  175. // from its collection, if it has one.
  176. destroy : function(options) {
  177. options || (options = {});
  178. var model = this;
  179. var success = function(resp) {
  180. if (model.collection) model.collection.remove(model);
  181. if (options.success) options.success(model, resp);
  182. };
  183. Backbone.sync('delete', this, success, options.error);
  184. return this;
  185. },
  186. // Default URL for the model's representation on the server -- if you're
  187. // using Backbone's restful methods, override this to change the endpoint
  188. // that will be called.
  189. url : function() {
  190. var base = getUrl(this.collection);
  191. if (this.isNew()) return base;
  192. return base + '/' + this.id;
  193. },
  194. // Create a new model with identical attributes to this one.
  195. clone : function() {
  196. return new this.constructor(this);
  197. },
  198. // A model is new if it has never been saved to the server, and has a negative
  199. // ID.
  200. isNew : function() {
  201. return !this.id;
  202. },
  203. // Call this method to fire manually fire a `change` event for this model.
  204. // Calling this will cause all objects observing the model to update.
  205. change : function() {
  206. this.trigger('change', this);
  207. this._previousAttributes = _.clone(this.attributes);
  208. this._changed = false;
  209. },
  210. // Determine if the model has changed since the last `changed` event.
  211. // If you specify an attribute name, determine if that attribute has changed.
  212. hasChanged : function(attr) {
  213. if (attr) return this._previousAttributes[attr] != this.attributes[attr];
  214. return this._changed;
  215. },
  216. // Return an object containing all the attributes that have changed, or false
  217. // if there are no changed attributes. Useful for determining what parts of a
  218. // view need to be updated and/or what attributes need to be persisted to
  219. // the server.
  220. changedAttributes : function(now) {
  221. var old = this._previousAttributes, now = now || this.attributes, changed = false;
  222. for (var attr in now) {
  223. if (!_.isEqual(old[attr], now[attr])) {
  224. changed = changed || {};
  225. changed[attr] = now[attr];
  226. }
  227. }
  228. return changed;
  229. },
  230. // Get the previous value of an attribute, recorded at the time the last
  231. // `changed` event was fired.
  232. previous : function(attr) {
  233. if (!attr || !this._previousAttributes) return null;
  234. return this._previousAttributes[attr];
  235. },
  236. // Get all of the attributes of the model at the time of the previous
  237. // `changed` event.
  238. previousAttributes : function() {
  239. return _.clone(this._previousAttributes);
  240. }
  241. });
  242. // Backbone.Collection
  243. // -------------------
  244. // Provides a standard collection class for our sets of models, ordered
  245. // or unordered. If a `comparator` is specified, the Collection will maintain
  246. // its models in sort order, as they're added and removed.
  247. Backbone.Collection = function(models, options) {
  248. options || (options = {});
  249. if (options.comparator) {
  250. this.comparator = options.comparator;
  251. delete options.comparator;
  252. }
  253. this._boundOnModelEvent = _.bind(this._onModelEvent, this);
  254. this._reset();
  255. if (models) this.refresh(models, {silent: true});
  256. if (this.initialize) this.initialize(models, options);
  257. };
  258. // Define the Collection's inheritable methods.
  259. _.extend(Backbone.Collection.prototype, Backbone.Events, {
  260. // The default model for a collection is just a **Backbone.Model**.
  261. // This should be overridden in most cases.
  262. model : Backbone.Model,
  263. // Add a model, or list of models to the set. Pass **silent** to avoid
  264. // firing the `added` event for every new model.
  265. add : function(models, options) {
  266. if (_.isArray(models)) {
  267. for (var i = 0, l = models.length; i < l; i++) {
  268. this._add(models[i], options);
  269. }
  270. } else {
  271. this._add(models, options);
  272. }
  273. return this;
  274. },
  275. // Remove a model, or a list of models from the set. Pass silent to avoid
  276. // firing the `removed` event for every model removed.
  277. remove : function(models, options) {
  278. if (_.isArray(models)) {
  279. for (var i = 0, l = models.length; i < l; i++) {
  280. this._remove(models[i], options);
  281. }
  282. } else {
  283. this._remove(models, options);
  284. }
  285. return this;
  286. },
  287. // Get a model from the set by id.
  288. get : function(id) {
  289. return id && this._byId[id.id != null ? id.id : id];
  290. },
  291. // Get a model from the set by client id.
  292. getByCid : function(cid) {
  293. return cid && this._byCid[cid.cid || cid];
  294. },
  295. // Get the model at the given index.
  296. at: function(index) {
  297. return this.models[index];
  298. },
  299. // Force the collection to re-sort itself. You don't need to call this under normal
  300. // circumstances, as the set will maintain sort order as each item is added.
  301. sort : function(options) {
  302. options || (options = {});
  303. if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
  304. this.models = this.sortBy(this.comparator);
  305. if (!options.silent) this.trigger('refresh', this);
  306. return this;
  307. },
  308. // Pluck an attribute from each model in the collection.
  309. pluck : function(attr) {
  310. return _.map(this.models, function(model){ return model.get(attr); });
  311. },
  312. // When you have more items than you want to add or remove individually,
  313. // you can refresh the entire set with a new list of models, without firing
  314. // any `added` or `removed` events. Fires `refresh` when finished.
  315. refresh : function(models, options) {
  316. models || (models = []);
  317. options || (options = {});
  318. this._reset();
  319. this.add(models, {silent: true});
  320. if (!options.silent) this.trigger('refresh', this);
  321. return this;
  322. },
  323. // Fetch the default set of models for this collection, refreshing the
  324. // collection when they arrive.
  325. fetch : function(options) {
  326. options || (options = {});
  327. var collection = this;
  328. var success = function(resp) {
  329. collection.refresh(resp.models);
  330. if (options.success) options.success(collection, resp);
  331. };
  332. Backbone.sync('read', this, success, options.error);
  333. return this;
  334. },
  335. // Create a new instance of a model in this collection. After the model
  336. // has been created on the server, it will be added to the collection.
  337. create : function(model, options) {
  338. options || (options = {});
  339. if (!(model instanceof Backbone.Model)) model = new this.model(model);
  340. model.collection = this;
  341. var success = function(resp) {
  342. if (!model.set(resp.model)) return false;
  343. model.collection.add(model);
  344. if (options.success) options.success(model, resp);
  345. };
  346. return model.save(null, {success : success, error : options.error});
  347. },
  348. // Reset all internal state. Called when the collection is refreshed.
  349. _reset : function(options) {
  350. this.length = 0;
  351. this.models = [];
  352. this._byId = {};
  353. this._byCid = {};
  354. },
  355. // Internal implementation of adding a single model to the set, updating
  356. // hash indexes for `id` and `cid` lookups.
  357. _add : function(model, options) {
  358. options || (options = {});
  359. if (!(model instanceof Backbone.Model)) {
  360. model = new this.model(model);
  361. }
  362. var already = this.getByCid(model);
  363. if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
  364. this._byId[model.id] = model;
  365. this._byCid[model.cid] = model;
  366. model.collection = this;
  367. var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length;
  368. this.models.splice(index, 0, model);
  369. model.bind('all', this._boundOnModelEvent);
  370. this.length++;
  371. if (!options.silent) this.trigger('add', model);
  372. return model;
  373. },
  374. // Internal implementation of removing a single model from the set, updating
  375. // hash indexes for `id` and `cid` lookups.
  376. _remove : function(model, options) {
  377. options || (options = {});
  378. model = this.getByCid(model);
  379. if (!model) return null;
  380. delete this._byId[model.id];
  381. delete this._byCid[model.cid];
  382. delete model.collection;
  383. this.models.splice(this.indexOf(model), 1);
  384. model.unbind('all', this._boundOnModelEvent);
  385. this.length--;
  386. if (!options.silent) this.trigger('remove', model);
  387. return model;
  388. },
  389. // Internal method called every time a model in the set fires an event.
  390. // Sets need to update their indexes when models change ids.
  391. _onModelEvent : function(ev, model, error) {
  392. switch (ev) {
  393. case 'change':
  394. if (model.hasChanged('id')) {
  395. delete this._byId[model.previous('id')];
  396. this._byId[model.id] = model;
  397. }
  398. this.trigger('change', model);
  399. break;
  400. case 'error':
  401. this.trigger('error', model, error);
  402. }
  403. }
  404. });
  405. // Underscore methods that we want to implement on the Collection.
  406. var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
  407. 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
  408. 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
  409. 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty'];
  410. // Mix in each Underscore method as a proxy to `Collection#models`.
  411. _.each(methods, function(method) {
  412. Backbone.Collection.prototype[method] = function() {
  413. return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
  414. };
  415. });
  416. // Backbone.View
  417. // -------------
  418. // Creating a Backbone.View creates its initial element outside of the DOM,
  419. // if an existing element is not provided...
  420. Backbone.View = function(options) {
  421. this._configure(options || {});
  422. if (this.options.el) {
  423. this.el = this.options.el;
  424. } else {
  425. var attrs = {};
  426. if (this.id) attrs.id = this.id;
  427. if (this.className) attrs.className = this.className;
  428. this.el = this.make(this.tagName, attrs);
  429. }
  430. if (this.initialize) this.initialize(options);
  431. };
  432. // jQuery lookup, scoped to DOM elements within the current view.
  433. // This should be prefered to global jQuery lookups, if you're dealing with
  434. // a specific view.
  435. var jQueryDelegate = function(selector) {
  436. return $(selector, this.el);
  437. };
  438. // Cached regex to split keys for `handleEvents`.
  439. var eventSplitter = /^(\w+)\s*(.*)$/;
  440. // Set up all inheritable **Backbone.View** properties and methods.
  441. _.extend(Backbone.View.prototype, {
  442. // The default `tagName` of a View's element is `"div"`.
  443. tagName : 'div',
  444. // Attach the jQuery function as the `$` and `jQuery` properties.
  445. $ : jQueryDelegate,
  446. jQuery : jQueryDelegate,
  447. // **render** is the core function that your view should override, in order
  448. // to populate its element (`this.el`), with the appropriate HTML. The
  449. // convention is for **render** to always return `this`.
  450. render : function() {
  451. return this;
  452. },
  453. // For small amounts of DOM Elements, where a full-blown template isn't
  454. // needed, use **make** to manufacture elements, one at a time.
  455. //
  456. // var el = this.make('li', {'class': 'row'}, this.model.get('title'));
  457. //
  458. make : function(tagName, attributes, content) {
  459. var el = document.createElement(tagName);
  460. if (attributes) $(el).attr(attributes);
  461. if (content) $(el).html(content);
  462. return el;
  463. },
  464. // Set callbacks, where `this.callbacks` is a hash of
  465. //
  466. // *{"event selector": "callback"}*
  467. //
  468. // {
  469. // 'mousedown .title': 'edit',
  470. // 'click .button': 'save'
  471. // }
  472. //
  473. // pairs. Callbacks will be bound to the view, with `this` set properly.
  474. // Uses jQuery event delegation for efficiency.
  475. // Omitting the selector binds the event to `this.el`.
  476. // `"change"` events are not delegated through the view because IE does not
  477. // bubble change events at all.
  478. handleEvents : function(events) {
  479. $(this.el).unbind();
  480. if (!(events || (events = this.events))) return this;
  481. for (key in events) {
  482. var methodName = events[key];
  483. var match = key.match(eventSplitter);
  484. var eventName = match[1], selector = match[2];
  485. var method = _.bind(this[methodName], this);
  486. if (selector === '' || eventName == 'change') {
  487. $(this.el).bind(eventName, method);
  488. } else {
  489. $(this.el).delegate(selector, eventName, method);
  490. }
  491. }
  492. return this;
  493. },
  494. // Performs the initial configuration of a View with a set of options.
  495. // Keys with special meaning *(model, collection, id, className)*, are
  496. // attached directly to the view.
  497. _configure : function(options) {
  498. if (this.options) options = _.extend({}, this.options, options);
  499. if (options.model) this.model = options.model;
  500. if (options.collection) this.collection = options.collection;
  501. if (options.id) this.id = options.id;
  502. if (options.className) this.className = options.className;
  503. if (options.tagName) this.tagName = options.tagName;
  504. this.options = options;
  505. }
  506. });
  507. // Set up inheritance for the model, collection, and view.
  508. var extend = Backbone.Model.extend = Backbone.Collection.extend = Backbone.View.extend = function (protoProps, classProps) {
  509. var child = inherits(this, protoProps, classProps);
  510. child.extend = extend;
  511. return child;
  512. };
  513. // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
  514. var methodMap = {
  515. 'create': 'POST',
  516. 'update': 'PUT',
  517. 'delete': 'DELETE',
  518. 'read' : 'GET'
  519. };
  520. // Backbone.sync
  521. // -------------
  522. // Override this function to change the manner in which Backbone persists
  523. // models to the server. You will be passed the type of request, and the
  524. // model in question. By default, uses jQuery to make a RESTful Ajax request
  525. // to the model's `url()`. Some possible customizations could be:
  526. //
  527. // * Use `setTimeout` to batch rapid-fire updates into a single request.
  528. // * Send up the models as XML instead of JSON.
  529. // * Persist models via WebSockets instead of Ajax.
  530. //
  531. Backbone.sync = function(method, model, success, error) {
  532. var data = method === 'read' ? {} : {model : JSON.stringify(model)};
  533. $.ajax({
  534. url : getUrl(model),
  535. type : methodMap[method],
  536. data : data,
  537. dataType : 'json',
  538. success : success,
  539. error : error
  540. });
  541. };
  542. // Helpers
  543. // -------
  544. // Helper function to correctly set up the prototype chain, for subclasses.
  545. // Similar to `goog.inherits`, but uses a hash of prototype properties and
  546. // class properties to be extended.
  547. var inherits = function(parent, protoProps, classProps) {
  548. var child;
  549. if (protoProps.hasOwnProperty('constructor')) {
  550. child = protoProps.constructor;
  551. } else {
  552. child = function(){ return parent.apply(this, arguments); };
  553. }
  554. var ctor = function(){};
  555. ctor.prototype = parent.prototype;
  556. child.prototype = new ctor();
  557. _.extend(child.prototype, protoProps);
  558. if (classProps) _.extend(child, classProps);
  559. child.prototype.constructor = child;
  560. return child;
  561. };
  562. // Helper function to get a URL from a Model or Collection as a property
  563. // or as a function.
  564. var getUrl = function(object) {
  565. return _.isFunction(object.url) ? object.url() : object.url;
  566. };
  567. })();