UNPKG

ampersand-collection-view

Version:

Renders a collection with one view per model within an element in a way that cleans up and unbinds all views when removed.

156 lines (144 loc) 5.54 kB
/*$AMPERSAND_VERSION*/ var assign = require('lodash/assign'); var invokeMap = require('lodash/invokeMap'); var pick = require('lodash/pick'); var find = require('lodash/find'); var difference = require('lodash/difference'); var bind = require('lodash/bind'); var Events = require('ampersand-events'); var ampExtend = require('ampersand-class-extend'); // options var options = ['collection', 'el', 'viewOptions', 'view', 'emptyView', 'filter', 'reverse', 'parent']; function CollectionView(spec) { if (!spec) { throw new ReferenceError('Collection view missing required parameters: collection, el'); } if (!spec.collection) { throw new ReferenceError('Collection view requires a collection'); } if (!spec.el && !this.insertSelf) { throw new ReferenceError('Collection view requires an el'); } assign(this, pick(spec, options)); this.views = []; this.listenTo(this.collection, 'add', this._addViewForModel); this.listenTo(this.collection, 'remove', this._removeViewForModel); this.listenTo(this.collection, 'sort', this._rerenderAll); this.listenTo(this.collection, 'refresh reset', this._reset); } assign(CollectionView.prototype, Events, { // for view contract compliance render: function () { this._renderAll(); return this; }, remove: function () { invokeMap(this.views, 'remove'); this.stopListening(); }, _getViewByModel: function (model) { return find(this.views, function (view) { return model === view.model; }); }, _createViewForModel: function (model, renderOpts) { var defaultViewOptions = {model: model, collection: this.collection, parent: this}; var view = new this.view(assign(defaultViewOptions, this.viewOptions)); this.views.push(view); view.renderedByParentView = true; view.render(renderOpts); return view; }, _getOrCreateByModel: function (model, renderOpts) { return this._getViewByModel(model) || this._createViewForModel(model, renderOpts); }, _addViewForModel: function (model, collection, options) { var matches = this.filter ? this.filter(model) : true; if (!matches) { return; } if (this.renderedEmptyView) { this._removeView(this.renderedEmptyView); delete this.renderedEmptyView; } var view = this._getOrCreateByModel(model, {containerEl: this.el}); if (options && options.rerender) { this._insertView(view); } else { this._insertViewAtIndex(view); } }, _insertViewAtIndex: function (view) { if (!view.insertSelf) { var pos = this.collection.indexOf(view.model); pos = this.reverse ? pos - 1 : pos + 1; var modelToInsertBefore = this.collection.at(pos); var viewToInsertBefore = this._getViewByModel(modelToInsertBefore); // FIX IE bug (https://developer.mozilla.org/en-US/docs/Web/API/Node.insertBefore) // "In Internet Explorer an undefined value as referenceElement will throw errors, while in rest of the modern browsers, this works fine." if (viewToInsertBefore) { this.el.insertBefore(view.el, viewToInsertBefore && viewToInsertBefore.el); } else { this.el.appendChild(view.el); } } }, _insertView: function (view) { if (!view.insertSelf) { if (this.reverse && this.el.firstChild) { this.el.insertBefore(view.el, this.el.firstChild); } else { this.el.appendChild(view.el); } } }, _removeViewForModel: function (model) { var view = this._getViewByModel(model); if (!view) { return; } var index = this.views.indexOf(view); if (index !== -1) { // remove it if we found it calling animateRemove // to give user option of gracefully destroying. view = this.views.splice(index, 1)[0]; this._removeView(view); this._renderEmptyView(); } }, _removeView: function (view) { if (view.animateRemove) { view.animateRemove(); } else { view.remove(); } }, _renderAll: function () { this.collection.each(bind(this._addViewForModel, this)); this._renderEmptyView(); }, _rerenderAll: function (collection, options) { options = options || {}; this.collection.each(bind(function (model) { this._addViewForModel(model, this, assign(options, {rerender: true})); }, this)); }, _renderEmptyView: function() { if (this.views.length === 0 && this.emptyView && !this.renderedEmptyView) { this.renderedEmptyView = new this.emptyView({parent: this}); this.el.appendChild(this.renderedEmptyView.render().el); } }, _reset: function () { var newViews = this.collection.map(bind(this._getOrCreateByModel, this)); //Remove existing views from the ui var toRemove = difference(this.views, newViews); toRemove.forEach(this._removeView, this); //Rerender the full list with the new views this.views = newViews; this._rerenderAll(); this._renderEmptyView(); } }); CollectionView.extend = ampExtend; module.exports = CollectionView;