From c71336641ee0354776556cdcfa5522f23ca14abf Mon Sep 17 00:00:00 2001 From: Adam Miller Date: Tue, 9 Feb 2016 13:02:02 -0800 Subject: [PATCH 01/69] Update to `modelId()` - modelId now takes the model in question as the second argument - modelId will look at the model in question's idAttribute before falling back to Collection.prototype.model.modelId and then 'id' - Makes basic polymorphic collection work out of the box while retaining backwards compatability --- backbone.js | 16 ++++++++-------- test/collection.js | 25 +++++++++++++++++-------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/backbone.js b/backbone.js index d0cad3eb6d..c136a37967 100644 --- a/backbone.js +++ b/backbone.js @@ -984,7 +984,7 @@ get: function(obj) { if (obj == null) return void 0; return this._byId[obj] || - this._byId[this.modelId(obj.attributes || obj)] || + this._byId[this.modelId(obj.attributes || obj, obj)] || obj.cid && this._byId[obj.cid]; }, @@ -1088,8 +1088,8 @@ }, // Define how to uniquely identify models in the collection. - modelId: function(attrs) { - return attrs[this.model.prototype.idAttribute || 'id']; + modelId: function(attrs, model) { + return attrs[(model && model.idAttribute) || this.model.prototype.idAttribute || 'id']; }, // Private method to reset all internal state. Called when the collection @@ -1129,7 +1129,7 @@ // Remove references before triggering 'remove' event to prevent an // infinite loop. #3693 delete this._byId[model.cid]; - var id = this.modelId(model.attributes); + var id = this.modelId(model.attributes, model); if (id != null) delete this._byId[id]; if (!options.silent) { @@ -1152,7 +1152,7 @@ // Internal method to create a model's ties to a collection. _addReference: function(model, options) { this._byId[model.cid] = model; - var id = this.modelId(model.attributes); + var id = this.modelId(model.attributes, model); if (id != null) this._byId[id] = model; model.on('all', this._onModelEvent, this); }, @@ -1160,7 +1160,7 @@ // Internal method to sever a model's ties to a collection. _removeReference: function(model, options) { delete this._byId[model.cid]; - var id = this.modelId(model.attributes); + var id = this.modelId(model.attributes, model); if (id != null) delete this._byId[id]; if (this === model.collection) delete model.collection; model.off('all', this._onModelEvent, this); @@ -1175,8 +1175,8 @@ if ((event === 'add' || event === 'remove') && collection !== this) return; if (event === 'destroy') this.remove(model, options); if (event === 'change') { - var prevId = this.modelId(model.previousAttributes()); - var id = this.modelId(model.attributes); + var prevId = this.modelId(model.previousAttributes(), model); + var id = this.modelId(model.attributes, model); if (prevId !== id) { if (prevId != null) delete this._byId[prevId]; if (id != null) this._byId[id] = model; diff --git a/test/collection.js b/test/collection.js index a0f6bf662e..0c53db3790 100644 --- a/test/collection.js +++ b/test/collection.js @@ -1642,12 +1642,20 @@ QUnit.test('modelId', function(assert) { var Stooge = Backbone.Model.extend(); - var StoogeCollection = Backbone.Collection.extend({model: Stooge}); + var StoogeCollection = Backbone.Collection.extend(); - // Default to using `Collection::model::idAttribute`. + // Default to using `id` if `model::idAttribute` and `Collection::model::idAttribute` not present. assert.equal(StoogeCollection.prototype.modelId({id: 1}), 1); + + // Default to using `model::idAttribute` if present. Stooge.prototype.idAttribute = '_id'; + var model = new Stooge({_id: 1}); + assert.equal(StoogeCollection.prototype.modelId(model.attributes, model), 1); + + // Default to using `Collection::model::idAttribute` if model::idAttribute not present. + StoogeCollection.prototype.model = Stooge; assert.equal(StoogeCollection.prototype.modelId({_id: 1}), 1); + }); QUnit.test('Polymorphic models work with "simple" constructors', function(assert) { @@ -1702,7 +1710,7 @@ assert.equal(collection.at(1), collection.get('b-1')); }); - QUnit.test('Collection with polymorphic models receives default id from modelId', function(assert) { + QUnit.test('Collection with polymorphic models receives id from modelId using model instance idAttribute', function(assert) { assert.expect(6); // When the polymorphic models use 'id' for the idAttribute, all is fine. var C1 = Backbone.Collection.extend({ @@ -1715,7 +1723,8 @@ assert.equal(c1.modelId({id: 1}), 1); // If the polymorphic models define their own idAttribute, - // the modelId method should be overridden, for the reason below. + // the modelId method will use the model's idAttribute property before the + // collection's model constructor's. var M = Backbone.Model.extend({ idAttribute: '_id' }); @@ -1725,12 +1734,12 @@ } }); var c2 = new C2({'_id': 1}); - assert.equal(c2.get(1), void 0); - assert.equal(c2.modelId(c2.at(0).attributes), void 0); + assert.equal(c2.get(1), c2.at(0)); + assert.equal(c2.modelId(c2.at(0).attributes, c2.at(0)), 1); var m = new M({'_id': 2}); c2.add(m); - assert.equal(c2.get(2), void 0); - assert.equal(c2.modelId(m.attributes), void 0); + assert.equal(c2.get(2), m); + assert.equal(c2.modelId(m.attributes, m), 2); }); QUnit.test('#3039 #3951: adding at index fires with correct at', function(assert) { From b3a28132901f3acc881bfa1efcc4acb04b2061b9 Mon Sep 17 00:00:00 2001 From: Adam Miller Date: Wed, 24 Feb 2016 17:36:51 -0800 Subject: [PATCH 02/69] Only pass idAttribute to collection.modelId() --- backbone.js | 16 ++++++++-------- test/collection.js | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backbone.js b/backbone.js index c136a37967..00e684052c 100644 --- a/backbone.js +++ b/backbone.js @@ -984,7 +984,7 @@ get: function(obj) { if (obj == null) return void 0; return this._byId[obj] || - this._byId[this.modelId(obj.attributes || obj, obj)] || + this._byId[this.modelId(obj.attributes || obj, obj.idAttribute)] || obj.cid && this._byId[obj.cid]; }, @@ -1088,8 +1088,8 @@ }, // Define how to uniquely identify models in the collection. - modelId: function(attrs, model) { - return attrs[(model && model.idAttribute) || this.model.prototype.idAttribute || 'id']; + modelId: function(attrs, idAttribute) { + return attrs[idAttribute || this.model.prototype.idAttribute || 'id']; }, // Private method to reset all internal state. Called when the collection @@ -1129,7 +1129,7 @@ // Remove references before triggering 'remove' event to prevent an // infinite loop. #3693 delete this._byId[model.cid]; - var id = this.modelId(model.attributes, model); + var id = this.modelId(model.attributes, model.idAttribute); if (id != null) delete this._byId[id]; if (!options.silent) { @@ -1152,7 +1152,7 @@ // Internal method to create a model's ties to a collection. _addReference: function(model, options) { this._byId[model.cid] = model; - var id = this.modelId(model.attributes, model); + var id = this.modelId(model.attributes, model.idAttribute); if (id != null) this._byId[id] = model; model.on('all', this._onModelEvent, this); }, @@ -1160,7 +1160,7 @@ // Internal method to sever a model's ties to a collection. _removeReference: function(model, options) { delete this._byId[model.cid]; - var id = this.modelId(model.attributes, model); + var id = this.modelId(model.attributes, model.idAttribute); if (id != null) delete this._byId[id]; if (this === model.collection) delete model.collection; model.off('all', this._onModelEvent, this); @@ -1175,8 +1175,8 @@ if ((event === 'add' || event === 'remove') && collection !== this) return; if (event === 'destroy') this.remove(model, options); if (event === 'change') { - var prevId = this.modelId(model.previousAttributes(), model); - var id = this.modelId(model.attributes, model); + var prevId = this.modelId(model.previousAttributes(), model.idAttribute); + var id = this.modelId(model.attributes, model.idAttribute); if (prevId !== id) { if (prevId != null) delete this._byId[prevId]; if (id != null) this._byId[id] = model; diff --git a/test/collection.js b/test/collection.js index 0c53db3790..7bdee03f61 100644 --- a/test/collection.js +++ b/test/collection.js @@ -1650,7 +1650,7 @@ // Default to using `model::idAttribute` if present. Stooge.prototype.idAttribute = '_id'; var model = new Stooge({_id: 1}); - assert.equal(StoogeCollection.prototype.modelId(model.attributes, model), 1); + assert.equal(StoogeCollection.prototype.modelId(model.attributes, model.idAttribute), 1); // Default to using `Collection::model::idAttribute` if model::idAttribute not present. StoogeCollection.prototype.model = Stooge; @@ -1735,11 +1735,11 @@ }); var c2 = new C2({'_id': 1}); assert.equal(c2.get(1), c2.at(0)); - assert.equal(c2.modelId(c2.at(0).attributes, c2.at(0)), 1); + assert.equal(c2.modelId(c2.at(0).attributes, c2.at(0).idAttribute), 1); var m = new M({'_id': 2}); c2.add(m); assert.equal(c2.get(2), m); - assert.equal(c2.modelId(m.attributes, m), 2); + assert.equal(c2.modelId(m.attributes, m.idAttribute), 2); }); QUnit.test('#3039 #3951: adding at index fires with correct at', function(assert) { From a5117079a108d3f4bca79555c1cf9a65d4dcc160 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Tue, 5 Jan 2016 10:21:30 -0500 Subject: [PATCH 03/69] fix modelId example again --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 0208b225ec..78b69690d9 100644 --- a/index.html +++ b/index.html @@ -1800,8 +1800,8 @@

Backbone.Collection

}); var library = new Library([ - {type: 'dvd', id: 1}, - {type: 'vhs', id: 1} + {type: 'dvd', dvd_id: 1}, + {type: 'vhs', dvd_id: 1} ]); var dvdId = library.get('dvd1').id; From 2d89d174911f637125e0d4984ab0143befa93c7a Mon Sep 17 00:00:00 2001 From: Craig Martin Date: Sat, 23 Jan 2016 22:47:20 -0500 Subject: [PATCH 04/69] cherry-pick 4caa6e68 to gh-pages, fix modelId example --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 78b69690d9..0208b225ec 100644 --- a/index.html +++ b/index.html @@ -1800,8 +1800,8 @@

Backbone.Collection

}); var library = new Library([ - {type: 'dvd', dvd_id: 1}, - {type: 'vhs', dvd_id: 1} + {type: 'dvd', id: 1}, + {type: 'vhs', id: 1} ]); var dvdId = library.get('dvd1').id; From b07faea900cc6876a1ebabfa2771541475bdda01 Mon Sep 17 00:00:00 2001 From: Adam Krebs Date: Tue, 5 Jan 2016 10:21:30 -0500 Subject: [PATCH 05/69] fix modelId example again --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 7d499040f6..b24b5233ed 100644 --- a/index.html +++ b/index.html @@ -1800,8 +1800,8 @@

Backbone.Collection

}); var library = new Library([ - {type: 'dvd', id: 1}, - {type: 'vhs', id: 1} + {type: 'dvd', dvd_id: 1}, + {type: 'vhs', dvd_id: 1} ]); var dvdId = library.get('dvd1').id; From 1853122b1480180aaad31123d08b6b9a908d9dae Mon Sep 17 00:00:00 2001 From: Craig Martin Date: Sat, 23 Jan 2016 22:47:20 -0500 Subject: [PATCH 06/69] cherry-pick 4caa6e68 to gh-pages, fix modelId example --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index b24b5233ed..7d499040f6 100644 --- a/index.html +++ b/index.html @@ -1800,8 +1800,8 @@

Backbone.Collection

}); var library = new Library([ - {type: 'dvd', dvd_id: 1}, - {type: 'vhs', dvd_id: 1} + {type: 'dvd', id: 1}, + {type: 'vhs', id: 1} ]); var dvdId = library.get('dvd1').id; From 402cc5b9f3897651f78ce2f535893faad9551106 Mon Sep 17 00:00:00 2001 From: Michael Rose Date: Sun, 11 Sep 2016 20:08:59 -0700 Subject: [PATCH 07/69] Add filtering to documentation --- docs/search.js | 58 +++++++ index.html | 452 ++++++++++++++++++++++++++----------------------- 2 files changed, 299 insertions(+), 211 deletions(-) create mode 100644 docs/search.js diff --git a/docs/search.js b/docs/search.js new file mode 100644 index 0000000000..97ae621366 --- /dev/null +++ b/docs/search.js @@ -0,0 +1,58 @@ +(function() { + var functions = document.querySelectorAll('[data-name]'); + var sections = document.querySelectorAll('.searchable_section'); + var searchInput = document.getElementById('function_filter'); + + function strIn(a, b) { + a = a.toLowerCase(); + b = b.toLowerCase(); + return b.indexOf(a) >= 0; + } + + function doesMatch(element) { + var name = element.getAttribute('data-name'); + var aliases = element.getAttribute('data-aliases') || ''; + return strIn(searchInput.value, name) || strIn(searchInput.value, aliases); + } + + function filterElement(element) { + element.style.display = doesMatch(element) ? '' : 'none'; + } + + function filterToc() { + _.each(functions, filterElement); + + var emptySearch = searchInput.value === ''; + + // Hide the titles of empty sections + _.each(sections, function(section) { + var sectionFunctions = section.querySelectorAll('[data-name]'); + var showSection = emptySearch || _.some(sectionFunctions, doesMatch); + section.style.display = showSection ? '' : 'none'; + }); + } + + function gotoFirst() { + var firstFunction = _.find(functions, doesMatch); + if(firstFunction) { + window.location.hash = firstFunction.lastChild.getAttribute('href'); + searchInput.focus(); + } + } + + searchInput.addEventListener('input', filterToc, false); + + // Press "Enter" to jump to the first matching function + searchInput.addEventListener('keypress', function(e) { + if (e.which === 13) { + gotoFirst(); + } + }); + + // Press "/" to search + document.body.addEventListener('keyup', function(event) { + if (191 === event.which) { + searchInput.focus(); + } + }); +}()); diff --git a/index.html b/index.html index 461d0231b3..aa1eabb8f7 100644 --- a/index.html +++ b/index.html @@ -61,6 +61,9 @@ .toc_section li a:hover { text-decoration: underline; } + input#function_filter { + width: 80%; + } div.container { position: relative; width: 550px; @@ -307,228 +310,254 @@
  • » Annotated Source
  • - - Getting Started - - + - - Events - - + - - Model - - +
    + + Events + + +
    - - Collection - - +
    + + Model + + +
    - - Router - - +
    + + Collection + + +
    - - History - - +
    + + Router + + +
    - - Sync - - +
    + + History + + +
    - - View - - + - - Utility - - +
    + + View + + +
    - - F.A.Q. - - + - - Examples - - + - - Change Log - +
    + + Examples + + +
    + + @@ -5066,6 +5095,7 @@

    Change Log

    + diff --git a/docs/examples/backbone.localStorage.html b/docs/examples/backbone.localStorage.html new file mode 100644 index 0000000000..01b7f5c8d0 --- /dev/null +++ b/docs/examples/backbone.localStorage.html @@ -0,0 +1,464 @@ + + + + + backbone.localStorage.js + + + + + +
    +
    + + + +
      + +
    • +
      +

      backbone.localStorage.js

      +
      +
    • + + + +
    • +
      + +
      + § +
      + +
      + +
      /**
      + * Backbone localStorage Adapter
      + * Version 1.1.0
      + *
      + * https://github.com/jeromegn/Backbone.localStorage
      + */
      +(function (root, factory) {
      +   if (typeof define === "function" && define.amd) {
      + +
    • + + +
    • +
      + +
      + § +
      +

      AMD. Register as an anonymous module.

      + +
      + +
            define(["underscore","backbone"], function(_, Backbone) {
      + +
    • + + +
    • +
      + +
      + § +
      +

      Use global variables if the locals are undefined.

      + +
      + +
              return factory(_ || root._, Backbone || root.Backbone);
      +      });
      +   } else {
      + +
    • + + +
    • +
      + +
      + § +
      +

      RequireJS isn’t being used. Assume underscore and backbone are loaded in script tags

      + +
      + +
            factory(_, Backbone);
      +   }
      +}(this, function(_, Backbone) {
      + +
    • + + +
    • +
      + +
      + § +
      +

      A simple module to replace Backbone.sync with localStorage-based +persistence. Models are given GUIDS, and saved into a JSON object. Simple +as that.

      + +
      + +
    • + + +
    • +
      + +
      + § +
      +

      Hold reference to Underscore.js and Backbone.js in the closure in order +to make things work even if they are removed from the global namespace

      + +
      + +
    • + + +
    • +
      + +
      + § +
      +

      Generate four random hex digits.

      + +
      + +
      function S4() {
      +   return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
      +};
      + +
    • + + +
    • +
      + +
      + § +
      +

      Generate a pseudo-GUID by concatenating random hexadecimal.

      + +
      + +
      function guid() {
      +   return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
      +};
      + +
    • + + +
    • +
      + +
      + § +
      +

      Our Store is represented by a single JS object in localStorage. Create it +with a meaningful name, like the name you’d give a table. +window.Store is deprecated, use Backbone.LocalStorage instead

      + +
      + +
      Backbone.LocalStorage = window.Store = function(name) {
      +  this.name = name;
      +  var store = this.localStorage().getItem(this.name);
      +  this.records = (store && store.split(",")) || [];
      +};
      +
      +_.extend(Backbone.LocalStorage.prototype, {
      + +
    • + + +
    • +
      + +
      + § +
      +

      Save the current state of the Store to localStorage.

      + +
      + +
        save: function() {
      +    this.localStorage().setItem(this.name, this.records.join(","));
      +  },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Add a model, giving it a (hopefully)-unique GUID, if it doesn’t already +have an id of it’s own.

      + +
      + +
        create: function(model) {
      +    if (!model.id) {
      +      model.id = guid();
      +      model.set(model.idAttribute, model.id);
      +    }
      +    this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
      +    this.records.push(model.id.toString());
      +    this.save();
      +    return this.find(model);
      +  },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Update a model by replacing its copy in this.data.

      + +
      + +
        update: function(model) {
      +    this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
      +    if (!_.include(this.records, model.id.toString()))
      +      this.records.push(model.id.toString()); this.save();
      +    return this.find(model);
      +  },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Retrieve a model from this.data by id.

      + +
      + +
        find: function(model) {
      +    return this.jsonData(this.localStorage().getItem(this.name+"-"+model.id));
      +  },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Return the array of all models currently in storage.

      + +
      + +
        findAll: function() {
      +    return _(this.records).chain()
      +      .map(function(id){
      +        return this.jsonData(this.localStorage().getItem(this.name+"-"+id));
      +      }, this)
      +      .compact()
      +      .value();
      +  },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Delete a model from this.data, returning it.

      + +
      + +
        destroy: function(model) {
      +    if (model.isNew())
      +      return false
      +    this.localStorage().removeItem(this.name+"-"+model.id);
      +    this.records = _.reject(this.records, function(id){
      +      return id === model.id.toString();
      +    });
      +    this.save();
      +    return model;
      +  },
      +
      +  localStorage: function() {
      +    return localStorage;
      +  },
      + +
    • + + +
    • +
      + +
      + § +
      +

      fix for “illegal access” error on Android when JSON.parse is passed null

      + +
      + +
        jsonData: function (data) {
      +      return data && JSON.parse(data);
      +  }
      +
      +});
      + +
    • + + +
    • +
      + +
      + § +
      +

      localSync delegate to the model or collection’s +localStorage property, which should be an instance of Store. +window.Store.sync and Backbone.localSync is deprectated, use Backbone.LocalStorage.sync instead

      + +
      + +
      Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options) {
      +  var store = model.localStorage || model.collection.localStorage;
      +
      +  var resp, errorMessage, syncDfd = $.Deferred && $.Deferred(); //If $ is having Deferred - use it.
      +
      +  try {
      +
      +    switch (method) {
      +      case "read":
      +        resp = model.id != undefined ? store.find(model) : store.findAll();
      +        break;
      +      case "create":
      +        resp = store.create(model);
      +        break;
      +      case "update":
      +        resp = store.update(model);
      +        break;
      +      case "delete":
      +        resp = store.destroy(model);
      +        break;
      +    }
      +
      +  } catch(error) {
      +    if (error.code === DOMException.QUOTA_EXCEEDED_ERR && window.localStorage.length === 0)
      +      errorMessage = "Private browsing is unsupported";
      +    else
      +      errorMessage = error.message;
      +  }
      +
      +  if (resp) {
      +    model.trigger("sync", model, resp, options);
      +    if (options && options.success)
      +      options.success(resp);
      +    if (syncDfd)
      +      syncDfd.resolve(resp);
      +
      +  } else {
      +    errorMessage = errorMessage ? errorMessage
      +                                : "Record Not Found";
      +
      +    if (options && options.error)
      +      options.error(errorMessage);
      +    if (syncDfd)
      +      syncDfd.reject(errorMessage);
      +  }
      + +
    • + + +
    • +
      + +
      + § +
      +

      add compatibility with $.ajax +always execute callback for success and error

      + +
      + +
        if (options && options.complete) options.complete(resp);
      +
      +  return syncDfd && syncDfd.promise();
      +};
      +
      +Backbone.ajaxSync = Backbone.sync;
      +
      +Backbone.getSyncMethod = function(model) {
      +  if(model.localStorage || (model.collection && model.collection.localStorage)) {
      +    return Backbone.localSync;
      +  }
      +
      +  return Backbone.ajaxSync;
      +};
      + +
    • + + +
    • +
      + +
      + § +
      +

      Override ‘Backbone.sync’ to default to localSync, +the original ‘Backbone.sync’ is still available in ‘Backbone.ajaxSync’

      + +
      + +
      Backbone.sync = function(method, model, options) {
      +  return Backbone.getSyncMethod(model).apply(this, [method, model, options]);
      +};
      +
      +return Backbone.LocalStorage;
      +}));
      + +
    • + +
    +
    + + diff --git a/docs/examples/todos/todos.html b/docs/examples/todos/todos.html new file mode 100644 index 0000000000..77e2e1ae46 --- /dev/null +++ b/docs/examples/todos/todos.html @@ -0,0 +1,791 @@ + + + + + todos.js + + + + + +
    +
    + + + +
      + +
    • +
      +

      todos.js

      +
      +
    • + + + +
    • +
      + +
      + § +
      +

      An example Backbone application contributed by +Jérôme Gravel-Niquet. This demo uses a simple +LocalStorage adapter +to persist Backbone models within your browser.

      + +
      + +
    • + + +
    • +
      + +
      + § +
      +

      Load the application once the DOM is ready, using jQuery.ready:

      + +
      + +
      $(function(){
      + +
    • + + +
    • +
      + +
      + § +
      +

      Todo Model

      + +
      + +
    • + + +
    • +
      + +
      + § +
      + +
      + +
    • + + +
    • +
      + +
      + § +
      +

      Our basic Todo model has title, order, and done attributes.

      + +
      + +
        var Todo = Backbone.Model.extend({
      + +
    • + + +
    • +
      + +
      + § +
      +

      Default attributes for the todo item.

      + +
      + +
          defaults: function() {
      +      return {
      +        title: "empty todo...",
      +        order: Todos.nextOrder(),
      +        done: false
      +      };
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Toggle the done state of this todo item.

      + +
      + +
          toggle: function() {
      +      this.save({done: !this.get("done")});
      +    }
      +
      +  });
      + +
    • + + +
    • +
      + +
      + § +
      +

      Todo Collection

      + +
      + +
    • + + +
    • +
      + +
      + § +
      + +
      + +
    • + + +
    • +
      + +
      + § +
      +

      The collection of todos is backed by localStorage instead of a remote +server.

      + +
      + +
        var TodoList = Backbone.Collection.extend({
      + +
    • + + +
    • +
      + +
      + § +
      +

      Reference to this collection’s model.

      + +
      + +
          model: Todo,
      + +
    • + + +
    • +
      + +
      + § +
      +

      Save all of the todo items under the "todos-backbone" namespace.

      + +
      + +
          localStorage: new Backbone.LocalStorage("todos-backbone"),
      + +
    • + + +
    • +
      + +
      + § +
      +

      Filter down the list of all todo items that are finished.

      + +
      + +
          done: function() {
      +      return this.where({done: true});
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Filter down the list to only todo items that are still not finished.

      + +
      + +
          remaining: function() {
      +      return this.where({done: false});
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      We keep the Todos in sequential order, despite being saved by unordered +GUID in the database. This generates the next order number for new items.

      + +
      + +
          nextOrder: function() {
      +      if (!this.length) return 1;
      +      return this.last().get('order') + 1;
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Todos are sorted by their original insertion order.

      + +
      + +
          comparator: 'order'
      +
      +  });
      + +
    • + + +
    • +
      + +
      + § +
      +

      Create our global collection of Todos.

      + +
      + +
        var Todos = new TodoList;
      + +
    • + + +
    • +
      + +
      + § +
      +

      Todo Item View

      + +
      + +
    • + + +
    • +
      + +
      + § +
      + +
      + +
    • + + +
    • +
      + +
      + § +
      +

      The DOM element for a todo item…

      + +
      + +
        var TodoView = Backbone.View.extend({
      + +
    • + + +
    • +
      + +
      + § +
      +

      … is a list tag.

      + +
      + +
          tagName:  "li",
      + +
    • + + +
    • +
      + +
      + § +
      +

      Cache the template function for a single item.

      + +
      + +
          template: _.template($('#item-template').html()),
      + +
    • + + +
    • +
      + +
      + § +
      +

      The DOM events specific to an item.

      + +
      + +
          events: {
      +      "click .toggle"   : "toggleDone",
      +      "dblclick .view"  : "edit",
      +      "click a.destroy" : "clear",
      +      "keypress .edit"  : "updateOnEnter",
      +      "blur .edit"      : "close"
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      The TodoView listens for changes to its model, re-rendering. Since there’s +a one-to-one correspondence between a Todo and a TodoView in this +app, we set a direct reference on the model for convenience.

      + +
      + +
          initialize: function() {
      +      this.listenTo(this.model, 'change', this.render);
      +      this.listenTo(this.model, 'destroy', this.remove);
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Re-render the titles of the todo item.

      + +
      + +
          render: function() {
      +      this.$el.html(this.template(this.model.toJSON()));
      +      this.$el.toggleClass('done', this.model.get('done'));
      +      this.input = this.$('.edit');
      +      return this;
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Toggle the "done" state of the model.

      + +
      + +
          toggleDone: function() {
      +      this.model.toggle();
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Switch this view into "editing" mode, displaying the input field.

      + +
      + +
          edit: function() {
      +      this.$el.addClass("editing");
      +      this.input.focus();
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Close the "editing" mode, saving changes to the todo.

      + +
      + +
          close: function() {
      +      var value = this.input.val();
      +      if (!value) {
      +        this.clear();
      +      } else {
      +        this.model.save({title: value});
      +        this.$el.removeClass("editing");
      +      }
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      If you hit enter, we’re through editing the item.

      + +
      + +
          updateOnEnter: function(e) {
      +      if (e.keyCode == 13) this.close();
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Remove the item, destroy the model.

      + +
      + +
          clear: function() {
      +      this.model.destroy();
      +    }
      +
      +  });
      + +
    • + + +
    • +
      + +
      + § +
      +

      The Application

      + +
      + +
    • + + +
    • +
      + +
      + § +
      + +
      + +
    • + + +
    • +
      + +
      + § +
      +

      Our overall AppView is the top-level piece of UI.

      + +
      + +
        var AppView = Backbone.View.extend({
      + +
    • + + +
    • +
      + +
      + § +
      +

      Instead of generating a new element, bind to the existing skeleton of +the App already present in the HTML.

      + +
      + +
          el: $("#todoapp"),
      + +
    • + + +
    • +
      + +
      + § +
      +

      Our template for the line of statistics at the bottom of the app.

      + +
      + +
          statsTemplate: _.template($('#stats-template').html()),
      + +
    • + + +
    • +
      + +
      + § +
      +

      Delegated events for creating new items, and clearing completed ones.

      + +
      + +
          events: {
      +      "keypress #new-todo":  "createOnEnter",
      +      "click #clear-completed": "clearCompleted",
      +      "click #toggle-all": "toggleAllComplete"
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      At initialization we bind to the relevant events on the Todos +collection, when items are added or changed. Kick things off by +loading any preexisting todos that might be saved in localStorage.

      + +
      + +
          initialize: function() {
      +
      +      this.input = this.$("#new-todo");
      +      this.allCheckbox = this.$("#toggle-all")[0];
      +
      +      this.listenTo(Todos, 'add', this.addOne);
      +      this.listenTo(Todos, 'reset', this.addAll);
      +      this.listenTo(Todos, 'all', this.render);
      +
      +      this.footer = this.$('footer');
      +      this.main = $('#main');
      +
      +      Todos.fetch();
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Re-rendering the App just means refreshing the statistics – the rest +of the app doesn’t change.

      + +
      + +
          render: function() {
      +      var done = Todos.done().length;
      +      var remaining = Todos.remaining().length;
      +
      +      if (Todos.length) {
      +        this.main.show();
      +        this.footer.show();
      +        this.footer.html(this.statsTemplate({done: done, remaining: remaining}));
      +      } else {
      +        this.main.hide();
      +        this.footer.hide();
      +      }
      +
      +      this.allCheckbox.checked = !remaining;
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Add a single todo item to the list by creating a view for it, and +appending its element to the <ul>.

      + +
      + +
          addOne: function(todo) {
      +      var view = new TodoView({model: todo});
      +      this.$("#todo-list").append(view.render().el);
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Add all items in the Todos collection at once.

      + +
      + +
          addAll: function() {
      +      Todos.each(this.addOne, this);
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      If you hit return in the main input field, create new Todo model, +persisting it to localStorage.

      + +
      + +
          createOnEnter: function(e) {
      +      if (e.keyCode != 13) return;
      +      if (!this.input.val()) return;
      +
      +      Todos.create({title: this.input.val()});
      +      this.input.val('');
      +    },
      + +
    • + + +
    • +
      + +
      + § +
      +

      Clear all done todo items, destroying their models.

      + +
      + +
          clearCompleted: function() {
      +      _.invoke(Todos.done(), 'destroy');
      +      return false;
      +    },
      +
      +    toggleAllComplete: function () {
      +      var done = this.allCheckbox.checked;
      +      Todos.each(function (todo) { todo.save({'done': done}); });
      +    }
      +
      +  });
      + +
    • + + +
    • +
      + +
      + § +
      +

      Finally, we kick things off by creating the App.

      + +
      + +
        var App = new AppView;
      +
      +});
      + +
    • + +
    +
    + + diff --git a/docs/todos.html b/docs/todos.html index 1ac4e52b49..225010684a 100644 --- a/docs/todos.html +++ b/docs/todos.html @@ -1,791 +1,13 @@ - - todos.js - - - - + -
    -
    - - - -
      - -
    • -
      -

      todos.js

      -
      -
    • - - - -
    • -
      - -
      - -
      -

      An example Backbone application contributed by -Jérôme Gravel-Niquet. This demo uses a simple -LocalStorage adapter -to persist Backbone models within your browser.

      - -
      - -
    • - - -
    • -
      - -
      - -
      -

      Load the application once the DOM is ready, using jQuery.ready:

      - -
      - -
      $(function(){
      - -
    • - - -
    • -
      - -
      - -
      -

      Todo Model

      - -
      - -
    • - - -
    • -
      - -
      - -
      - -
      - -
    • - - -
    • -
      - -
      - -
      -

      Our basic Todo model has title, order, and done attributes.

      - -
      - -
        var Todo = Backbone.Model.extend({
      - -
    • - - -
    • -
      - -
      - -
      -

      Default attributes for the todo item.

      - -
      - -
          defaults: function() {
      -      return {
      -        title: "empty todo...",
      -        order: Todos.nextOrder(),
      -        done: false
      -      };
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      Toggle the done state of this todo item.

      - -
      - -
          toggle: function() {
      -      this.save({done: !this.get("done")});
      -    }
      -
      -  });
      - -
    • - - -
    • -
      - -
      - -
      -

      Todo Collection

      - -
      - -
    • - - -
    • -
      - -
      - -
      - -
      - -
    • - - -
    • -
      - -
      - -
      -

      The collection of todos is backed by localStorage instead of a remote -server.

      - -
      - -
        var TodoList = Backbone.Collection.extend({
      - -
    • - - -
    • -
      - -
      - -
      -

      Reference to this collection’s model.

      - -
      - -
          model: Todo,
      - -
    • - - -
    • -
      - -
      - -
      -

      Save all of the todo items under the "todos-backbone" namespace.

      - -
      - -
          localStorage: new Backbone.LocalStorage("todos-backbone"),
      - -
    • - - -
    • -
      - -
      - -
      -

      Filter down the list of all todo items that are finished.

      - -
      - -
          done: function() {
      -      return this.where({done: true});
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      Filter down the list to only todo items that are still not finished.

      - -
      - -
          remaining: function() {
      -      return this.where({done: false});
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      We keep the Todos in sequential order, despite being saved by unordered -GUID in the database. This generates the next order number for new items.

      - -
      - -
          nextOrder: function() {
      -      if (!this.length) return 1;
      -      return this.last().get('order') + 1;
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      Todos are sorted by their original insertion order.

      - -
      - -
          comparator: 'order'
      -
      -  });
      - -
    • - - -
    • -
      - -
      - -
      -

      Create our global collection of Todos.

      - -
      - -
        var Todos = new TodoList;
      - -
    • - - -
    • -
      - -
      - -
      -

      Todo Item View

      - -
      - -
    • - - -
    • -
      - -
      - -
      - -
      - -
    • - - -
    • -
      - -
      - -
      -

      The DOM element for a todo item…

      - -
      - -
        var TodoView = Backbone.View.extend({
      - -
    • - - -
    • -
      - -
      - -
      -

      … is a list tag.

      - -
      - -
          tagName:  "li",
      - -
    • - - -
    • -
      - -
      - -
      -

      Cache the template function for a single item.

      - -
      - -
          template: _.template($('#item-template').html()),
      - -
    • - - -
    • -
      - -
      - -
      -

      The DOM events specific to an item.

      - -
      - -
          events: {
      -      "click .toggle"   : "toggleDone",
      -      "dblclick .view"  : "edit",
      -      "click a.destroy" : "clear",
      -      "keypress .edit"  : "updateOnEnter",
      -      "blur .edit"      : "close"
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      The TodoView listens for changes to its model, re-rendering. Since there’s -a one-to-one correspondence between a Todo and a TodoView in this -app, we set a direct reference on the model for convenience.

      - -
      - -
          initialize: function() {
      -      this.listenTo(this.model, 'change', this.render);
      -      this.listenTo(this.model, 'destroy', this.remove);
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      Re-render the titles of the todo item.

      - -
      - -
          render: function() {
      -      this.$el.html(this.template(this.model.toJSON()));
      -      this.$el.toggleClass('done', this.model.get('done'));
      -      this.input = this.$('.edit');
      -      return this;
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      Toggle the "done" state of the model.

      - -
      - -
          toggleDone: function() {
      -      this.model.toggle();
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      Switch this view into "editing" mode, displaying the input field.

      - -
      - -
          edit: function() {
      -      this.$el.addClass("editing");
      -      this.input.focus();
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      Close the "editing" mode, saving changes to the todo.

      - -
      - -
          close: function() {
      -      var value = this.input.val();
      -      if (!value) {
      -        this.clear();
      -      } else {
      -        this.model.save({title: value});
      -        this.$el.removeClass("editing");
      -      }
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      If you hit enter, we’re through editing the item.

      - -
      - -
          updateOnEnter: function(e) {
      -      if (e.keyCode == 13) this.close();
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      Remove the item, destroy the model.

      - -
      - -
          clear: function() {
      -      this.model.destroy();
      -    }
      -
      -  });
      - -
    • - - -
    • -
      - -
      - -
      -

      The Application

      - -
      - -
    • - - -
    • -
      - -
      - -
      - -
      - -
    • - - -
    • -
      - -
      - -
      -

      Our overall AppView is the top-level piece of UI.

      - -
      - -
        var AppView = Backbone.View.extend({
      - -
    • - - -
    • -
      - -
      - -
      -

      Instead of generating a new element, bind to the existing skeleton of -the App already present in the HTML.

      - -
      - -
          el: $("#todoapp"),
      - -
    • - - -
    • -
      - -
      - -
      -

      Our template for the line of statistics at the bottom of the app.

      - -
      - -
          statsTemplate: _.template($('#stats-template').html()),
      - -
    • - - -
    • -
      - -
      - -
      -

      Delegated events for creating new items, and clearing completed ones.

      - -
      - -
          events: {
      -      "keypress #new-todo":  "createOnEnter",
      -      "click #clear-completed": "clearCompleted",
      -      "click #toggle-all": "toggleAllComplete"
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      At initialization we bind to the relevant events on the Todos -collection, when items are added or changed. Kick things off by -loading any preexisting todos that might be saved in localStorage.

      - -
      - -
          initialize: function() {
      -
      -      this.input = this.$("#new-todo");
      -      this.allCheckbox = this.$("#toggle-all")[0];
      -
      -      this.listenTo(Todos, 'add', this.addOne);
      -      this.listenTo(Todos, 'reset', this.addAll);
      -      this.listenTo(Todos, 'all', this.render);
      -
      -      this.footer = this.$('footer');
      -      this.main = $('#main');
      -
      -      Todos.fetch();
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      Re-rendering the App just means refreshing the statistics – the rest -of the app doesn’t change.

      - -
      - -
          render: function() {
      -      var done = Todos.done().length;
      -      var remaining = Todos.remaining().length;
      -
      -      if (Todos.length) {
      -        this.main.show();
      -        this.footer.show();
      -        this.footer.html(this.statsTemplate({done: done, remaining: remaining}));
      -      } else {
      -        this.main.hide();
      -        this.footer.hide();
      -      }
      -
      -      this.allCheckbox.checked = !remaining;
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      Add a single todo item to the list by creating a view for it, and -appending its element to the <ul>.

      - -
      - -
          addOne: function(todo) {
      -      var view = new TodoView({model: todo});
      -      this.$("#todo-list").append(view.render().el);
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      Add all items in the Todos collection at once.

      - -
      - -
          addAll: function() {
      -      Todos.each(this.addOne, this);
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      If you hit return in the main input field, create new Todo model, -persisting it to localStorage.

      - -
      - -
          createOnEnter: function(e) {
      -      if (e.keyCode != 13) return;
      -      if (!this.input.val()) return;
      -
      -      Todos.create({title: this.input.val()});
      -      this.input.val('');
      -    },
      - -
    • - - -
    • -
      - -
      - -
      -

      Clear all done todo items, destroying their models.

      - -
      - -
          clearCompleted: function() {
      -      _.invoke(Todos.done(), 'destroy');
      -      return false;
      -    },
      -
      -    toggleAllComplete: function () {
      -      var done = this.allCheckbox.checked;
      -      Todos.each(function (todo) { todo.save({'done': done}); });
      -    }
      -
      -  });
      - -
    • - - -
    • -
      - -
      - -
      -

      Finally, we kick things off by creating the App.

      - -
      - -
        var App = new AppView;
      -
      -});
      - -
    • - -
    -
    +

    This annotated source has moved to examples/todos/todos.html. You will be automatically redirected in two seconds.

    + diff --git a/index.html b/index.html index 70b7d7bd90..3badf00300 100644 --- a/index.html +++ b/index.html @@ -3595,7 +3595,7 @@

    Examples

    Todo List application that is bundled in the repository as Backbone example. If you're wondering where to get started with Backbone in general, take a moment to - read through the annotated source. The app uses a + read through the annotated source. The app uses a LocalStorage adapter to transparently save all of your todos within your browser, instead of sending them to a server. Jérôme also has a version hosted at From dac55793f66009fd3dc0728c86cba02cb4bed80c Mon Sep 17 00:00:00 2001 From: Julian Gonggrijp Date: Sat, 26 Feb 2022 00:57:24 +0100 Subject: [PATCH 69/69] Add a note about changed filenames to the change log --- index.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/index.html b/index.html index 3badf00300..50c4e837db 100644 --- a/index.html +++ b/index.html @@ -4470,6 +4470,12 @@

    Change Log

  • Several improvements to the online documentation.
  • +
  • + Due to upgraded development tools, the annotated sources of the example + code as well as the sourcemap of the minified bundle have changed + filenames. Aliases and redirects in the old locations are kept for + backwards compatibility. +
  • 1.4.0Feb. 19, 2019