backbone-fractal
Lightweight composite views for Backbone.
Fractal: a pattern that repeats in a self-similar way at different scales, such as the branches of a tree or a river, the bumpy contours of a mountain or the spiral sections of a snail’s house.
Introduction
Very often, it is wise to compose a view out of smaller subviews. Doing so has two main benefits over creating a single monolithic view: modularity and performance. However, it can be tricky to get this right with plain Backbone.View
s. backbone-fractal offers two base classes, CompositeView
and CollectionView
, which make this much easier. By using these base classes, you always get the best out of composition:
- A short, declarative syntax.
- Guaranteed correctness: no memory leaks, no dangling references.
- Top efficiency: a parent view can render independently of its subviews.
Below, we illustrate the challenges with composing Backbone.View
s, as well as the way in which backbone-fractal solves them.
A motivating example
We start by considering the following monolithic SearchForm
view.
import { View } from 'backbone';
class SearchForm extends View {
template({placeholder}) {
return `
<input placeholder="${placeholder}">
<button>Submit</button>
`;
}
render() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
}
SearchForm.prototype.tagName = 'form';
This view is a <form>
that contains an <input>
with a placeholder and a <button>
to submit. The placeholder is set by passing the view a model that has a placeholder
attribute.
While the above code is straightforward, there are a couple of problems with it:
- We will probably create other form views, such as a
LoginForm
, which also have a<button>
to submit. Some will have an<input>
with a placeholder as well. With the above approach, we are going to repeat the HTML for those elements over and over, as well as their event handlers. - We probably want all submit buttons in our application to look and behave in the same way. If we repeat the code for the submit button in many places, we will have to edit all those places every time when we decide to change something about our submit buttons.
- When the placeholder text changes, we need to re-render the entire search form, even though nothing has changed about the submit button. While the inefficiency may not seem like a big deal in this example, it will quickly add up in a real application with more complicated views.
So our SearchForm
is inefficient and it lacks modularity. We need to factor out the <input>
and the <button>
into separate views, like this:
class QueryField extends View {
render() {
this.$el.prop('placeholder', this.model.get('placeholder'));
return this;
}
}
QueryField.prototype.tagName = 'input';
class SubmitButton extends View {
render() {
this.$el.text('Submit');
return this;
}
}
SubmitButton.prototype.tagName = 'button';
This is excellent! We just created two very simple views that we can reuse everywhere in our app. If we change something about our submit buttons, we only need to edit the SubmitButton
class. Now, our first attempt at composing SearchForm
out of these views might look like this:
class SearchForm extends View {
render() {
let input = new QueryField({model: this.model});
let button = new SubmitButton();
this.$el.html(''); // clear any previous contents
input.render().$el.appendTo(this.el);
button.render().$el.appendTo(this.el);
return this;
}
}
This works and we solved the modularity problem, but this is still inefficient. We are always re-rendering everything, even the submit button that never changes. To make things worse, we have now introduced a memory leak. We are creating new instances of QueryField
and SubmitButton
every time we call .render()
on the SearchForm
, but we never call .remove()
on those instances.
We can address these issues by rewriting the SearchForm
class again.
class SearchForm extends View {
initialize() {
this.input = new QueryField({model: this.model});
this.button = new SubmitButton();
this.input.render();
this.button.render();
}
render() {
this.input.$el.detach();
this.button.$el.detach();
this.$el.html(''); // could create container HTML here
this.input.$el.appendTo(this.el);
this.button.$el.appendTo(this.el);
return this;
}
remove() {
this.input.remove();
this.button.remove();
return super.remove();
}
}
This is starting to look unwieldly. We will address that next, but let’s first consider what we have gained.
- We have turned the query field and the submit button into permanent members of the search form. This enables us to render them once when we create the search form and then only re-render them when they actually need to change. It also enables us to neatly clean them up when we don’t need the search form anymore.
- When rendering the search form, we detach the subviews before overwriting the HTML of the parent view. Otherwise, the
.el
members of the subviews would become dangling references to destroyed elements as soon as we render the search form for the second time. By detaching the subviews, overwriting the HTML and then re-inserting the subviews again, the search form can have arbitrary unique HTML of its own, in addition to the HTML that the subviews contribute. We will illustrate this later. - We have three views, the query field, the submit button and the search form, that can all render completely independently of each other, even though one of them is composed out of the other two.
So we have finally arrived at a solution that is both more modular and more efficient than what we started with. Now we just have to do something about the fact that the SearchForm
class is rather long for a view that is composed out of two smaller ones. The render
and remove
methods follow a pattern that will look the same for all views that we compose in this way. This is where backbone-fractal comes in.
Introducing CompositeView
The following code is equivalent to our last version of SearchForm
.
import { CompositeView } from 'backbone-fractal';
class SearchForm extends CompositeView {
initialize() {
this.input = new QueryField({model: this.model});
this.button = new SubmitButton();
this.input.render();
this.button.render();
}
}
SearchForm.prototype.subviews = ['input', 'button'];
We just derive from CompositeView
instead of Backbone’s View
and then declaratively list its subviews. The CompositeView
class then infers the correct implementation of the render
and remove
methods for us.
As mentioned before, the search form may define some unique HTML of its own. CompositeView
lets us render this with the renderContainer
method. We may also want to insert the subviews in a nested element instead of the parent view’s root element. We can specify this by passing a more elaborate description to the subviews
member. Both features are illustrated below.
class SearchForm extends CompositeView {
// initialize is exactly the same as above
initialize() {
this.input = new QueryField({model: this.model});
this.button = new SubmitButton();
this.input.render();
this.button.render();
}
template({title}) {
return `
<h1>${title}</h1>
<div class="container"></div>
`;
}
renderContainer() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
}
SearchForm.prototype.subviews = [{
view: 'input',
selector: '.container',
}, {
view: 'button',
selector: '.container',
}];
The SearchForm
class just defines how to render its own HTML and the CompositeView
parent class understands that it needs to insert the subviews in the nested div.container
element. There are lots of customization options; you can read all about them in the reference.
This example illustrates really well why we may want the search form and the query field to re-render independently from each other. While they share the same model, the search form only needs to refresh when the title changes and the query field only needs to refresh when the placeholder changes.
Introducing CollectionView
Besides CompositeView
, backbone-fractal provides one other base class, CollectionView
. It is meant for those situations where a view should represent an entire collection by representing each model in the collection with a separate subview. As an example, the following view classes represent a collection of books as a list of hyperlinks to the books.
import { CollectionView } from 'backbone-fractal';
class BookListItem extends View {
template({url, title}) {
return `<a href="${url}">${title}</a>`;
}
render() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
}
BookLink.prototype.tagName = 'li';
class LibraryListing extends CollectionView {
initialize() {
this.initItems().initCollectionEvents();
}
}
LibraryListing.prototype.subview = BookListItem;
LibraryListing.prototype.tagName = 'ol';
The above definition of LibraryListing
is very short, but it is enough for the CollectionView
class to represent each book in the LibraryListing
’s .collection
as a separate BookListItem
. It will keep the subviews in sync with the collection at all times, adding and removing views as models are added and removed, and keeping the <ol>
in the same order as the collection.
As with CompositeView
, CollectionView
can have a renderContainer
method. It also has lots of other customization options. For the full details, head over to the reference.
Conclusion
By now, we have seen how CompositeView
and CollectionView
offer a short and declarative syntax, correctness and efficiency. Using backbone-fractal, it is easy to get the best out of composing views.
A few more notes. Firstly, you can treat CompositeView
and CollectionView
subclasses just like a regular Backbone.View
subclass. This means you can also nest them inside each other, as deeply as you want. You are encouraged to do this, so you get maximum modularity and efficiency throughout your application.
Secondly, CompositeView
and CollectionView
serve the same purposes for nested document structures as tuples and arrays for nested data structures, respectively. This means that together, they are structurally complete: they can support every conceivable document structure. Our recipe for chimera views covers the most exotic cases.
Installation
$ npm install backbone-fractal
The library is fully compatible with both Underscore and Lodash, so you can use either. The minimum required versions of the dependencies are as follows:
- Backbone 1.4
- Underscore 1.8 or Lodash 4.17
- jQuery 3.3
For those who use TypeScript, the library includes type declaration files. You don’t need to install any additional @types/
packages.
If you wish to load the library from a CDN in production (for example via exposify): like all NPM packages, backbone-fractal is available from jsDelivr and unpkg. Be sure to use the backbone-fractal.js
from the package root directory. It should be embedded after Underscore, jQuery and Backbone and before your own code. It will expose its namespace as window.BackboneFractal
. Please note that the library is only about 2 KiB when minified and gzipped, so the benefit from keeping it out of your bundle might be insignificant.
Comparison with Marionette
Marionette offers a way to compose views out of smaller ones, too. This library already existed before backbone-fractal. So why did I create backbone-fractal, and why would you use it instead of Marionette? To answer these questions, let’s compare these libraries side by side.
feature | Marionette | backbone-fractal |
---|---|---|
regions/selectors | regions must be declared in advance | selector can be arbitrarily chosen |
child views per region/selector | one | as many as you want |
positioning of subviews | inside the region (region can contain nothing else) | before, inside or after the element identified by the selector |
ordering of subviews | not applicable | free to choose |
insertion of subview | manual, need to name a region | happens automatically at the right time inside the render method (exceptions possible when needed) |
CollectionView class |
keeps the subviews in sync with its collection | keeps the subviews in sync with its collection |
CollectionView.render |
destroys and recreates all of the subviews, even if its collection did not change | redraws only the HTML that doesn't belong to the subviews |
other features |
emptyView , Behavior , MnObject , Application , ui , triggers , bubbling events and then some |
none |
size, minified and gzipped | 9.3 KiB | 2.0 KiB |
Overall, Marionette is a mature library that offers many features besides composing views. If you like those other features, then Marionette is for you.
On the other hand, backbone-fractal is small. It does only one thing, and it does it in a way that is more flexible and requires less code than Marionette. If all you need is an easy, modular and efficient way to compose view, you may be better off with backbone-fractal.
Reference
Common interface
Both CompositeView
and CollectionView
extend Backbone.View. The extensions listed in this section are common to both classes.
renderContainer
view.renderContainer() => view
Default: no-op.
Renders the container of the subviews. You should not call this method directly; call render
instead.
Override this method to render the HTML context in which the subviews will be placed, if applicable. In other words, mentally remove all subviews from your desired end result and if any internal HTML structure remains after this, render that structure in renderContainer
. The logic is like the render
method of a simple (i.e., non-composed) view.
You don’t need to define renderContainer
if your desired end result looks like this:
<root-element>
<sub-view-1></sub-view-1>
<sub-view-2></sub-view-2>
</root-element>
You do need to define renderContainer
if your desired end result looks like this:
<root-element>
<p>Own content</p>
<div>
<sub-view-1></sub-view-1>
<sub-view-2></sub-view-2>
<button>Also own content</button>
</div>
</root-element>
In the latter case, your renderContainer
method should do something that is functionally equivalent to the following:
class Example extends CompositeView {
renderContainer() {
this.$el.html(`
<p>Own content</p>
<div>
<button>Also own content</button>
</div>
`);
return this;
}
}
Of course, you are encouraged to follow the convention of using a template
for this purpose.
The renderContainer
method is also a good place to define behaviour that should only affect the container HTML without touching the HTML of any of the subviews. For example, you may want to apply a jQuery plugin directly after setting this.$el.html
.
beforeRender, afterRender
view.beforeRender() => view
view.afterRender() => view
Default: no-op.
You can override these methods to add additional behaviour directly before or directly after rendering, respectively. Such additional behaviour could include, for example, administrative operations or triggering events.
class Example extends CollectionView {
beforeRender() {
return this.trigger('render:start');
}
afterRender() {
return this.trigger('render:end');
}
}
render
view.render() => view
CompositeView
and CollectionView
can (and should be) rendered just like any other Backbone.View
. The render
method is predefined to take the following steps:
- Call
this.beforeRender()
. - Detach the subviews from
this.el
. - Call
this.renderContainer()
. - (Re)insert the subviews within
this.el
. - Call
this.afterRender()
. - Return
this
.
The predefined render
method is safe and efficient. It is safe because it prevents accidental corruption of the subviews and enforces that each selector is matched at most once so no accidental “ghost copies” of subviews are made. It is efficient because it does not re-render the subviews; instead, it assumes that each individual subview has its own logic and event handlers to determine when it should render (we do, however, have a recipe for those who want to couple subview rendering to parent rendering).
You should never need to override render
. For customizations, use the renderContainer
, beforeRender
and afterRender
hooks instead.
The implementation of steps 2 and 4 differs between CompositeView
and CollectionView
, though in both cases, you don’t need to implement them yourself. For details, refer to the respective sections of the reference.
render
and remove
have the special property that subviews are always detached in the opposite order in which they were inserted, even when you have a deeply nested hierarchy of subviews and sub-subviews or when you follow our recipe for chimera views.
remove
view.remove() => view
Recursively calls .remove()
on all subviews and finally cleans up view
. Like with all Backbone views, call this method when you are done with the view. Like render
, you should not need to override this method.
As mentioned in render
, subviews are removed in the opposite order in which they were inserted.
CompositeView
CompositeView
lets you insert a heterogeneous set of subviews in arbitrary places within the HTML skeleton of your parent view. Its rendering logic is the same as that of CollectionView
, as described in the common interface. This section describes how to specify the subviews, as well some utility methods.
constructor/initialize
new DerivedCompositeView(options);
While these methods are the same as in Backbone.View
, it is recommended that you create the subviews here and keep them on this
throughout the lifetime of the CompositeView
. In some cases, you may also want to render a subview directly on creation. I suggest that you make each subview responsible for re-rendering itself afterwards whenever needed.
The following example class will be reused in the remainder of the CompositeView
reference.
import { BadgeView, DropdownView, ImageView } from '../your/own/code';
class Example extends CompositeView {
initialize(options) {
this.badge = new BadgeView(...);
this.dropdown = new DropdownView(...);
this.image = new ImageView(...);
this.image.render();
}
}
defaultPlacement
Example.prototype.defaultPlacement: InsertionMethod
InsertionMethod
can be one of 'append'
, 'prepend'
, 'after'
, 'before'
or 'replaceWith'
, roughly from most to least recommended.
Default: 'append'
.
This property determines how each subview will be inserted relative to a given element. You can override this for specific subviews on a case-by-case basis. Examples are provided next under subviews
.
subviews
view.subviews: SubviewDescription[]
view.subviews() => SubviewDescription[]
SubviewDescription: {
view: View,
method?: InsertionMethod,
selector?: string,
place?: boolean
}
Default: []
.
The subviews
property or method is the core administration with which you declare your subviews and which enables the CompositeView
logic to work.
It is very important that each “live” subview appears exactly once in subviews
. “Live” here means that the subview has been new
ed and not (yet) manually .remove()
d; it does not matter whether it is or should be in the DOM. If you want to (temporarily) skip a subview during insertion, do not omit it from subviews
; use place: false
instead. You may also set the place
field to a function that returns a boolean, if the decision whether to insert a particular subviews depends on circumstances.
There is a lot of freedom in the way you can define the subviews
array. Instead of a full-blown SubviewDescription
, you may also just put a subview directly in the array, in which case default values are assumed for the method
, selector
and place
fields. Each piece of information can be provided either directly or as a function that returns a value of the appropriate type. Functions will be bound to this
. In addition, all pieces except for the method
and selector
fields may be replaced by a string that names a property or method of your view. So the following classes are all equivalent:
// property with direct view names
class Example1 extends Example {}
Example1.prototype.subviews = ['badge', 'dropdown', 'image'];
// property with SubviewDescriptions with view names
class Example2 extends Example {}
Example2.prototype.subviews = [
{ view: 'badge' },
{ view: 'dropdown' },
{ view: 'image' },
];
// method with direct views by value
class Example3 extends Example {
subviews() {
return [this.badge, this.dropdown, this.image];
}
}
// mix with the name of a method that returns a SubviewDescription
class Example4 extends Example {
getBadge() {
return { view: this.badge };
}
subviews() {
return ['getBadge', 'dropdown', this.image];
}
}
In most cases, setting subviews
as a static array on the prototype should suffice. You only need to define subviews
as a method if you are doing something fancy that causes the set of subviews to change during the lifetime of the parent view.
If you pass a selector
, it should match exactly one element within the HTML skeleton of the parent view (i.e., the container of the subviews). If the selector
does not match any element, the subview will simply not be inserted. As a precaution, if the selector
matches more than one element, only the first matching element will be used. If you do not pass a selector, the root element of the parent view is used instead. We call the element that ends up being used the reference element.
The method
determines how the subview is inserted relative to the reference element. If the selector
is undefined
(i.e., the reference element is the parent view’s root element), the method
must be 'append'
or 'prepend'
, because the other methods work on the outside of the reference element. If not provided, the method
defaults to this.defaultPlacement
, which in turn defaults to 'append'
. A summary of the available methods:
-
'append'
: make the subview the last child of the reference element. -
'prepend'
: make the subview the first child of the reference element. -
'after'
: make the subview the first sibling after the reference element. -
'before'
: make the subview the last sibling before the reference element. -
'replaceWith'
: (danger!) remove the reference element and put the subview in its place. I recommend using this only if you want to work with custom elements.
For the following examples, assume that example.renderContainer
produces the following container HTML:
<root-element>
<p>Own content</p>
<div>
<button>Also own content</button>
</div>
</root-element>
Continuing the definition of the Example
class from before, the following
class Example extends CompositeView {
// initialize with badge, dropdown and image as before
// renderContainer as above
}
Example.prototype.subviews = ['badge', 'dropdown', 'image'];
let example = new Example();
example.render();
gives
<root-element>
<p>Own content</p>
<div>
<button>Also own content</button>
</div>
<badge-view></badge-view>
<dropdown-view></dropdown-view>
<image-view></image-view>
</root-element>
From
Example.prototype.subviews = ['badge', 'dropdown', 'image'];
Example.prototype.defaultPlacement = 'prepend';
we get
<root-element>
<image-view></image-view>
<dropdown-view></dropdown-view>
<badge-view></badge-view>
<p>Own content</p>
<div>
<button>Also own content</button>
</div>
</root-element>
From
Example.prototype.subviews = [{
view: 'badge',
selector: 'button',
place: false,
}, {
view: 'dropdown',
method: 'before',
selector: 'button',
}, {
view: 'image',
method: 'prepend',
}];
we get
<root-element>
<image-view></image-view>
<p>Own content</p>
<div>
<dropdown-view></dropdown-view>
<button>Also own content</button>
</div>
</root-element>
Finally, from
class Example extends CompositeView {
// ...
shouldInsertBadge() {
return this.subviews.length === 3;
}
}
Example.prototype.subviews = [{
view: 'badge',
selector: 'button',
place: 'shouldInsertBadge',
}, {
view: 'dropdown',
method: 'prepend',
selector: 'div',
}, {
view: 'image',
method: 'after',
selector: 'p',
}];
we get
<root-element>
<p>Own content</p>
<image-view></image-view>
<div>
<dropdown-view></dropdown-view>
<button>Also own content<badge-view></badge-view></button>
</div>
</root-element>
forEachSubview
view.forEachSubview(iteratee, [options]) => view
iteratee: (subview, referenceElement, method) => <ignored>
options: {
reverse: boolean,
placeOnly: boolean
}
This utility method processes view.subviews
. It calls iteratee
once for each entry, passing the subview, reference element and method as separate arguments. Defaults are applied, names are dereferenced and functions are invoked in order to arrive at the concrete values before invoking iteratee
. It passes view.$el
as the reference element if the subview description has no selector. In addition, iteratee
is bound to view
so that it can access view
through this
.
So if view.subviews
evaluates to the following array,
[
'badge',
{
view: function() { return this.dropdown; },
selector: 'button',
method: 'before',
},
]
then the expression view.forEachSubview(iteratee)
will be functionally equivalent to the following sequence of statements:
iteratee.call(view, view.badge, view.$el, view.defaultPlacement);
iteratee.call(view, view.dropdown, view.$('button').first(), 'before');
If, for example, you want to emit an event from each subview reporting where it will be inserted, the following will work regardless of how you specified view.subviews
:
view.forEachSubview(function(subview, referenceElement, method) {
subview.trigger('renderInfo', referenceElement, method);
});
If you pass {placeOnly: true}
, all subviews for which the place
option is explicitly set to (something that evaluates to) false
are skipped. For example, if view.subviews
has the following three entries,
[
'badge',
'dropdown',
{
view: 'image',
place: false,
},
]
then the expression view.forEachSubview(iteratee, {placeOnly: true})
will be functionally equivalent to the following two statements:
iteratee.call(view, view.badge, view.$el, view.defaultPlacement);
iteratee.call(view, view.dropdown, view.$el, view.defaultPlacement);
You may also pass {reverse: true}
to process the subviews in reverse order. So with the following view.subviews
,
['badge', 'dropdown', 'image']
subview.forEachSubview(iteratee, {reverse: true})
is equivalent to the following sequence of statements (note that view.image
comes first and view.badge
last):
iteratee.call(view, view.image, view.$el, view.defaultPlacement);
iteratee.call(view, view.dropdown, view.$el, view.defaultPlacement);
iteratee.call(view, view.badge, view.$el, view.defaultPlacement);
placeSubviews
view.placeSubviews() => view
This method puts all subviews that should be placed (as indicated by the place
option in each subview description) in their target position within the container HTML. The predefined render
method calls this.placeSubviews
internally. In general, you shouldn't call this method yourself; use render
instead.
There is, however, a corner case in which it does make sense to call placeSubviews
directly: when you have non-trivial container HTML that you want to leave unchanged but you want to move the subviews to different positions within it. If you want to implement such behaviour, you should implement subviews
as a method so that it can return a different position for each subview, and possibly different insertion orders, depending on circumstances.
class OrderedComposite extends CompositeView {
initialize() {
this.first = new View();
this.first.$el.html('first');
this.second = new View();
this.second.$el.html('second');
}
subviews() {
if (this.order === 'rtl') {
return ['second', 'first'];
} else {
return ['first', 'second'];
}
}
renderContainer() {
// imagine this method generates a huge chunk of HTML
}
}
let ordered = new OrderedComposite();
ordered.render();
// ordered.$el now contains a huge chunk of HTML, ending in
// <div>first</div><div>second</div>
ordered.order = 'rtl';
ordered.placeSubviews();
// still the same huge chunk of HTML, but now ending in
// <div>second</div><div>first</div>
It is wise to always first call view.detachSubviews()
before manually calling view.placeSubviews()
. In this way, you ensure that one subview cannot be accidentally inserted inside another subview if the latter subview happens to match your selector first.
detachSubviews
view.detachSubviews() => view
This method takes all subviews out of the container HTML by calling subview.$el.detach()
on each, in reverse order of insertion. The purpose of this method is to remove subviews temporarily so they can be re-inserted again later; all event listeners associated with the subviews stay active. This can be done to reset the parent to a pristine state with no inserted subviews, or before DOM manipulations in order to prevent accidental corruption of the subviews.
The predefined render
method calls this.detachSubviews
internally and most of the time, you don't need to invoke it yourself. It is, however, entirely safe to do so at any time. You might do this if you're going to apply a jQuery plugin that might unintentionally affect your subviews, or if you're going to omit some of the subviews that used to be inserted. Usage of detachSubviews
should generally follow the following pattern:
class YourCompositeView extends CompositeView {
aSpecialMethod() {
this.detachSubviews();
// apply dangerous operations to the DOM and/or
// apply changes that cause {place: false} on some of the subviews
this.placeSubviews();
// probably return something
}
}
removeSubviews
view.removeSubviews() => view
This is an irreversible operation. The reversible variant is detachSubviews
.
This method takes all subviews out of the container HTML by calling subview.remove()
on each, in reverse order of insertion. The purpose of this method is to remove subviews permanently and to unregister all their associated event listeners. This is facilitates garbage collection when the subviews aren't needed anymore.
The predefined remove
method calls this.removeSubviews
internally. The only reason to invoke it yourself would be to completely replace all subviews with a new set, or to clean up the subviews ahead of time, for example if you intend to continue using the parent as if it were a regular non-composite view.
CollectionView
CollectionView
, as the name suggests, lets you represent a Backbone.Collection
as a composed view in which each of the models is represented by a separate subview. Contrary to CompositeView
, the subviews are always kept together in the DOM, but the number of subviews is variable. It can automatically keep the subviews in sync with the contents of the collection. Its rendering logic is the same as that of CompositeView
, as described in the common interface. This section describes how to specify the subviews, as well some utility methods.
container
view.container: string
The container
property can be set to a jQuery selector to identify the element within your container HTML where the subviews should be inserted. If set, it is important that the selector identifies exactly one element within the parent view. If you leave this property undefined
, the parent view’s root element will be used instead.
For some examples, suppose that view.renderContainer
produces the following HTML.
<root-element>
<h2>The title</h2>
<section class="listing">
<!-- position 1 -->
</section>
<!-- position 2 -->
</root-element>
If you set view.container = '.listing'
, the subviews will be appended after the <!-- position 1 -->
comment node.
If, on the other hand, you don’t assign any value to view.container
, then the subviews will be appended after the <!-- position 2 -->
comment node.
If you want to use different container
values during the lifetime of your view, an appropriate place to update it is inside the renderContainer
method.
subview
new view.subview([options]) => subview
The subview
property should be set to the constructor function of the subview type. Example:
class SubView extends Backbone.View { }
class ParentView extends CollectionView {
initialize() {
this.initItems();
}
}
ParentView.prototype.subview = SubView;
let exampleCollection = new Backbone.Collection([{}, {}, {}]);
let parent = new ParentView({collection: exampleCollection});
parent.render();
// The root element of parent now holds three instances of SubView.
If you want to represent the models in the collection with a mixture of different subview types, this is also possible. Simply override the makeItem
method to return different subview types depending on the criteria of your choice. It is up to you whether and how to use the subview
property in that case.
makeItem
view.makeItem([model]) => subview
This method is invoked whenever a new subview must be created for a given model
. The default implementation is equivalent to new view.subview({model})
. If you wish to pass additional options to the subview constructor or to bind event handlers on the newly created subview
, override the makeItem
method. You are allowed to return different view types as needed.
Normally, you do not need to invoke this method yourself.
initItems
view.initItems() => view
This method initializes the internal list of subviews, invoking view.makeItem
for each model in view.collection
. You must invoke this method once in the constructor or the initialize
method of your CollectionView
subclass.
class Example extends CollectionView {
initialize() {
this.initItems();
}
}
If you wish to show only a subset of the collection, use a Backbone.Collection
adapter that takes care of the subsetting. See our pagination recipe for details.
items
view.items: subview[]
This property holds all of the subviews in an array. It is created by view.initItems
. You can iterate over this array if you need to do something with every subview.
view.items.forEach(subview => subview.render());
// All subviews of view have now been rendered.
import { isEqual } from 'underscore';
const modelsFromSubviews = view.items.map(subview => subview.model);
const modelsFromCollection = view.collection.models;
isEqual(modelsFromSubviews, modelsFromCollection); // true
Do not manually change the contents or the order of the items
array; but see the next section on the initCollectionEvents
method.
initCollectionEvents
view.initCollectionEvents() => view
This method binds event handlers on view.collection
to keep view.items
and the DOM in sync. In most cases, you should invoke this method together with initItems
in the constructor or the initialize
method:
class Example extends CollectionView {
initialize() {
this.initItems().initCollectionEvents();
}
}
On this same line, you may as well invoke .render
for the first time:
class Example extends CollectionView {
initialize() {
this.initItems().initCollectionEvents().render();
}
}
Whether you want to do this is up to you. The order of these invocations does not matter, except that initItems
should be invoked before render
.
Rare reasons to not invoke initCollectionEvents
may include the following:
- You expect the collection to never change.
- You expect the collection to change, but you want to create a “frozen” representation in the DOM that doesn’t follow changes in the collection.
- You expect the collection to change and you do want to update the DOM accordingly, but only to a limited extend or in a special way, or you want to postpone the updates until after certain conditions are met.
In the latter case, you may want to manually bind a subset of the event handlers or adjust the handlers themselves. The following sections provide further details on the event handlers.
insertItem
view.insertItem(model, [collection, [options]]) => view
Default handler for the 'add'
event on view.collection
.
This method calls view.makeItem(model)
and inserts the result in view.items
, trying hard to give it the same position as model
in collection
. For a 100% reliable way to ensure matching order, see sortItems
.
removeItem
view.removeItem(model, collection, options) => view
Default handler for the 'remove'
event on view.collection
.
This method takes the subview at view.items[options.index]
, calls .remove
on it and finally deletes it from view.items
. If you invoke removeItem
manually, make sure that options.index
is the actual (former) index of model
in collection
.
sortItems
view.sortItems() => view
Default handler for the 'sort'
event on view.collection
.
This method puts view.items
in the exact same order as the corresponding models in view.collection
. A precondition for this to work, is that the set of models represented in view.items
is identical to the set of models in view.collection
; under typical conditions, this is the job of insertItem
and removeItem
.
If you expect the 'sort'
event to trigger very often, you can save some precious CPU cycles by debouncing sortItems
:
import { debounce } from 'underscore';
class Example extends CollectionView {
// ...
}
Example.prototype.sortItems = debounce(CollectionView.prototype.sortItems, 50);
In the example, we debounce by 50 milliseconds, but you can of course choose a different interval.
placeItems
view.placeItems() => view
Default handler for the 'update'
event on view.collection
.
This method appends all subviews in view.items
to the element identified by view.container
or directly to view.$el
if view.container
is undefined. If the subviews were already present in the DOM, no copies are made, but the existing elements in the DOM are reordered to match the order in view.items
. If view.items
matches the order of view.collection
(for example because of a prior call to view.sortItems
), this effectively puts the subviews in the same order as the models in view.collection
.
Like in view.sortItems
, you can debounce this method if you expect the 'update'
event to trigger often.
resetItems
view.resetItems() => view
Default handler for the 'reset'
event on view.collection
.
This method is equivalent to view.clearItems().initItems().placeItems()
(see clearItems
, initItems
, placeItems
). It removes and destroys all existing subviews, creates a completely fresh set matching view.collection
and inserts the new subviews in the DOM.
Like in view.sortItems
, you can debounce this method if you expect the 'reset'
event to trigger often.
clearItems
view.clearItems() => view
This method calls .remove
on each subview in view.items
. Generally, you won’t need to invoke clearItems
yourself; it is called internally in remove
and in resetItems
.
detachItems
view.detachItems() => view
This method takes the subviews in view.items
temporarily out of the DOM in order to protect their integrity. It is called internally by the render
method. Calling this method is perfectly safe, although it is unlikely that you will need to do so.
Recipes
Need a recipe for your own use case? Drop me an issue!
Pagination
While this recipe describes how to do pagination with a
CollectionView
, it illustrates a more general principle. Whenever you want to show only a subset of a collection in aCollectionView
, this is best achieved by employing an intermediate adapter collection which contains only the subset in question.
Suppose you have a collection, library
, which contains about 1000 models of type Book
. You want to show the library
in a CollectionView
, but you want to show only 20 books at a time, providing “next” and “previous” buttons so the user can browse through the collection. This is quite easy to achieve using backbone.paginator and a regular CollectionView
.
backbone.paginator provides the PageableCollection
class, which can hold a single page of some underlying collection and which provides methods for selecting different pages. When you select a different page, the models in that page replace the contents of the PageableCollection
. The class has three modes, “server”, “client” and “infinite”, which respectively let you fetch and hold one page at a time, fetch and hold all data at once or fetch the data one page at a time and hold on to all data already fetched. In the “client” and “infinite” modes, you can access the underlying collection, i.e., the one containing all of the models that were already fetched, as the fullCollection
property.
In the following example, we use client mode because this is probably most similar to a situation without PageableCollection
. However, of course you can use a different mode and different settings; the recipe remains roughly the same. Suppose that your library
collection would otherwise look like this:
import { Collection } from 'backbone';
class Books extends Collection { }
Books.prototype.url = '/api/books';
let library = new Books();
// Get all books from the server.
library.fetch();
// Will trigger the 'update' event when fetching is ready.
then we can change it into the following to fetch all 1000 books at once, but expose only the first 20 books initially:
import { PageableCollection } from 'backbone.paginator';
class Books extends PageableCollection { }
Books.prototype.url = '/api/books';
let library = new Books(null, {
mode: 'client',
state: {
pageSize: 20,
},
});
// Get all books from the server.
library.fetch();
// When the 'update' event is triggered, library.models will contain
// the first 20 books, while library.fullCollection.models will
// contain ALL books.
Having adapted our library
thus, presenting one page at a time with a CollectionView
is now almost trivial:
import { BookView } from '../your/own/code';
// Example static template with a reserved place for the books and
// some buttons. With a real templating engine like Underscore or
// Mustache, you could make this more sophisticated, for example by
// hiding buttons that are not applicable or by showing the total and
// current page numbers.
const libraryTemplate = `
<h1>My Books</h1>
<table>
<thead>
<tr><th>Title</th><th>Author</th><th>Year</th></tr>
</thead>
<tbody>
<!-- book views will be inserted here -->
</tbody>
</table>
<footer>
<button class="first-page">first</button>
<button class="previous-page">previous</button>
<button class="next-page">next</button>
<button class="last-page">last</button>
</footer>
`;
class LibraryView extends CollectionView {
initialize() {
this.initItems().initCollectionEvents();
}
renderContainer() {
this.$el.html(this.template);
return this;
}
// Methods for browsing to a different page.
showFirst() {
this.collection.getFirstPage();
return this;
}
showPrevious() {
this.collection.getPreviousPage();
return this;
}
showNext() {
this.collection.getNextPage();
return this;
}
showLast() {
this.collection.getLastPage();
return this;
}
}
LibraryView.prototype.template = libraryTemplate;
LibraryView.prototype.subview = BookView;
LibraryView.prototype.container = 'tbody';
LibraryView.prototype.events = {
'click .first-page': 'showFirst',
'click .previous-page': 'showPrevious',
'click .next-page': 'showNext',
'click .last-page': 'showLast',
};
// That's all! Just use it like a regular view.
let libraryView = new LibraryView({ collection: library });
library.render().$el.appendTo(document.body);
// As soon as library is done fetching, the user will see the first
// page of 20 books. If she clicks on the "next" button, the next
// page will appear, etcetera.
Mixins
Suppose you want to make a subclass of CompositeView
. However, you want this same subclass to also derive from AnimatedView
, a class provided by some other library. Both CompositeView
and AnimatedView
derive directly from Backbone.View
and given JavaScript’s single-inheritance prototype chain, there is no obvious way in which you can extend both at the same time. Fortunately, Backbone’s extend
method lets you easily mix one class into another:
import { CompositeView } from 'backbone-fractal';
import { AnimatedView } from 'some-other-library';
export const AnimatedCompositeView = AnimatedView.extend(
CompositeView.prototype,
CompositeView,
);
Neither this problem nor its solution is specific to backbone-fractal, but there you have it.
If you are using TypeScript, there is one catch. The @types/backbone
package currently has incomplete typings for the extend
method, which renders the TypeScript compiler unable to infer the correct type for AnimatedCompositeView
. Until this problem is fixed, you can work around it by making a few special type annotations:
import { ViewOptions } from 'backbone'; // defined in @types/backbone
import { CompositeView } from 'backbone-fractal';
import { AnimatedView } from 'some-other-library';
export type AnimatedCompositeView = CompositeView & AnimatedView;
export type AnimatedCompositeViewCtor = {
new(options?: ViewOptions): AnimatedCompositeView;
} & typeof CompositeView & typeof AnimatedView;
export const AnimatedCompositeView = AnimatedView.extend(
CompositeView.prototype,
CompositeView,
) as AnimatedCompositeViewCtor;
Rendering subviews automatically with the parent
Rendering the subviews automatically when the parent view renders is generally not recommended because this negates one of the key benefits of backbone-fractal: the ability to selectively update the parent view without having to re-render all of the subviews. It is much more efficient to have each view in a complex hierarchy take care of refreshing itself while leaving all other views unchanged, including its subviews, than to always refresh an entire hierarchy. Image re-rendering a big table just because the caption changed, or re-rendering a big modal form just because the status message in its title bar changed; it is a waste of energy and time, potentially causing noticeable delays for the user as well.
With this out of the way, I realise that I cannot foresee all possible use cases. There may be corner cases where there truly is a valid reason for always re-rendering the subviews when the parent renders. Doing this is quite straightforward.
It takes just one line of code, which should be added to the beforeRender
, renderContainer
or afterRender
hook of the parent view. It does not really matter which hook you choose; the end effect is the same except for the timing.
If the parent view is a CompositeView
, add the following line:
this.forEachSubview(sv => sv.render(), { placeOnly: true });
If the parent view is a CollectionView
, add the following line instead:
this.items.forEach(sv => sv.render());
Needless to say, the automatic re-rendering of subviews does not “bleed through” to lower levels of the hierarchy. If a subview is itself the parent of yet smaller subviews, these sub-subviews will not automatically re-render as a side effect. If you want to make them automatically re-render as well, add the same line to intermediate parent views.
Custom elements
It is currently fashionable in other frameworks, such as React, Angular and Vue, to represent subviews (invariably called components) as custom elements in the template of the parent view. A similar approach is also taken in the upcoming Web Components standard. This is approximately the same notation we have seen so far in our pseudo-HTML examples, except that it is literally what is written in the template code (or in the case of Web Components, directly in plain HTML):
<p>Some text</p>
<div>
<sub-view-1></sub-view-1>
<sub-view-2></sub-view-2>
<button>Click me</button>
</div>
Backbone and backbone-fractal are fit for implementing true Web Components and conversely, Web Components can be integrated in any templating engine. However, Web Components is not 100% ready for production. If you like the pattern, you can also mimic it to some extent with CompositeView
by employing the 'replaceWith'
insertion method. For the following example code, we will reuse the Example
class from the CompositeView
reference, repeated below:
import { BadgeView, DropdownView, ImageView } from '../your/own/code';
class Example extends CompositeView {
initialize(options) {
this.badge = new BadgeView(...);
this.dropdown = new DropdownView(...);
this.image = new ImageView(...);
this.image.render();
}
}
In the most basic case, you just insert custom elements in your template in the places where the subviews should appear, use these custom elements as selectors in subviews
and set defaultPlacement
to 'replaceWith'
:
class Example extends CompositeView {
// ...
renderContainer() {
this.$el.html(this.template);
return this;
}
}
Example.prototype.template = `
<p>Some text</p>
<image-view></image-view>
<div>
<dropdown-view></dropdown-view>
<button>Click me<badge-view></badge-view></button>
</div>
`;
Example.prototype.defaultPlacement = 'replaceWith';
Example.prototype.subviews = [{
view: 'badge',
selector: 'badge-view',
}, {
view: 'dropdown',
selector: 'dropdown-view',
}, {
view: 'image',
selector: 'image-view',
}];
Perhaps you want to take this one step further. You might want the custom element names to be intrinsic to each view class. For example, you may want a BadgeView
to always appear as badge-view
in your templates. If this is the case, you probably don’t want to have to repeat that name as the selector
in the subview description every time. You may also want to keep the same custom element name in the final HTML.
Backbone.View
’s tagName
property is the ideal place to document a fixed custom element name for a view class. We can have all of the above by using tagName
and by doing some runtime preprocessing of the subviews
array. If you take this route, however, I recommend that you start all custom element names in your application with a common prefix. For example, if your application is called Awesome Webapplication, you could start all custom element names with aw-
, so the tagName
of BadgeView
would become aw-badge
instead of badge-view
.
With that out of the way, it is time to show an oversimplified version of the code that you need to realise intrinsic custom elements. Assume that BadgeView
, DropdownView
and ImageView
already have their tagName
s set to aw-badge
, aw-dropdown
and aw-image
, respectively:
import { result, isString } from 'underscore';
// The preprocessing function where most of the magic happens.
// It processes one subview description at a time.
function preprocessSubviewDescription(description) {
let view, place;
// Note that we don't support custom selector or method, yet.
if (isString(description)) {
view = description;
} else {
{ view, place } = description;
}
if (isString(view)) view = result(this, view);
let selector = view.tagName;
return { view, selector, place };
}
class Example extends CompositeView {
// ...
// Note that subviews is now a dynamic function instead of a static array.
subviews() {
return this._subviews.map(preprocessSubviewDescription.bind(this));
}
}
Example.prototype.template = `
<p>Some text</p>
<aw-image></aw-image>
<div>
<aw-dropdown></aw-dropdown>
<button>Click me<aw-badge></aw-badge></button>
</div>
`;
Example.prototype.defaultPlacement = 'replaceWith';
// _subviews is the un-preprocessed precursor to subviews.
Example.prototype._subviews = ['badge', 'dropdown', 'image'];
// Note the leading '_' and the fact that we don't include selectors anymore.
// For simplicity of the preprocessSubviewDescription function, we restrict
// ourselves to either just the name of a subview or a description containing
// the name of a subview, in the latter case with an optional place field.
The example code above works, but there are some caveats. Most importantly, the preprocessing function doesn’t support customized selectors. This is going to be a problem as soon as you have two subviews of the same class within the same parent, because they will share the same selector. We could fix this by amending the preprocessSubviewDescription
function so that it also accepts optional selectorPrefix
and selectorSuffix
fields:
function preprocessSubviewDescription(description) {
let view, place, selectorPrefix, selectorSuffix;
if (isString(description)) {
view = description;
} else {
{ view, place, selectorPrefix, selectorSuffix } = description;
}
if (isString(view)) view = result(this, view);
let prefix = selectorPrefix || '';
let suffix = selectorSuffix || '';
let selector = prefix + view.tagName + suffix;
return { view, selector, place };
}
// Now, let's adapt our template a bit to demonstrate how the above
// modifications solve our problem when we have two badge subviews.
Example.prototype.template = `
<p>Some text<aw-badge></aw-badge></p>
<aw-image></aw-image>
<div>
<aw-dropdown></aw-dropdown>
<button>Click me<aw-badge></aw-badge></button>
</div>
`;
// Note how we use prefixes to distinguish the badges.
Example.prototype._subviews = [
{
view: 'introBadge',
selectorPrefix: 'p ',
},
{
view: 'buttonBadge',
selectorPrefix: 'button ',
},
'dropdown',
'image',
];
This last example would be safe to use in production. The final HTML output by the render
method would be identical to the template, except that the instances of <aw-badge>
, <aw-dropdown>
and <aw-image>
would have their own internal structure.
You could go even further. You might, for example, further extend preprocessSubviewDescription
to permit exceptions to the rule that all subviews are inserted through the replaceWith
method. These and other sophistications are however outside of the scope of this recipe.
Chimera views
There are many situations where one might want to create a view that combines aspects from both CompositeView
and CollectionView
. For example, imagine that you are implementing a sortable table view with a few fixed clickable column headers and a variable set of rows, one for each model in a collection. The structure of your table view might look as follows in pseudo-HTML:
<table>
<thead>
<tr>
<clickable-header-view></clickable-header-view>
<clickable-header-view></clickable-header-view>
</tr>
</thead>
<tbody>
<row-view></row-view>
<row-view></row-view>
<row-view></row-view>
<row-view></row-view>
<!-- ... -->
</tbody>
</table>
In nearly all cases, including this example, there will be an element in your structure that contains all of the variable subset of subviews and none of the fixed subset of subviews. In the example, that element is the <tbody>
. You can always make that element a CollectionView
in its own right and make that a subview of a CompositeView
which also contains the fixed subviews. So in our example, the table as a whole becomes a CompositeView
with the following structure:
<table>
<thead>
<tr>
<clickable-header-view></clickable-header-view>
<clickable-header-view></clickable-header-view>
</tr>
</thead>
<table-collection-view></table-collection-view>
</table>
and the TableCollectionView
in turn is a CollectionView
with the following structure.
<tbody>
<row-view></row-view>
<row-view></row-view>
<row-view></row-view>
<row-view></row-view>
<!-- ... -->
</tbody>
Side remark: it is only a small step from here to make the
<thead>
aCollectionView
as well, so you can support a variable number of columns.
In nearly all remaining cases (where there is no element available that you can turn into a CollectionView
subview), you can restructure your HTML to end up in the same situation after all. For example, suppose that you started out with an old-fashioned “flat” table:
<table>
<tr>
<clickable-header-view></clickable-header-view>
<clickable-header-view></clickable-header-view>
</tr>
<row-view></row-view>
<row-view></row-view>
<row-view></row-view>
<row-view></row-view>
<!-- ... -->
</table>
then you can just add <thead>
and <tbody>
elements, creating the situation with which we started.
In rare cases, however, circumstances will force you to insert the variable subviews in the same element as the fixed subviews. One example of this is Bulma’s panel class, where you might want to have a couple of fixed .panel-block
s or .panel-tabs
s with controls at the top and bottom and a variable number of .panel-block
s in between to represent some collection. Bulma’s example could look like this in our pseudo-HTML notation:
<panel-view class="panel">
<p class="panel-heading">repositories</p>
<search-view class="panel-block"></search-view>
<tabs-view class="panel-tabs"></tabs-view>
<repository-view class="panel-block"></repository-view>
<repository-view class="panel-block"></repository-view>
<repository-view class="panel-block"></repository-view>
<!-- variable number of repository-views -->
<button-view class="panel-block"></button-view>
</panel-view>
Bulma requires all parts to be nested directly within the .panel
element in order for its styling to work, so we can’t cleanly separate out the variable subset of the subviews.
For this type of situation, we need to create a chimera view. The principle is to create a CompositeView
and a CollectionView
that share the same root element, where one owns the other “off the record”, i.e., without treating it as a regular subview. We may refer to the owning view as the master and the other as the slave. The slave is hidden from the view client and does not alter the view’s HTML content. The master serves as the public interface and calls methods of the slave under the hood. It also implements the .renderContainer
method if needed.
In most cases, it is probably most straightforward to make the CollectionView
the master, because this makes it easiest to position the fixed subviews relative to the variable subviews. Our Bulma panel example then looks like this:
import { CompositeView, CollectionView } from 'backbone-fractal';
import {
SearchView,
TabsView,
ButtonView,
RepositoryView,
someCollection,
} from '../your/own/code';
class PanelSlave extends CompositeView {
initialize() {
this.searchView = new SearchView();
this.tabsView = new TabsView();
this.buttonView = new ButtonView();
}
// We don't need to define or override any other method in this
// case.
}
// Note that we insert tabsView first and then searchView, because
// prepending multiple elements to the same parent element will make
// them appear in the reverse order of insertion.
PanelSlave.prototype.subviews = [{
view: 'tabsView',
method: 'prepend',
}, {
view: 'searchView',
method: 'prepend',
}, 'buttonView'];
// Panel is the master and represents the complete chimera view.
class Panel extends CollectionView {
initialize() {
this.initItems().initCollectionEvents();
this.slave = new PanelSlave({el: this.el});
}
beforeRender() {
this.slave.detachSubviews();
return this;
}
renderContainer() {
// this.template is left to your imagination. Its only
// duty is to produce the p.heading, since all other
// contents of the panel are subviews.
this.$el.html(this.template({}));
return this;
}
afterRender() {
this.slave.placeSubviews();
return this;
}
remove() {
this.slave.remove();
return super.remove();
}
setElement(el) {
if (this.slave) this.slave.setElement(el);
return super.setElement(el);
}
}
Panel.prototype.subview = RepositoryView;
// To use, just treat Panel like any other view class.
let somePanel = new Panel({collection: someCollection});
// Draw the entire panel structure as in our pseudo-HTML structure.
somePanel.render().$el.appendTo(document.body);
// Clean up after use.
somePanel.remove();
In general, the pattern looks like this:
class ChimeraSlave extends CompositeView {
// Like any other CompositeView, with some simplifications.
// Must NOT define .renderContainer.
// Should not define .el, .id, .tagName, .className, .attributes.
render() {
// The slave's .render method should never be called. Calling
// the inherited .render method will not seriously break
// anything, but it will likely insert the slave's subviews
// in the wrong order relative to the master's subviews.
// For ultimate robustness, make it a no-op.
return this;
}
}
// master:
class Chimera extends CollectionView {
// Like any other CollectionView, with the following additions.
initialize() {
// The following line may be placed in the constructor
// instead. It does not matter whether it runs before or
// after this.initItems.initCollectionEvents(), but it must
// run before the first .render().
this.slave = new ChimeraSlave({
// The slave MUST have the same .el as the master.
el: this.el,
// If there is a model, the slave probably needs it, too.
model: this.model,
// You can forward other options as needed.
});
}
beforeRender() {
this.slave.beforeRender();
// pre-render operations of the master
this.slave.detachSubviews();
}
afterRender() {
this.slave.placeSubviews();
// post-render operations of the master
this.slave.afterRender();
}
remove() {
this.slave.remove();
return super.remove();
}
setElement(el) {
// Keeps the .el in sync. We check whether the slave exists,
// because .setElement is always invoked once during
// construction when the slave hasn't been created yet.
if (this.slave) this.slave.setElement(el);
return super.setElement(el);
}
}
// In general, chimera views will often be passed both a model and a
// collection, since they combine aspects of both CompositeView and
// CollectionView.
let someChimera = new Chimera({ model: someModel, collection: someCollection });
Note that the slave’s rendering steps “wrap around” the master’s rendering steps in the same order as in the slave’s inherited .render
method. This preserves the property that all subviews are removed from the DOM in the opposite order in which they were inserted. The overridden .remove
method preserves this property, too.
There are many variations possible. Depending on your needs, you may want to reverse roles between the CompositeView
and the CollectionView
, where the former becomes the master and the latter the slave. The pattern can also be extended to multiple slaves, for example if there are multiple collections in play.