Permalink
1093 lines (818 sloc) 32.4 KB

Marionette.CollectionView

A CollectionView like View manages a portion of the DOM via a single parent DOM element or el. This view manages an ordered set of child views that are shown within the view's el. These children are most often created to match the models of a Backbone.Collection though a CollectionView does not require a collection and can manage any set of views.

CollectionView includes:

A CollectionView can have Behaviors.

Documentation Index

Instantiating a CollectionView

When instantiating a CollectionView there are several properties, if passed, that will be attached directly to the instance: attributes, behaviors, childView, childViewContainer, childViewEventPrefix, childViewEvents, childViewOptions, childViewTriggers, className, collection, collectionEvents, el, emptyView, emptyViewOptions, events, id, model, modelEvents, sortWithCollection, tagName, template, templateContext, triggers, ui, viewComparator, viewFilter

import { CollectionView } from 'backbone.marionette';

const myCollectionView = new CollectionView({ ... });

Some of these properties come from Marionette, but many are inherited from Backbone.View.

Rendering a CollectionView

The render method of the CollectionView is primarily responsible for rendering the entire collection. It loops through each of the children in the collection and renders them individually as a childView.

import { CollectionView } from 'backbone.marionette';

const MyCollectionView = CollectionView.extend({...});

// all of the children views will now be rendered.
new MyCollectionView().render();

Rendering a Template

In addition to rendering children, the CollectionView may have a template. The child views can be rendered within a DOM element of this template. The CollectionView will serialize either the model or collection along with context for the template to render.

For more detail on how to render templates, see View Template Rendering.

Defining the childViewContainer

By default the CollectionView will render the children into the el of the CollectionView. If you are rendering a template you will want to set the childViewContainer to be a selector for an element within the template for child view attachment.

import { CollectionView } from 'backbone.marionette';

const MyCollectionView = CollectionView.extend({
  childViewContainer: '.js-widgets',
  template: _.template('<h1>Widgets</h1><ul class="js-widgets"></ul>')
});

Errors An error will throw if the childViewContainer can not be found.

Re-rendering the CollectionView

If you need to re-render the entire collection or the template, you can call the collectionView.render method. This method will destroying all of the child views that may have previously been added.

View Lifecycle and Events

An instantiated CollectionView is aware of its lifecycle state and will throw events related to when that state changes. The view states indicate whether the view is rendered, attached to the DOM, or destroyed.

Read More:

Entity Events

The CollectionView can bind to events that occur on the attached model and collection - this includes both standard backbone-events and custom events.

Read More:

DOM Interactions

In addition to what Backbone provides the views, Marionette has additional API for DOM interactions: events, triggers, and ui.

By default ui is only bound to the elements within the template. However as events and triggers are delegated to the view's el they will apply to any children. There may be instances where binding ui is helpful when you want to access elements inside CollectionViews children with getUI(). For these cases you will need to bind ui yourself. To do so run bindUIElements on the CollectionView:

import { CollectionView } from 'backbone.marionette';

const MyCollectionView = CollectionView.extend({
  // ...

  ui: {
    checkbox: 'input[type="checkbox"]'
  }
});

const collectionView = new MyCollectionView();

collectionView.bindUIElements();

console.log(collectionView.getUI('checkbox')); // Output all checkboxes.

Read More:

Behaviors

A Behavior provides a clean separation of concerns to your view logic, allowing you to share common user-facing operations between your views.

Read More:

Managing Children

Children are automatically managed once the CollectionView is rendered. For each model within the collection the CollectionView will build and store a childView within its children object. This allows you to easily access the views within the collection view, iterate them, find them by a given indexer such as the view's model or id and more.

After the initial render the CollectionView binds to the update and reset events of the collection.

When the collection for the view is reset, the view will destroy all children and re-render the entire collection.

When a model is added to the collection, the CollectionView will render that one model into the children.

When a model is removed from the collection (or destroyed / deleted), the CollectionView will destroy and remove that model's child view.

When the collection for the view is sorted, the view by default automatically re-sort its child views unless the sortWithCollection attribute on the CollectionView is set to false, or the viewComparator is false.

import Backbone from 'backbone';
import { View, CollectionView } from 'backbone.marionette';

const collection = new Backbone.Collection();

const MyChildView = View.extend({:
  template: false
});

const MyCollectionView = CollectionView.extend({
  childView: MyChildView,
  collection,
});

const myCollectionView = new MyCollectionView();

// Collection view will not re-render as it has not been rendered
collection.reset([{foo: 'foo'}]);

myCollectionView.render();

// Collection view will effectively re-render displaying the new model
collection.reset([{foo: 'bar'}]);

When the children are rendered the render:children and before:render:children events will trigger.

When a childview is added to the children add:child and before:add:child events will trigger

When a childview is removed from the children remove:child and before:remove:child events will trigger.

Attaching children within the el

By default the CollectionView will add the HTML of each ChildView into an element buffer array, and then call the DOM API's appendContents once at the end to move all of the HTML within the collection view's el.

You can override this by specifying an attachHtml method in your view definition. This method takes two parameters and has no return value.

import { CollectionView } from 'backbone.marionette';

CollectionView.extend({

  // The default implementation:
  attachHtml(els, $container){
    // Unless childViewContainer, $container === this.$el
    this.Dom.appendContents(this.el, els);
  }
});

The first parameter is the HTML buffer, and the second parameter is the expected container for the children which by default equates to the view's el unless a childViewContainer is set.

Destroying All children

CollectionView implements a destroy method which automatically destroys its children and cleans up listeners.

When the children are destroyed the destroy:children and before:destroy:children events will trigger.

Read More:

CollectionView's childView

When using a collection to manage the children of CollectionView, specify a childView for your CollectionView. This must be a Backbone view class definition, not an instance. It can be any Backbone.View related class including both Marionette's View and CollectionView.

import { View, CollectionView } from 'backbone.marionette';

const MyChildView = View.extend({});

const MyCollectionView = CollectionView.extend({
  childView: MyChildView
});

Errors If you do not specify a childView, an exception will be thrown stating that you must specify a childView.

You can also define childView as a function. In this form, the value returned by this method is the ChildView class that will be instantiated when a Model needs to be initially rendered. This method also gives you the ability to customize per Model ChildViews.

import _ from 'underscore';
import Backbone from 'backbone';
import { View, CollectionView } from 'backbone.marionette';

const FooView =View.extend({
  template: _.template('foo')
});

const BarView = View.extend({
  bar
});

const MyCollectionView = CollectionView.extend({
  collection: new Backbone.Collection(),
  childView(item) {
    // Choose which view class to render,
    // depending on the properties of the item model
    if  (item.get('isFoo')) {
      return FooView;
    }
    else {
      return BarView;
    }
  }
});

const collectionView = new MyCollectionView();

const foo = new Backbone.Model({
  isFoo: true
});

const bar = new Backbone.Model({
  isFoo: false
});

// Renders a FooView
collectionView.collection.add(foo);

// Renders a BarView
collectionView.collection.add(bar);

Errors If childView is a function that does not return a view class an error will be thrown.

Building the children

The buildChildView method is responsible for taking the ChildView class and instantiating it with the appropriate data. This method takes three parameters and returns a view instance to be used as the child view.

buildChildView(child, ChildViewClass, childViewOptions){
  // build the final list of options for the childView class
  const options = _.extend({model: child}, childViewOptions);
  // create the child view instance
  const view = new ChildViewClass(options);
  // return it
  return view;
},

Override this method when you need a more complicated build, but use childView if you need to determine which View class to instantiate.

import _ from 'underscore';
import Backbone from 'backbone';
import { CollectionView } from 'backbone.marionette';
import MyListView from './my-list-view';
import MyView from './my-view';

const MyCollectionView = CollectionView.extend({
  childView(child) {
    if (child.get('type') === 'list') {
      return MyListView;
    }

    return MyView;
  },
  buildChildView(child, ChildViewClass, childViewOptions) {
    const options = {};

    if (child.get('type') === 'list') {
      const childList = new Backbone.Collection(child.get('list'));
      options = _.extend({collection: childList}, childViewOptions);
    } else {
      options = _.extend({model: child}, childViewOptions);
    }

    // create the child view instance
    const view = new ChildViewClass(options);
    // return it
    return view;
  }
});

Passing Data to the childView

There may be scenarios where you need to pass data from your parent collection view in to each of the childView instances. To do this, provide a childViewOptions definition on your collection view as an object literal. This will be passed to the constructor of your childView as part of the options.

import { View, CollectionView } from 'backbone.marionette';

const ChildView = View.extend({
  initialize(options) {
    console.log(options.foo); // => "bar"
  }
});

const MyCollectionView = CollectionView.extend({
  childView: ChildView,

  childViewOptions: {
    foo: 'bar'
  }
});

You can also specify the childViewOptions as a function, if you need to calculate the values to return at runtime. The model will be passed into the function should you need access to it when calculating childViewOptions. The function must return an object, and the attributes of the object will be copied to the childView instance's options.

import { CollectionView } from 'backbone.marionette';

const MyCollectionView = CollectionView.extend({
  childViewOptions(model) {
    // do some calculations based on the model
    return {
      foo: 'bar'
    };
  }
});

CollectionView's emptyView

When a collection has no children, and you need to render a view other than the list of childViews, you can specify an emptyView attribute on your collection view. The emptyView just like the childView can also be passed as an option on instantiation or can be a function that returns the emptyView.

import _ from 'underscore';
import { View, CollectionView } from 'backbone.marionette';

const MyEmptyView = View.extend({
  template: _.template('Nothing to display.')
});

const MyCollectionView = CollectionView.extend({
  // ...

  emptyView: MyEmptyView
});

CollectionView's getEmptyRegion

When a CollectionView is instantiated it creates a region for showing the emptyView. This region can be requested using the getEmptyRegion method. The region will share the el with the CollectionView and is shown with replaceElement: false.

Note The CollectionView expects to be the only entity managing the region. Showing things in this region directly is not advised.

const isEmptyShowing = myCollectionView.getEmptyRegion().hasView();

This region can be useful for handling the EmptyView Region Events.

Passing Data to the emptyView

Similar to childView and childViewOptions, there is an emptyViewOptions property that will be passed to the emptyView constructor. It can be provided as an object literal or as a function.

If emptyViewOptions aren't provided the CollectionView will default to passing the childViewOptions to the emptyView.

import { View, CollectionView } from 'backbone.marionette';

const EmptyView = View.extend({
  initialize(options){
    console.log(options.foo); // => "bar"
  }
});

const MyCollectionView = CollectionView.extend({
  emptyView: EmptyView,

  emptyViewOptions: {
    foo: 'bar'
  }
});

Defining When an emptyView shows

If you want to control when the empty view is rendered, you can override isEmpty:

import { CollectionView } from 'backbone.marionette';

const MyCollectionView = CollectionView.extend({
  isEmpty() {
    // some logic to calculate if the view should be rendered as empty
    return this.collection.length < 2;
  }
});

The default implementation of isEmpty returns !this.children.length.

You can also use this method to determine when the empty view was shown:

import { CollectionView } from 'backbone.marionette';

const MyCollectionView = CollectionView.extend({
  // ...
  onRenderChildren() {
    if (this.isEmpty()) { console.log('Empty View Shown'); }
  }
});

Accessing a Child View

You can retrieve a view by a number of methods. If the findBy* method cannot find the view, it will return undefined.

Note That children represents the views rendered that are or will be attached within the view's el.

CollectionView children's: findByCid

Find a view by it's cid.

const bView = myCollectionView.children.findByCid(buttonView.cid);

CollectionView children's: findByModel

Find a view by model.

const bView = myCollectionView.children.findByModel(buttonView.model);

CollectionView children's: findByModelCid

Find a view by model cid.

const bView = myCollectionView.children.findByModelCid(buttonView.model.cid);

CollectionView children's: findByIndex

Find by numeric index (unstable)

const bView = myCollectionView.children.findByIndex(0);

CollectionView children's: findIndexByView

Find the index of the view inside the children

const index = myCollectionView.children.findIndexByView(bView);

CollectionView children Iterators And Collection Functions

The container object borrows several functions from Underscore.js, to provide iterators and other collection functions, including:

These methods can be called directly on the container, to iterate and process the views held by the container.

import Backbone from 'backbone';
import { CollectionView } from 'backbone.marionette';

const collectionView = new CollectionView({
  collection: new Backbone.Collection()
});

collectionView.render();

// iterate over all of the views and process them
collectionView.children.each(function(childView) {
  // process the `childView` here
});

Listening to Events on the children

The CollectionView can take action depending on what events are triggered in its children.

Read More:

Self Managed children

In addition to children added by Marionette matching the model of a collection, the children of the CollectionView can be manually managed.

Adding a Child View

The addChildView method can be used to add a view that is independent of your Backbone.Collection. This method takes two parameters, the child view instance and optionally the index for where it should be placed within the CollectionView's children. It returns the added view.

import { CollectionView } from 'backbone.marionette';
import ButtonView from './button-view';

const MyCollectionView = CollectionView.extend({
  onRender() {
    View = new ButtonView();
    this.addChildView(buttonView, this.children.length);
  }
});

const myCollectionView = new MyCollectionView();

myCollectionView.render();

Note Unless an index is specified, this added view will be subject to filtering and sorting and may be difficult to manage in complex situations. Use with care.

Removing a Child View

The removeChildView method is useful if you need to remove and destroy a view from the CollectionView without affecting the view's collection. In most cases it is better to use the data to determine what the CollectionView should display.

This method accepts the child view instance to remove as its parameter. It returns the removed view;

import { CollectionView } from 'backbone.marionette';

const MyCollectionView = CollectionView.extend({
  onChildViewFooEvent(childView, model) {
    // NOTE: we must wait for the server to confirm
    // the destroy PRIOR to removing it from the collection
    model.destroy({wait: true});

    // but go ahead and remove it visually
    this.removeChildView(childView);
  }
});

Detaching a Child View

This method is the same as removeChildView with the exception that the removed view is not destroyed.

Swapping Child Views

Swap the location of two views in the CollectionView children and in the el. This can be useful when sorting is arbitrary or is not performant.

Errors If either of the two views aren't part of the CollectionView an error will be thrown.

If one child is in the el but the other is not, filter will be called.

import Backbone from 'backbone';
import { CollectionView } from 'backbone.marionette';
import MyChildView from './my-child-view';

const collection = new Backbone.Collection([
  { name: 'first' },
  { name: 'middle' },
  { name: 'last' }
]);

const myColView = new CollectionView({
  collection: collection,
  childView: MyChildView
});

myColView.swapChildViews(myColView.children.first(), myColView.children.last());

myColView.children.first().model.get('name'); // "last"
myColView.children.last().model.get('name'); // "first"

Sorting the children

The sort method will loop through the CollectionView children prior to filtering and sort them with the viewComparator. By default, if a viewComparator is not set, the CollectionView will sort the views by the order of the models in the collection. If set to false view sorting will be disabled.

This method is called internally when rendering and sort and before:sort events will trigger.

By default the CollectionView will maintain a sorted collection's order in the DOM. This behavior can be disabled by specifying {sortWithCollection: false} on initialize.

Defining the viewComparator

CollectionView allows for a custom viewComparator option if you want your CollectionView's children to be rendered with a different sort order than the underlying Backbone collection uses.

import { CollectionView } from 'backbone.marionette';

const myCollectionView = new CollectionView({
  collection: someCollection,
  viewComparator: 'otherFieldToSortOn'
});
import Backbone from 'backbone';
import { CollectionView } from 'backbone.marionette';

const myCollection = new Backbone.Collection([
  { id: 1 },
  { id: 4 },
  { id: 3 },
  { id: 2 }
]);

myCollection.comparator = 'id';

const mySortedColView = new CollectionView({
  //...
  collection: myCollection
});

const myUnsortedColView = new CollectionView({
  //...
  collection: myCollection,
  viewComparator: false
});

mySortedColView.render(); // 1 4 3 2
myUnsortedColView.render(); // 1 4 3 2

myCollection.sort();
// mySortedColView auto-renders 1 2 3 4
// myUnsortedColView has no change

The viewComparator can take any of the acceptable Backbone.Collection comparator formats -- 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.

getComparator

Override this method to determine which viewComparator to use.

import { CollectionView } from 'backbone.marionette';

const MyCollectionView = CollectionView.extend({
  sortAsc(model) {
    return -model.get('order');
  },
  sortDesc(model) {
    return model.get('order');
  },
  getComparator() {
    // The collectionView's model
    if (this.model.get('sorted') === 'ASC') {
      return this.sortAsc;
    }

    return this.sortDesc;
  }
});

setComparator

The setComparator method modifies the CollectionView's viewComparator attribute and re-sorts. Passing { preventRender: true } in the options argument will prevent the view being rendered.

import { CollectionView } from 'backbone.marionette';

const cv = new CollectionView({
  collection: someCollection
});

cv.render();

// Note: the setComparator is preventing the automatic re-render
cv.setComparator('orderBy', { preventRender: true });

// Render the children ordered by the orderBy attribute
cv.render();

removeComparator

This function is actually an alias of setComparator(null, options). It is useful for removing the comparator. removeComparator also accepts preventRender as a option.

import { CollectionView } from 'backbone.marionette';

const cv = new CollectionView({
  collection: someCollection
});

cv.render();

cv.setComparator('orderBy');

//Remove the current comparator without rendering again.
cv.removeComparator({ preventRender: true });

Maintaining the collection's sort

By default the CollectionView will maintain a sorted collection's order in the DOM. This behavior can be disabled by specifying {sortWithCollection: false} on initialize or on the view definiton.

import Backbone from 'backbone';
import { CollectionView } from 'backbone.marionette';

const myCollection = new Backbone.Collection([
  { id: 1 },
  { id: 4 },
  { id: 3 },
  { id: 2 }
]);

myCollection.comparator = 'id';

const mySortedColView = new CollectionView({
  //...
  collection: myCollection
});

const myUnsortedColView = new CollectionView({
  //...
  collection: myCollection,
  sortWithCollection: false
});

mySortedColView.render(); // 1 4 3 2
myUnsortedColView.render(); // 1 4 3 2

myCollection.sort();
// mySortedColView auto-renders 1 2 3 4
// myUnsortedColView has no change

Filtering the children

The filter method will loop through the CollectionView's sorted children and test them against the viewFilter. The views that pass the viewFilterare rendered if necessary and attached to the CollectionView and the views that are filtered out will be detached. After filtering the children will only contain the views to be attached.

If a viewFilter exists the filter and before:filter events will trigger.

By default the CollectionView will refilter when views change or when the CollectionView is sorted.

Note This is a presentation functionality used to easily filter in and out constructed children. All children of a collection will be instantiated once regardless of their filtered status. If you would prefer to manage child view instantiation, you should filter the collection itself.

Defining the viewFilter

CollectionView allows for a custom viewFilter option if you want to prevent some of the underlying children from being attached to the DOM. A viewFilter can be a function, predicate object. or string.

Errors An error will be thrown if the ViewFilter is not one of these options.

viewFilter as a function

The viewFilter function takes a view from the children and returns a truthy value if the child should be attached, and a falsey value if it should not.

import Backbone from 'backbone';
import { CollectionView } from 'backbone.marionette';

const cv = new CollectionView({
  childView: SomeChildView,
  emptyView: SomeEmptyView,
  collection: new Bb.Collection([
    { value: 1 },
    { value: 2 },
    { value: 3 },
    { value: 4 }
  ]),

  // Only show views with even values
  viewFilter(view, index, children) {
    return view.model.get('value') % 2 === 0;
  }
});

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

viewFilter as a predicate object

The viewFilter predicate object will filter against the view's model attributes.

import Backbone from 'backbone';
import { CollectionView } from 'backbone.marionette';

const cv = new CollectionView({
  childView: SomeChildView,
  emptyView: SomeEmptyView,
  collection: new Bb.Collection([
    { value: 1 },
    { value: 2 },
    { value: 3 },
    { value: 4 }
  ]),

  // Only show views with value 2
  viewFilter: { value: 2 }
});

// renders the view with values '2'
cv.render();

viewFilter as a string

The viewFilter string represents the view's model attribute and will filter truthy values.

import Backbone from 'backbone';
import { CollectionView } from 'backbone.marionette';

const cv = new CollectionView({
  childView: SomeChildView,
  emptyView: SomeEmptyView,
  collection: new Bb.Collection([
    { value: 0 },
    { value: 1 },
    { value: 2 },
    { value: null },
    { value: 4 }
  ]),

  // Only show views 1,2, and 4
  viewFilter: 'value'
});

// renders the view with values '1', '2', and '4'
cv.render();

getFilter

Override this function to programatically decide which viewFilter to use when filter is called.

import { CollectionView } from 'backbone.marionette';

const MyCollectionView = CollectionView.extend({
  summaryFilter(view) {
    return view.model.get('type') === 'summary';
  },
  getFilter() {
    if (this.collection.length > 100) {
      return this.summaryFilter;
    }
    return this.viewFilter;
  }
});

setFilter

The setFilter method modifies the CollectionView's viewFilter attribute and filters. Passing { preventRender: true } in the options argument will prevent the view being rendered.

import { CollectionView } from 'backbone.marionette';

const cv = new CollectionView({
  collection: someCollection
});

cv.render();

const newFilter = function(view, index, children) {
  return view.model.get('value') % 2 === 0;
};

// Note: the setFilter is preventing the automatic re-render
cv.setFilter(newFilter, { preventRender: true });

//Render the new state of the ChildViews instead of the whole DOM.
cv.render();

removeFilter

This function is actually an alias of setFilter(null, options). It is useful for removing filters. removeFilter also accepts preventRender as a option.

import { CollectionView } from 'backbone.marionette';

const cv = new CollectionView({
  collection: someCollection
});

cv.render();

cv.setFilter(function(view, index, children) {
  return view.model.get('value') % 2 === 0;
});

//Remove the current filter without rendering again.
cv.removeFilter({ preventRender: true });