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
JavaScript
/*$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;