/* @date 05/31/2011 @class Backbone.Subset @name Backbone Subset @desc Implements an imaginary subset of a Backbone Collection (as superset) */ // Extend the default Backbone.Collection _.extend(Backbone.Collection.prototype, { build: function (attrs) { var model = new this.model(attrs); this.add(model); return model; }, merge: function (collection) { this.add(collection.models); return this } }); // Standard Constructor Backbone.Subset = function(options) { this.options = options || (options={}); // use the comparator supplied by the options if(options.comparator) { this.comparator = options.comparator; delete options.comparator; } if(!options.superset) { throw 'Subset must belong to a superset!'; } if(!options.filter) { throw 'Subset must have a filter'; } if(!(options.superset instanceof Backbone.Collection) && !(options.superset instanceof Backbone.Subset)) { throw "Subset must have Backbone.Collection or Backbone.Subset as its superset!"; } var self = this; // transform method, to be applied on models this.transform = options.transform || function(echo) { return echo; }; this.filter = options.filter; this.superset = options.superset; // hook on superset's events this.superset.bind("all", function(ev) { // TODO: CLEAN UP RESET!!! switch(ev) { case "add": case "remove": if(self.filter(arguments[1])) { // we are affected, forward events on this subset self._reset(); self.trigger.apply(self, arguments); } break; case "refresh": self._reset(); break; default: // model has changed, maybe it doesn't belong in this subset anymore if(ev.indexOf("change:") === 0) { // sub collection already has object so it could be removed if(self.getByCid(arguments[1])) { // maybe trigger remove if(!self.filter(arguments[1])) { self._reset(); self.trigger('remove', arguments[1], self); } else { // still in the set, forward event to this subset self._reset(); self.trigger.apply(self, arguments); } } // we got a new element, yay! if(!self.getByCid(arguments[1]) && self.filter(arguments[1])) { self._reset(); self.trigger('add', arguments[1], self); } } } }); // remove crucial entries from options delete options.filter delete options.superset; // get an event if a model changes this._boundOnModelEvent = _.bind(this._onModelEvent, this); // refresh the models this._reset(); // call custom constructor this.initialize(options); }; _.extend(Backbone.Subset.prototype, Backbone.Collection.prototype, { // array holding the models as json objects toJSON: function() { return this.map(function(c) { return c.toJSON(); }) }, // add models add: function(models, options) { var self = this; models = _.filter(models, this.filter); // return if no models resist if(models.length == 0) { return; } // actually add the models to the superset this.superset.add(models, options); return this; }, // remove models remove: function(models, options) { // remove model from superset this.superset.remove(_.filter(_.filter(models, function(cm) { return m != null; }), this.filter), options); }, // get a certain model by id! get: function(model_id) { return _.select(this.models, function(cm) { return cm.id == model_id; })[0] }, // get a certain model by cid ! getByCid: function(model_cid) { return _.select(this.models, function(cm) { return cm.cid == (model_cid.cid || model_cid); })[0] }, // get a model at a certain position in the _subset_ at: function(index) { return this.models[index] }, // sorting sort: function(options) { this.superset.sort(options); return this; }, // pluck an attribute from each model in the subset pluck: function(attr) { return _.map(this.models, function(model) { return model.get(m) }) }, // refresh the superset (triggers event to refresh this one too) refresh: function(models, options) { this.superset.refresh(models, options); return this; }, fetch: function(options) { this.superset.fetch(options); return this; }, create: function(model, options) { return this.superset.create(model, options); }, parse: function(resp) { return resp; }, length: function() { this._reset(); return this.models.length; }, chain: function() { return this.superset.chain(); }, // reset state and refresh the models _reset: function() { this.model = this.options.model || this.superset.model; this.models = this._models(); }, // get the models which belong to this collection _models: function() { // using internal filter method to filter the models that belong to this subset return _.filter(_.filter(this.transform(this.superset.models), function(cm) { return cm != null; }), this.filter); } }); var subsetMethods = ["forEach", "each", "map", "reduce", "reduceRight", "find", "detect", "filter", "select", "reject", "every", "all", "some", "any", "include", "invoke", "max", "min", "sortBy", "sortedIndex", "toArray", "size", "first", "rest", "last", "without", "indexOf", "lastIndexOf", "isEmpty"]; // add common function to this subset _.each(subsetMethods, function(cMethod) { Backbone.Subset.prototype[cMethod] = function() { return _[cMethod].apply(_, [this._models()].concat(_.toArray(arguments))) }; });