UNPKG

k.backbone.marionette

Version:
476 lines (390 loc) 14.6 kB
/* jshint maxstatements: 14 */ // Collection View // --------------- // A view that iterates over a Backbone.Collection // and renders an individual child view for each model. Marionette.CollectionView = Marionette.View.extend({ // used as the prefix for child view events // that are forwarded through the collectionview childViewEventPrefix: 'childview', // constructor // option to pass `{sort: false}` to prevent the `CollectionView` from // maintaining the sorted order of the collection. // This will fallback onto appending childView's to the end. constructor: function(options){ var initOptions = options || {}; this.sort = _.isUndefined(initOptions.sort) ? true : initOptions.sort; this.once('render', this._initialEvents); this._initChildViewStorage(); Marionette.View.apply(this, arguments); this.initRenderBuffer(); }, // Instead of inserting elements one by one into the page, // it's much more performant to insert elements into a document // fragment and then insert that document fragment into the page initRenderBuffer: function() { this.elBuffer = document.createDocumentFragment(); this._bufferedChildren = []; }, startBuffering: function() { this.initRenderBuffer(); this.isBuffering = true; }, endBuffering: function() { this.isBuffering = false; this._triggerBeforeShowBufferedChildren(); this.attachBuffer(this, this.elBuffer); this._triggerShowBufferedChildren(); this.initRenderBuffer(); }, _triggerBeforeShowBufferedChildren: function() { if (this._isShown) { _.each(this._bufferedChildren, _.partial(this._triggerMethodOnChild, 'before:show')); } }, _triggerShowBufferedChildren: function() { if (this._isShown) { _.each(this._bufferedChildren, _.partial(this._triggerMethodOnChild, 'show')); this._bufferedChildren = []; } }, // Internal method for _.each loops to call `Marionette.triggerMethodOn` on // a child view _triggerMethodOnChild: function(event, childView) { Marionette.triggerMethodOn(childView, event); }, // Configured the initial events that the collection view // binds to. _initialEvents: function() { if (this.collection) { this.listenTo(this.collection, 'add', this._onCollectionAdd); this.listenTo(this.collection, 'remove', this._onCollectionRemove); this.listenTo(this.collection, 'reset', this.render); if (this.sort) { this.listenTo(this.collection, 'sort', this._sortViews); } } }, // Handle a child added to the collection _onCollectionAdd: function(child) { this.destroyEmptyView(); var ChildView = this.getChildView(child); var index = this.collection.indexOf(child); this.addChild(child, ChildView, index); }, // get the child view by model it holds, and remove it _onCollectionRemove: function(model) { var view = this.children.findByModel(model); this.removeChildView(view); this.checkEmpty(); }, // Override from `Marionette.View` to trigger show on child views onShowCalled: function() { this.children.each(_.partial(this._triggerMethodOnChild, 'show')); }, // Render children views. Override this method to // provide your own implementation of a render function for // the collection view. render: function() { this._ensureViewIsIntact(); this.triggerMethod('before:render', this); this._renderChildren(); this.triggerMethod('render', this); return this; }, // Render view after sorting. Override this method to // change how the view renders after a `sort` on the collection. // An example of this would be to only `renderChildren` in a `CompositeView` // rather than the full view. resortView: function() { this.render(); }, // Internal method. This checks for any changes in the order of the collection. // If the index of any view doesn't match, it will render. _sortViews: function() { // check for any changes in sort order of views var orderChanged = this.collection.find(function(item, index){ var view = this.children.findByModel(item); return !view || view._index !== index; }, this); if (orderChanged) { this.resortView(); } }, // Internal method. Separated so that CompositeView can have // more control over events being triggered, around the rendering // process _renderChildren: function() { this.destroyEmptyView(); this.destroyChildren(); if (this.isEmpty(this.collection)) { this.showEmptyView(); } else { this.triggerMethod('before:render:collection', this); this.startBuffering(); this.showCollection(); this.endBuffering(); this.triggerMethod('render:collection', this); } }, // Internal method to loop through collection and show each child view. showCollection: function() { var ChildView; this.collection.each(function(child, index) { ChildView = this.getChildView(child); this.addChild(child, ChildView, index); }, this); }, // Internal method to show an empty view in place of // a collection of child views, when the collection is empty showEmptyView: function() { var EmptyView = this.getEmptyView(); if (EmptyView && !this._showingEmptyView) { this.triggerMethod('before:render:empty'); this._showingEmptyView = true; var model = new Backbone.Model(); this.addEmptyView(model, EmptyView); this.triggerMethod('render:empty'); } }, // Internal method to destroy an existing emptyView instance // if one exists. Called when a collection view has been // rendered empty, and then a child is added to the collection. destroyEmptyView: function() { if (this._showingEmptyView) { this.triggerMethod('before:remove:empty'); this.destroyChildren(); delete this._showingEmptyView; this.triggerMethod('remove:empty'); } }, // Retrieve the empty view class getEmptyView: function() { return this.getOption('emptyView'); }, // Render and show the emptyView. Similar to addChild method // but "child:added" events are not fired, and the event from // emptyView are not forwarded addEmptyView: function(child, EmptyView) { // get the emptyViewOptions, falling back to childViewOptions var emptyViewOptions = this.getOption('emptyViewOptions') || this.getOption('childViewOptions'); if (_.isFunction(emptyViewOptions)){ emptyViewOptions = emptyViewOptions.call(this); } // build the empty view var view = this.buildChildView(child, EmptyView, emptyViewOptions); // Proxy emptyView events this.proxyChildEvents(view); // trigger the 'before:show' event on `view` if the collection view // has already been shown if (this._isShown) { Marionette.triggerMethodOn(view, 'before:show'); } // Store the `emptyView` like a `childView` so we can properly // remove and/or close it later this.children.add(view); // Render it and show it this.renderChildView(view, -1); // call the 'show' method if the collection view // has already been shown if (this._isShown) { Marionette.triggerMethodOn(view, 'show'); } }, // Retrieve the `childView` class, either from `this.options.childView` // or from the `childView` in the object definition. The "options" // takes precedence. // This method receives the model that will be passed to the instance // created from this `childView`. Overriding methods may use the child // to determine what `childView` class to return. getChildView: function(child) { var childView = this.getOption('childView'); if (!childView) { throw new Marionette.Error({ name: 'NoChildViewError', message: 'A "childView" must be specified' }); } return childView; }, // Render the child's view and add it to the // HTML for the collection view at a given index. // This will also update the indices of later views in the collection // in order to keep the children in sync with the collection. addChild: function(child, ChildView, index) { var childViewOptions = this.getOption('childViewOptions'); if (_.isFunction(childViewOptions)) { childViewOptions = childViewOptions.call(this, child, index); } var view = this.buildChildView(child, ChildView, childViewOptions); // increment indices of views after this one this._updateIndices(view, true, index); this._addChildView(view, index); return view; }, // Internal method. This decrements or increments the indices of views after the // added/removed view to keep in sync with the collection. _updateIndices: function(view, increment, index) { if (!this.sort) { return; } if (increment) { // assign the index to the view view._index = index; // increment the index of views after this one this.children.each(function (laterView) { if (laterView._index >= view._index) { laterView._index++; } }); } else { // decrement the index of views after this one this.children.each(function (laterView) { if (laterView._index >= view._index) { laterView._index--; } }); } }, // Internal Method. Add the view to children and render it at // the given index. _addChildView: function(view, index) { // set up the child view event forwarding this.proxyChildEvents(view); this.triggerMethod('before:add:child', view); // Store the child view itself so we can properly // remove and/or destroy it later this.children.add(view); this.renderChildView(view, index); if (this._isShown && !this.isBuffering) { Marionette.triggerMethodOn(view, 'show'); } this.triggerMethod('add:child', view); }, // render the child view renderChildView: function(view, index) { view.render(); this.attachHtml(this, view, index); return view; }, // Build a `childView` for a model in the collection. buildChildView: function(child, ChildViewClass, childViewOptions) { var options = _.extend({model: child}, childViewOptions); return new ChildViewClass(options); }, // Remove the child view and destroy it. // This function also updates the indices of // later views in the collection in order to keep // the children in sync with the collection. removeChildView: function(view) { if (view) { this.triggerMethod('before:remove:child', view); // call 'destroy' or 'remove', depending on which is found if (view.destroy) { view.destroy(); } else if (view.remove) { view.remove(); } this.stopListening(view); this.children.remove(view); this.triggerMethod('remove:child', view); // decrement the index of views after this one this._updateIndices(view, false); } return view; }, // check if the collection is empty isEmpty: function() { return !this.collection || this.collection.length === 0; }, // If empty, show the empty view checkEmpty: function() { if (this.isEmpty(this.collection)) { this.showEmptyView(); } }, // You might need to override this if you've overridden attachHtml attachBuffer: function(collectionView, buffer) { collectionView.$el.append(buffer); }, // Append the HTML to the collection's `el`. // Override this method to do something other // than `.append`. attachHtml: function(collectionView, childView, index) { if (collectionView.isBuffering) { // buffering happens on reset events and initial renders // in order to reduce the number of inserts into the // document, which are expensive. collectionView.elBuffer.appendChild(childView.el); collectionView._bufferedChildren.push(childView); } else { // If we've already rendered the main collection, append // the new child into the correct order if we need to. Otherwise // append to the end. if (!collectionView._insertBefore(childView, index)){ collectionView._insertAfter(childView); } } }, // Internal method. Check whether we need to insert the view into // the correct position. _insertBefore: function(childView, index) { var currentView; var findPosition = this.sort && (index < this.children.length - 1); if (findPosition) { // Find the view after this one currentView = this.children.find(function (view) { return view._index === index + 1; }); } if (currentView) { currentView.$el.before(childView.el); return true; } return false; }, // Internal method. Append a view to the end of the $el _insertAfter: function(childView) { this.$el.append(childView.el); }, // Internal method to set up the `children` object for // storing all of the child views _initChildViewStorage: function() { this.children = new Backbone.ChildViewContainer(); }, // Handle cleanup and other destroying needs for the collection of views destroy: function() { if (this.isDestroyed) { return; } this.triggerMethod('before:destroy:collection'); this.destroyChildren(); this.triggerMethod('destroy:collection'); return Marionette.View.prototype.destroy.apply(this, arguments); }, // Destroy the child views that this collection view // is holding on to, if any destroyChildren: function() { var childViews = this.children.map(_.identity); this.children.each(this.removeChildView, this); this.checkEmpty(); return childViews; }, // Set up the child view event forwarding. Uses a "childview:" // prefix in front of all forwarded events. proxyChildEvents: function(view) { var prefix = this.getOption('childViewEventPrefix'); // Forward all child view events through the parent, // prepending "childview:" to the event name this.listenTo(view, 'all', function() { var args = slice.call(arguments); var rootEvent = args[0]; var childEvents = this.normalizeMethods(_.result(this, 'childEvents')); args[0] = prefix + ':' + rootEvent; args.splice(1, 0, view); // call collectionView childEvent if defined if (typeof childEvents !== 'undefined' && _.isFunction(childEvents[rootEvent])) { childEvents[rootEvent].apply(this, args.slice(1)); } this.triggerMethod.apply(this, args); }, this); } });