/static/js/models/livecollection.js
JavaScript | 289 lines | 124 code | 53 blank | 112 comment | 28 complexity | 1109cc61ec27b0a4675da7e82f926a9f MD5 | raw file
Possible License(s): Apache-2.0
- define([
- 'underscore',
- 'backbone',
- 'lib/promise',
- 'models/statemixin'
- ],
- function (util, Backbone, Promise, stateMixin) {
- 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 = util.extend(stateMixin, {
- // The last time the collection was loaded.
- loaded: undefined,
- // A 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 });
- //
- // Returns a `Promise` for the response value.
- // This implementation of fetch completely bypasses Backbone's fetch
- // because we want to be able to provide the response, without parsing,
- // to the success event. It does use `Backbone.sync`, though.
- fetch: function (options) {
- options = options || {};
- var isLoading = this.state() === 'loading';
-
- // If the collection is not already loading and in-flight, create
- // a new Promise for this fetch.
- if (!isLoading) this._promisedFetch = new Promise();
- // Just a quick reference to the promisedFetch.
- var promisedFetch = this._promisedFetch;
- // If success and error callbacks were provided, add them to the
- // `then` queue. At this point, promisedFetch is either the in-flight
- // promise, or the brand new promise.
- promisedFetch.then(options.success, options.error);
- // If another fetch is already in flight, return the promise for
- // that fetch. We don't need to issue a new fetch.
- if (isLoading) return promisedFetch;
- // Create a promise to represent the fetch.
- var collection = this,
- state = util.bind(this.state, this);
- // Set the state to loading -- this tells others that the collection is
- // in-flight.
- state('loading');
- // Trigger a fetch event -- useful for adding, then removing
- // loaders.
- if (!options.silent) this.trigger('fetch');
- // Redefine success/error handlers to manage promise.
- options.success = function (resp) {
- collection = collection.process(resp, options);
- // Set the load time.
- collection.loaded = Date.now();
- state('clean');
- promisedFetch.resolve(collection);
- };
- options.error = function (resp) {
- state('error');
- if (!options.silent) collection.trigger('error', resp, resp, options);
- promisedFetch.reject(resp);
- };
- (this.sync || Backbone.sync).call(this, 'read', this, options);
- return promisedFetch;
- },
- // Process a response object -- the object is usually the result of a
- // call to `fetch`, but may be passed to `process` directly.
- // Either way, the logical path for processing a response object should
- // be the same.
- process: function (resp, options) {
- options = options || {};
- var collection = this;
- if (options.diff) {
- // Set add to true.
- // Make sure we de-dupe by ID.
- options.add = options.unique = true;
- // Remove old models.
- collection.prune(resp, options);
- }
- // Notify other interested parties that we've had a value come
- // back from the server. Provide the value unparsed.
- if (!options.silent) collection.trigger('success', resp, options);
- // Use any passed `parse` option, or fall back to `this.parse`.
- resp = (options.parse || this.parse).call(this, resp);
- return collection[options.add ? 'add' : 'reset'](resp, options);
- },
-
- // A custom add function that can prevent models with duplicate IDs
- // from being added to the collection. Usage:
- //
- // myCollection.add([], { unique: true });
- //
- // Defaults to true.
- add: function(models, options) {
- options = options || {};
- util.defaults(options, {
- unique: true
- });
- this.state('dirty');
- // If unique option is set, don't add duplicate IDs.
- if (options.unique) {
- // Convert to array, if not array.
- if (!util.isArray(models)) models = [models];
- // Convert to true models if JSON is passed.
- // We're using an internal Backbone method; HIC SVNT DRACONES.
- models = util.map(models, this._prepareModel, this);
-
- models = util.reject(models, function (model) {
- // Exists in the collection already. Don't add a dupe.
- return !!( this.get(model.id) );
- }, this);
- }
- return Collection.prototype.add.call(this, models, 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 (resp, options) {
- // Process response -- we get the raw
- // results directly from Backbone.sync.
- var collection = this,
- parsedResp = this.parse(resp),
- // We assume the id attribute is defined by
- // the collection's model's idAttribute property.
- // This could fail if you don't specify a model type for
- // your collection AND you have a custom ID attribute.
- //
- // All this because the models
- idAttribute = this.model.prototype.idAttribute,
- respIDs, collectionIDs, oldModels;
- // Convert array of JSON model objects to array of IDs.
- respIDs = util.pluck(parsedResp, idAttribute);
- collectionIDs = collection.pluck(idAttribute);
-
- // Find the difference between the two...
- oldModels = util.difference(collectionIDs, respIDs);
-
- // ...and remove it from the collection
- // (remove can take IDs or objects).
- collection.remove(oldModels);
- return collection;
- },
-
- // 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 = util.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 = util.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 util.isUndefined(this.pollTimeout);
- },
- // Move a model object to a specified index. Fires a `move` event, handing
- // you the model, collection and options. Options contains index and
- // previous index.
- move: function (model, options) {
- options = options || {};
- var fromIndex = this.indexOf(model),
- toIndex = options.at,
- silently = { silent:true };
- // Throw an error if the model does not exist in the collection.
- // TODO: whould we throw an error if the model does not exist in
- // the collection?
- if (fromIndex === -1) throw new Error(
- 'Model does not exist in collection. Use add method instead.'
- );
- // No index provided, or same index? Do nothing.
- if (!util.isNumber(toIndex) || fromIndex === toIndex) return this;
- // Move the model silently.
- this.remove(model, silently);
- this.add(model, util.extend({ at: toIndex }, silently));
- // Trigger a `move` event, passing `index` and `fromIndex`.
- this.trigger('move', model, this, {
- index: toIndex,
- fromIndex: fromIndex
- });
- return this;
- },
- // DEPRECATED. Please use `move`, above.
- // Given a model for reference, add it to the collection, if it doesn't
- // already exist. If it does exist, remove it from it's old place
- // and *then* add it.
- put: function (model, options) {
- var existsAt = this.indexOf(model),
- atIsNumber = options && util.isNumber(options.at);
- // If you've requested the existing model be moved, and that
- // model already exists at the specified location, do nothing.
- if (atIsNumber && existsAt === options.at) return this;
- // If the model already exists in the collection, you've requested to
- // it be moved, remove it.
- if (existsAt !== -1 && atIsNumber) this.remove(
- model,
- { silent: (options && options.silent) }
- );
- // If the model does not exist in the collection, add it.
- // OR, if the model DOES exist, and you have requested it be moved,
- // add it.
- if (existsAt === -1 || atIsNumber) this.add(model, options);
- return this;
- }
- });
-
- // Extend Backbone.collection with mixed __LiveCollection object.
- var LiveCollection = Collection.extend(__LiveCollection);
- return LiveCollection;
- });