Created
December 27, 2011 19:40
-
-
Save gordonbrander/1524919 to your computer and use it in GitHub Desktop.
Backbone Live Collections w/ Streaming and Automatic Duplicate Handling
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
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);
},
Great point @tracend. I've updated the snippet to fix this.
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
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.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.