Skip to content

Commit

Permalink
(#1596) Add 'filter' option to the CollectionView
Browse files Browse the repository at this point in the history
The filter option will allow CollectionViews to only show some of their models. This takes
into account the new reorder on sort code, as well as sorting in
general.
  • Loading branch information
cmaher authored and samccone committed Feb 23, 2015
1 parent 9ccd6eb commit 4c5ef01
Show file tree
Hide file tree
Showing 5 changed files with 517 additions and 19 deletions.
1 change: 1 addition & 0 deletions SpecRunner.html
Expand Up @@ -102,6 +102,7 @@
<script src="test/unit/collection-view.empty-view.spec.js"></script>
<script src="test/unit/collection-view.item-view-options.spec.js"></script>
<script src="test/unit/collection-view.reset.spec.js"></script>
<script src="test/unit/collection-view.filter.spec.js"></script>
<script src="test/unit/collection-view.spec.js"></script>
<script src="test/unit/commands.spec.js"></script>
<script src="test/unit/composite-view.child-view-container.spec.js"></script>
Expand Down
11 changes: 10 additions & 1 deletion api/collection-view.yaml
Expand Up @@ -323,4 +323,13 @@ functions:
@api public
@param {Marionette.View} view
filter:
description: |
...
@api public
@param {Backbone.Model} model
@param {Number} index
@param {Backbone.Collection} collection
49 changes: 49 additions & 0 deletions docs/marionette.collectionview.md
Expand Up @@ -58,6 +58,7 @@ will provide features such as `onShow` callbacks, etc. Please see
* [CollectionView's attachHtml](#collectionviews-attachhtml)
* [CollectionView's resortView](#collectionviews-resortview)
* [CollectionView's viewComparator](#collectionviews-viewcomparator)
* [CollectionView's `filter`](#collectionviews-filter)
* [CollectionView's children](#collectionviews-children)
* [CollectionView destroy](#collectionview-destroy)

Expand Down Expand Up @@ -302,6 +303,9 @@ will be reordered. This can be a problem if your `ChildView`s use their collecti
in their rendering. In this case, you cannot use this option as you need to re-render each
`ChildView`.
If you combine this option with a [filter](#collectionviews-filter) that changes the views that are
to be displayed, `reorderOnSort` will be bypassed to render new children and remove those that are rejected by the filter.
## CollectionView's `emptyView`
When a collection has no children, and you need to render a view other than
Expand Down Expand Up @@ -848,6 +852,51 @@ CollectionView allows for a custom `viewComparator` option if you want your Coll
The `viewComparator` can take any of the acceptable `Backbone.Collection` [comparator formats](http://backbonejs.org/#Collection-comparator) -- a sortBy (pass a function that takes a single argument), as a sort (pass a comparator function that expects two arguments), or as a string indicating the attribute to sort by.
## CollectionView's `filter`
CollectionView allows for a custom `filter` option if you want to prevent some of the
underlying `collection`'s models from being rendered as child views.
The filter function takes a model from the collection and returns a truthy value if the child should be rendered,
and a falsey value if it should not.
```js
var cv = new Marionette.CollectionView({
childView: SomeChildView,
emptyView: SomeEmptyView,
collection: new Backbone.Collection([
{ value: 1 },
{ value: 2 },
{ value: 3 },
{ value: 4 }
]),

// Only show views with even values
filter: function (child, index, collection) {
return child.get('value') % 2 === 0;
}
});

// renders the views with values '2' and '4'
cv.render();

// change the filter
cv.filter = function (child, index, collection) {
return child.get('value') % 2 !== 0;
};

// renders the views with values '1' and '3'
cv.render();

// remove the filter
// note that using `delete cv.filter` will cause the prototype's filter to be used
// which may be undesirable
cv.filter = null;

// renders all views
cv.render();
```
## CollectionView's children
The CollectionView uses [Backbone.BabySitter](https://github.com/marionettejs/backbone.babysitter)
Expand Down
71 changes: 53 additions & 18 deletions src/collection-view.js
Expand Up @@ -92,17 +92,17 @@ Marionette.CollectionView = Marionette.View.extend({
// Handle a child added to the collection
_onCollectionAdd: function(child, collection, opts) {
var index;

this.destroyEmptyView();
var ChildView = this.getChildView(child);

if (opts.at !== undefined) {
index = opts.at;
} else {
index = this._sortedModels().indexOf(child);
index = _.indexOf(this._filteredSortedModels(), child);
}

this.addChild(child, ChildView, index);
if (this._shouldAddChild(child, index)) {
this.destroyEmptyView();
var ChildView = this.getChildView(child);
this.addChild(child, ChildView, index);
}
},

// get the child view by model it holds, and remove it
Expand Down Expand Up @@ -134,17 +134,28 @@ Marionette.CollectionView = Marionette.View.extend({
// all the collectionView
reorder: function () {
var children = this.children;

// get the DOM nodes in the same order as the models
var els = _.map(this._sortedModels(), function (model) {
return children.findByModel(model).el;
var models = this._filteredSortedModels();
var modelsChanged = _.find(models, function (model) {
return !children.findByModel(model);
});

// since append moves elements that are already in the DOM,
// appending the elements will effectively reorder them
this.triggerMethod('before:reorder');
this.$el.append(els);
this.triggerMethod('reorder');
// If the models we're displaying have changed due to filtering
// We need to add and/or remove child views
// So render as normal
if (modelsChanged) {
this.render();
} else {
// get the DOM nodes in the same order as the models
var els = _.map(models, function (model) {
return children.findByModel(model).el;
});

// since append moves elements that are already in the DOM,
// appending the elements will effectively reorder them
this.triggerMethod('before:reorder');
this.$el.append(els);
this.triggerMethod('reorder');
}
},

// Render view after sorting. Override this method to
Expand All @@ -162,7 +173,7 @@ Marionette.CollectionView = Marionette.View.extend({
// 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() {
var models = this._sortedModels();
var models = this._filteredSortedModels();

// check for any changes in sort order of views
var orderChanged = _.find(models, function(item, index){
Expand Down Expand Up @@ -193,14 +204,19 @@ Marionette.CollectionView = Marionette.View.extend({
this.showCollection();
this.endBuffering();
this.triggerMethod('render:collection', this);

// If we have shown children and none have passed the filter, show the empty view
if (this.children.isEmpty()) {
this.showEmptyView();
}
}
},

// Internal method to loop through collection and show each child view.
showCollection: function() {
var ChildView;

var models = this._sortedModels();
var models = this._filteredSortedModels();

_.each(models, function(child, index) {
ChildView = this.getChildView(child);
Expand All @@ -209,7 +225,7 @@ Marionette.CollectionView = Marionette.View.extend({
},

// Allow the collection to be sorted by a custom view comparator
_sortedModels: function() {
_filteredSortedModels: function() {
var models;
var viewComparator = this.getViewComparator();

Expand All @@ -223,6 +239,13 @@ Marionette.CollectionView = Marionette.View.extend({
models = this.collection.models;
}

// Filter after sorting in case the filter uses the index
if (this.getOption('filter')) {
models = _.filter(models, function (model, index) {
return this._shouldAddChild(model, index);
}, this);
}

return models;
},

Expand Down Expand Up @@ -522,6 +545,18 @@ Marionette.CollectionView = Marionette.View.extend({
return childViews;
},

// Return true if the given child should be shown
// Return false otherwise
// The filter will be passed (child, index, collection)
// Where
// 'child' is the given model
// 'index' is the index of that model in the collection
// 'collection' is the collection referenced by this CollectionView
_shouldAddChild: function (child, index) {
var filter = this.getOption('filter');
return !_.isFunction(filter) || filter.call(this, child, index, this.collection);
},

// Set up the child view event forwarding. Uses a "childview:"
// prefix in front of all forwarded events.
proxyChildEvents: function(view) {
Expand Down

0 comments on commit 4c5ef01

Please sign in to comment.