/src/collection.js
JavaScript | 334 lines | 290 code | 18 blank | 26 comment | 73 complexity | 201d300c957709698b27789ca5d5bf2e MD5 | raw file
- /*global createRegistryWrapper, dataObject, getEventCallback, getValue, modelCidAttributeName, viewCidAttributeName */
- var _fetch = Backbone.Collection.prototype.fetch,
- _reset = Backbone.Collection.prototype.reset,
- _replaceHTML = Thorax.View.prototype._replaceHTML,
- collectionCidAttributeName = 'data-collection-cid',
- collectionEmptyAttributeName = 'data-collection-empty',
- collectionElementAttributeName = 'data-collection-element',
- ELEMENT_NODE_TYPE = 1;
- Thorax.Collection = Backbone.Collection.extend({
- model: Thorax.Model || Backbone.Model,
- initialize: function() {
- this.cid = _.uniqueId('collection');
- return Backbone.Collection.prototype.initialize.apply(this, arguments);
- },
- isEmpty: function() {
- if (this.length > 0) {
- return false;
- } else {
- return this.length === 0 && this.isPopulated();
- }
- },
- isPopulated: function() {
- return this._fetched || this.length > 0 || (!this.length && !getValue(this, 'url'));
- },
- shouldFetch: function(options) {
- return options.fetch && !!getValue(this, 'url') && !this.isPopulated();
- },
- fetch: function(options) {
- options = options || {};
- var success = options.success;
- options.success = function(collection, response) {
- collection._fetched = true;
- success && success(collection, response);
- };
- return _fetch.apply(this, arguments);
- },
- reset: function(models, options) {
- this._fetched = !!models;
- return _reset.call(this, models, options);
- }
- });
- Thorax.Collections = {};
- createRegistryWrapper(Thorax.Collection, Thorax.Collections);
- dataObject('collection', {
- set: 'setCollection',
- bindCallback: onSetCollection,
- defaultOptions: {
- render: true,
- fetch: true,
- success: false,
- errors: true
- },
- change: onCollectionReset,
- $el: 'getCollectionElement',
- cidAttrName: collectionCidAttributeName
- });
- Thorax.CollectionView = Thorax.View.extend({
- _defaultTemplate: Handlebars.VM.noop,
- _collectionSelector: '[' + collectionElementAttributeName + ']',
- // preserve collection element if it was not created with {{collection}} helper
- _replaceHTML: function(html) {
- if (this.collection && this._objectOptionsByCid[this.collection.cid] && this._renderCount) {
- var element;
- var oldCollectionElement = this.getCollectionElement();
- element = _replaceHTML.call(this, html);
- if (!oldCollectionElement.attr('data-view-cid')) {
- this.getCollectionElement().replaceWith(oldCollectionElement);
- }
- } else {
- return _replaceHTML.call(this, html);
- }
- },
- //appendItem(model [,index])
- //appendItem(html_string, index)
- //appendItem(view, index)
- appendItem: function(model, index, options) {
- //empty item
- if (!model) {
- return;
- }
- var itemView,
- $el = this.getCollectionElement();
- options = _.defaults(options || {}, {
- filter: true
- });
- //if index argument is a view
- index && index.el && (index = $el.children().indexOf(index.el) + 1);
- //if argument is a view, or html string
- if (model.el || _.isString(model)) {
- itemView = model;
- model = false;
- } else {
- index = index || this.collection.indexOf(model) || 0;
- itemView = this.renderItem(model, index);
- }
- if (itemView) {
- itemView.cid && this._addChild(itemView);
- //if the renderer's output wasn't contained in a tag, wrap it in a div
- //plain text, or a mixture of top level text nodes and element nodes
- //will get wrapped
- if (_.isString(itemView) && !itemView.match(/^\s*</m)) {
- itemView = '<div>' + itemView + '</div>';
- }
- var itemElement = itemView.el ? [itemView.el] : _.filter($($.trim(itemView)), function(node) {
- //filter out top level whitespace nodes
- return node.nodeType === ELEMENT_NODE_TYPE;
- });
- model && $(itemElement).attr(modelCidAttributeName, model.cid);
- var previousModel = index > 0 ? this.collection.at(index - 1) : false;
- if (!previousModel) {
- $el.prepend(itemElement);
- } else {
- //use last() as appendItem can accept multiple nodes from a template
- var last = $el.children('[' + modelCidAttributeName + '="' + previousModel.cid + '"]').last();
- last.after(itemElement);
- }
- this.trigger('append', null, function(el) {
- el.setAttribute(modelCidAttributeName, model.cid);
- });
- !options.silent && this.trigger('rendered:item', this, this.collection, model, itemElement, index);
- options.filter && applyItemVisiblityFilter.call(this, model);
- }
- return itemView;
- },
- //Â updateItem only useful if there is no item view, otherwise
- //Â itemView.render() provides the same functionality
- updateItem: function(model) {
- this.removeItem(model);
- this.appendItem(model);
- },
- removeItem: function(model) {
- var $el = this.getCollectionElement(),
- viewEl = $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]');
- if (!viewEl.length) {
- return false;
- }
- viewEl.remove();
- var viewCid = viewEl.attr(viewCidAttributeName),
- child = this.children[viewCid];
- if (child) {
- this._removeChild(child);
- child.destroy();
- }
- return true;
- },
- renderCollection: function() {
- if (this.collection) {
- if (this.collection.isEmpty()) {
- handleChangeFromNotEmptyToEmpty.call(this);
- } else {
- handleChangeFromEmptyToNotEmpty.call(this);
- this.collection.forEach(function(item, i) {
- this.appendItem(item, i);
- }, this);
- }
- this.trigger('rendered:collection', this, this.collection);
- applyVisibilityFilter.call(this);
- } else {
- handleChangeFromNotEmptyToEmpty.call(this);
- }
- },
- emptyClass: 'empty',
- renderEmpty: function() {
- if (!this.emptyTemplate && !this.emptyView) {
- assignTemplate.call(this, 'emptyTemplate', {
- extension: '-empty',
- required: false
- });
- }
- if (this.emptyView) {
- var viewOptions = {};
- if (this.emptyTemplate) {
- viewOptions.template = this.emptyTemplate;
- }
- var view = Thorax.Util.getViewInstance(this.emptyView, viewOptions);
- view.ensureRendered();
- return view;
- } else {
- return this.emptyTemplate && this.renderTemplate(this.emptyTemplate);
- }
- },
- renderItem: function(model, i) {
- if (!this.itemTemplate && !this.itemView) {
- assignTemplate.call(this, 'itemTemplate', {
- extension: '-item',
- // only require an itemTemplate if an itemView
- // is not present
- required: !this.itemView
- });
- }
- if (this.itemView) {
- var viewOptions = {
- model: model
- };
- if (this.itemTemplate) {
- viewOptions.template = this.itemTemplate;
- }
- var view = Thorax.Util.getViewInstance(this.itemView, viewOptions);
- view.ensureRendered();
- return view;
- } else {
- return this.renderTemplate(this.itemTemplate, this.itemContext(model, i));
- }
- },
- itemContext: function(model /*, i */) {
- return model.attributes;
- },
- appendEmpty: function() {
- var $el = this.getCollectionElement();
- $el.empty();
- var emptyContent = this.renderEmpty();
- emptyContent && this.appendItem(emptyContent, 0, {
- silent: true,
- filter: false
- });
- this.trigger('rendered:empty', this, this.collection);
- },
- getCollectionElement: function() {
- var element = this.$(this._collectionSelector);
- return element.length === 0 ? this.$el : element;
- }
- });
- Thorax.CollectionView.on({
- collection: {
- reset: onCollectionReset,
- sort: onCollectionReset,
- filter: function() {
- applyVisibilityFilter.call(this);
- },
- change: function(model) {
- // If we rendered with item views, model changes will be observed
- // by the generated item view but if we rendered with templates
- // then model changes need to be bound as nothing is watching
- !this.itemView && this.updateItem(model);
- applyItemVisiblityFilter.call(this, model);
- },
- add: function(model) {
- var $el = this.getCollectionElement();
- this.collection.length === 1 && $el.length && handleChangeFromEmptyToNotEmpty.call(this);
- if ($el.length) {
- var index = this.collection.indexOf(model);
- this.appendItem(model, index);
- }
- },
- remove: function(model) {
- var $el = this.getCollectionElement();
- this.removeItem(model);
- this.collection.length === 0 && $el.length && handleChangeFromNotEmptyToEmpty.call(this);
- }
- }
- });
- Thorax.View.on({
- collection: {
- error: function(collection, message) {
- if (this._objectOptionsByCid[collection.cid].errors) {
- this.trigger('error', message, collection);
- }
- }
- }
- });
- function onCollectionReset(collection) {
- var options = collection && this._objectOptionsByCid[collection.cid];
- // we would want to still render in the case that the
- // collection has transitioned to being falsy
- if (!collection || (options && options.render)) {
- this.renderCollection && this.renderCollection();
- }
- }
- // Even if the view is not a CollectionView
- // ensureRendered() to provide similar behavior
- // to a model
- function onSetCollection() {
- this.ensureRendered();
- }
- function applyVisibilityFilter() {
- if (this.itemFilter) {
- this.collection.forEach(function(model) {
- applyItemVisiblityFilter.call(this, model);
- }, this);
- }
- }
- function applyItemVisiblityFilter(model) {
- var $el = this.getCollectionElement();
- this.itemFilter && $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]')[itemShouldBeVisible.call(this, model) ? 'show' : 'hide']();
- }
- function itemShouldBeVisible(model) {
- return this.itemFilter(model, this.collection.indexOf(model));
- }
- function handleChangeFromEmptyToNotEmpty() {
- var $el = this.getCollectionElement();
- this.emptyClass && $el.removeClass(this.emptyClass);
- $el.removeAttr(collectionEmptyAttributeName);
- $el.empty();
- }
- function handleChangeFromNotEmptyToEmpty() {
- var $el = this.getCollectionElement();
- this.emptyClass && $el.addClass(this.emptyClass);
- $el.attr(collectionEmptyAttributeName, true);
- this.appendEmpty();
- }
- //$(selector).collection() helper
- $.fn.collection = function(view) {
- if (view && view.collection) {
- return view.collection;
- }
- var $this = $(this),
- collectionElement = $this.closest('[' + collectionCidAttributeName + ']'),
- collectionCid = collectionElement && collectionElement.attr(collectionCidAttributeName);
- if (collectionCid) {
- view = $this.view();
- if (view) {
- return view.collection;
- }
- }
- return false;
- };