Miss any of our Open RFC calls?Watch the recordings here! »

backbone-fractal

1.1.0 • Public • Published

pipeline status latest version on npm code hosted on GitLab issue tracker on GitLab author: Julian Gonggrijp license text

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.Views. 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.Views, 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:

  1. Call this.beforeRender().
  2. Detach the subviews from this.el.
  3. Call this.renderContainer().
  4. (Re)insert the subviews within this.el.
  5. Call this.afterRender().
  6. 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 newed 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 a CollectionView, 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 tagNames 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: '',
    },
    {
        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> a CollectionView 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-blocks or .panel-tabss with controls at the top and bottom and a variable number of .panel-blocks 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.

Install

npm i backbone-fractal

DownloadsWeekly Downloads

2

Version

1.1.0

License

BSD-3-Clause

Unpacked Size

181 kB

Total Files

31

Last publish

Collaborators

  • avatar