Skip to content

Instantly share code, notes, and snippets.

@gordonbrander
Created December 27, 2011 19:40
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save gordonbrander/1524919 to your computer and use it in GitHub Desktop.
Save gordonbrander/1524919 to your computer and use it in GitHub Desktop.
Backbone Live Collections w/ Streaming and Automatic Duplicate Handling
var LiveCollection = (function (_, Backbone) {
var Collection = Backbone.Collection;
// Define a Backbone Collection handling the details of running a "live"
// collection. Live collections are expected to handle fetching their own
// data, rather than being composed from separate models.
// They typically add new models instead of resetting the collection.
// A custom add comparison makes sure that duplicate models are not
// added. End result: only new elements will be added, instead
// of redrawing the whole collection.
//
// myCollection.fetch({ diff: true }); // adds and avoids duplicates
// myCollection.fetch(); // resets
//
// Thanks to this Bocoup article:
// <http://weblog.bocoup.com/backbone-live-collections>
var LiveCollection = Collection.extend({
// An specialized fetch that can add new models and remove
// outdated ones. Models in response that are already in the
// collection are left alone. Usage:
//
// myCollection.fetch({ diff: true });
fetch: function (options) {
options = options || {};
if (options.diff) {
var success = options.success,
prune = _.bind(this.prune, this);
// Add new models, rather than resetting.
options.add = true;
// Wrap the success callback, adding a pruning
// step after fetching.
options.success = function (collection, resp) {
prune(collection, resp);
if (success) success(collection, resp);
};
}
// Delegate to original fetch method.
Collection.prototype.fetch.call(this, options);
},
// A custom add function that can prevent models with duplicate IDs
// from being added to the collection. Usage:
//
// myCollection.add({ unique: true });
add: function(models, options) {
var modelsToAdd = models;
// If a single model is passed in, convert it to an
// array so we can use the same logic for both cases
// below.
if (!_.isArray(models)) models = [models];
options = _.extend({
unique: true
}, options);
// If unique option is set, don't add duplicate IDs.
if (options.unique) {
modelsToAdd = [];
_.each(models, function(model) {
if ( _.isUndefined( this.get(model.id) ) ) {
modelsToAdd.push(model);
}
}, this);
}
return Collection.prototype.add.call(this, modelsToAdd, options);
},
// Weed out old models in collection, that are no longer being returned
// by the endpoint. Typically used as a callback for this.fetch's
// success option.
prune: function (collection, resp) {
// Process response -- we get the raw
// results directly from Backbone.sync.
var parsedResp = this.parse(resp),
modelToID = function (model) { return model.id; },
respIDs, collectionIDs, oldModels;
// Convert array of JSON model objects to array of IDs.
respIDs = _.map(parsedResp, modelToID);
collectionIDs = _.map(collection.toJSON(), modelToID);
// Find the difference between the two...
oldModels = _.difference(collectionIDs, respIDs);
// ...and remove it from the collection
// (remove can take IDs or objects).
collection.remove(oldModels);
},
// Poll this collection's endpoint.
// Options:
//
// * `interval`: time between polls, in milliseconds.
// * `tries`: the maximum number of polls for this stream.
stream: function(options) {
var polledCount = 0;
// Cancel any potential previous stream.
this.unstream();
var update = _.bind(function() {
// Make a shallow copy of the options object.
// `Backbone.collection.fetch` wraps the success function
// in an outer function (line `527`), replacing options.success.
// That means if we don't copy the object every poll, we'll end
// up modifying the reference object and creating callback inception.
//
// Furthermore, since the sync success wrapper
// that wraps and replaces options.success has a different arguments
// order, you'll end up getting the wrong arguments.
var opts = _.clone(options);
if (!opts.tries || polledCount < opts.tries) {
polledCount = polledCount + 1;
this.fetch(opts);
this.pollTimeout = setTimeout(update, opts.interval || 1000);
}
}, this);
update();
},
// Stop polling.
unstream: function() {
clearTimeout(this.pollTimeout);
delete this.pollTimeout;
},
isStreaming : function() {
return _.isUndefined(this.pollTimeout);
}
});
return LiveCollection;
})(_, Backbone);
@gordonbrander
Copy link
Author

A custom Backbone.Collection extension based on ideas from http://weblog.bocoup.com/backbone-live-collections. Gives you:

  • myLiveCollection.fetch({ update: true })
    When passed this option, fetch will:
    • add new models instead of resetting collection.
    • Avoid adding models that have the same ID as models already in the collection.
    • Remove models who's IDs are not present in the fetch response.
  • myLiveCollection.add([], { unique: true}) (default)
    When unique is set to true, the add method will not add a model if a model with the same ID is already present in the collection (prevents duplicate IDs).
  • myLiveCollection.stream({ interval: 1000 }), myLiveCollection.unstream(), myLiveCollection.isStreaming()
    Helpers for polling using fetch.

@tracend
Copy link

tracend commented Feb 8, 2012

I had trouble using the custom add() method because it doesn't support passing one element (not an array).

The default add method() considers that and converts it into a one element array.

This is the modified add method I'm using:

    add: function(models, options) {

      var modelsToAdd = [];

      // add in an array if only one item
      models = _.isArray(models) ? models.slice() : [models];

      // Don't add duplicate IDs by default (can be overridden).
      options = _.extend({ unique: true }, options);

      if (options.unique) {
        _.each(models, function(model) {
          if ( _.isUndefined( this.get(model.id) ) ) {
            modelsToAdd.push(model);
          }
        }, this);
      }

      return Collection.prototype.add.call(this, modelsToAdd, options);
    },

@gordonbrander
Copy link
Author

Great point @tracend. I've updated the snippet to fix this.

Copy link

ghost commented Jan 20, 2013

Thank you for the code. Unfortunately I can't figure out, how to change it, if I need to update an existing item.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment