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 .
$3 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
`console $ 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][exposify]): like all NPM packages, backbone-fractal is available from [jsDelivr][jsDelivr-pkg] and [unpkg][unpkg-pkg]. 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.[exposify]: https://github.com/thlorenz/exposify [jsDelivr-pkg]: https://www.jsdelivr.com/package/npm/backbone-fractal [unpkg-pkg]: https://unpkg.com/backbone-fractal/
Comparison with Marionette [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 collectionCollectionView.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 KiBOverall, 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.
[marionette]: https://marionettejs.com
Reference - Common interface - renderContainer - beforeRender, afterRender - render - remove - CompositeView - constructor/initialize - defaultPlacement - subviews - forEachSubview - placeSubviews - detachSubviews - removeSubviews - CollectionView - container - subview - makeItem - initItems - items - initCollectionEvents - insertItem - removeItem - sortItems - placeItems - resetItems - clearItems - detachItems
$3 Both
CompositeView and CollectionView extend [Backbone.View][backbone-view]. The extensions listed in this section are common to both classes.[backbone-view]: https://backbonejs.org/#View
#### renderContainer
`js 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][backbone-render] method of a simple (i.e., non-composed) view.[backbone-render]: https://backbonejs.org/#View-render
You don’t need to define
renderContainer if your desired end result looks like this:
`html `You do need to define
renderContainer if your desired end result looks like this:
`html Own content
Also own content
`In the latter case, your
renderContainer method should do something that is functionally equivalent to the following:
`js class Example extends CompositeView { renderContainer() { this.$el.html( Own content
Also own content
); return this; } }`Of course, you are encouraged to follow the convention of using a [
template][backbone-template] for this purpose.[backbone-template]: https://backbonejs.org/#View-template
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
`js 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.
`js class Example extends CollectionView { beforeRender() { return this.trigger('render:start'); } afterRender() { return this.trigger('render:end'); } }` #### render
`js view.render() => view`
CompositeView and CollectionView can (and should be) [rendered][backbone-render] 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
`js 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][backbone-remove] in the opposite order in which they were inserted.[backbone-remove]: https://backbonejs.org/#View-remove
$3
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
`js new DerivedCompositeView(options);`While these methods are the same as in [
Backbone.View][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.
`js 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
`js Example.prototype.defaultPlacement: InsertionMethod`
InsertionMethod can be one of ['append'][jquery-append], ['prepend'][jquery-prepend], ['after'][jquery-after], ['before'][jquery-before] or ['replaceWith'][jquery-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.[jquery-append]: https://api.jquery.com/append/ [jquery-prepend]: https://api.jquery.com/prepend/ [jquery-after]: https://api.jquery.com/after/ [jquery-before]: https://api.jquery.com/before/ [jquery-replaceWith]: https://api.jquery.com/replaceWith/
#### subviews
`js 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:
`js // 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'][jquery-append]: make the subview the last child of the reference element. - ['prepend'][jquery-prepend]: make the subview the first child of the reference element. - ['after'][jquery-after]: make the subview the first sibling after the reference element. - ['before'][jquery-before]: make the subview the last sibling before the reference element. - ['replaceWith'][jquery-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:
`html Own content
Also own content
`Continuing the definition of the
Example class from before, the following
`js 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
`html Own content
Also own content
`From
`js Example.prototype.subviews = ['badge', 'dropdown', 'image']; Example.prototype.defaultPlacement = 'prepend';`we get
`html Own content
Also own content
`From
`js Example.prototype.subviews = [{ view: 'badge', selector: 'button', place: false, }, { view: 'dropdown', method: 'before', selector: 'button', }, { view: 'image', method: 'prepend', }];`we get
`html Own content
Also own content
`Finally, from
`js 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
`html Own content
Also own content
` #### forEachSubview
`js view.forEachSubview(iteratee, [options]) => viewiteratee: (subview, referenceElement, method) => 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,
`js [ '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:
`js 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:
`js 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,
`js [ 'badge', 'dropdown', { view: 'image', place: false, }, ]`then the expression
view.forEachSubview(iteratee, {placeOnly: true}) will be functionally equivalent to the following two statements:
`js 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,
`js ['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):
`js 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
`js 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.
`js 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 //
first
second
ordered.order = 'rtl'; ordered.placeSubviews(); // still the same huge chunk of HTML, but now ending in //
second
first
`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
`js 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:
`js 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
`js 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.
$3
CollectionView, as the name suggests, lets you represent a [Backbone.Collection][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.[backbone-collection]: https://backbonejs.org/#Collection
#### container
`js 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.
`html The title `If you set
view.container = '.listing', the subviews will be appended after the comment node.If, on the other hand, you don’t assign any value to
view.container, then the subviews will be appended after the 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
`js new view.subview([options]) => subview`The
subview property should be set to the constructor function of the subview type. Example:
`js 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
`js 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
`js 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.
`js 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
`js 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.
`js 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
`js view.initCollectionEvents() => view`This method binds [event handlers][backbone-event-catalog] 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:
`js class Example extends CollectionView { initialize() { this.initItems().initCollectionEvents(); } }`On this same line, you may as well invoke
.render for the first time:
`js 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.
[backbone-event-catalog]: https://backbonejs.org/#Events-catalog
#### insertItem
`js view.insertItem(model, [collection, [options]]) => view`Default handler for the [
'add' event][backbone-event-catalog] 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
`js view.removeItem(model, collection, options) => view`Default handler for the [
'remove' event][backbone-event-catalog] 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
`js view.sortItems() => view`Default handler for the [
'sort' event][backbone-event-catalog] 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][underscore-debounce] sortItems:
`js 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.
[underscore-debounce]: https://underscorejs.org/#debounce
#### placeItems
`js view.placeItems() => view`Default handler for the [
'update' event][backbone-event-catalog] 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][underscore-debounce] this method if you expect the 'update' event to trigger often. #### resetItems
`js view.resetItems() => view`Default handler for the [
'reset' event][backbone-event-catalog] 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][underscore-debounce] this method if you expect the 'reset' event to trigger often. #### clearItems
`js 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
`js 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][issue-list]!
[issue-list]: https://gitlab.com/jgonggrijp/backbone-fractal/issues/
$3 > 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][backbone-paginator] and a regular CollectionView.[backbone-paginator]: https://github.com/backbone-paginator/backbone.paginator
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:
`js 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:
`js 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:
`js 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 =
My Books ;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.
`
$3 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:
`js 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][backbone-types] 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:
`ts 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;
`[backbone-types]: https://www.npmjs.com/package/@types/backbone
$3 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:
`js this.forEachSubview(sv => sv.render(), { placeOnly: true });`If the parent view is a
CollectionView, add the following line instead:
`js 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.
$3 It is currently fashionable in other frameworks, such as [React][react], [Angular][angular] and [Vue][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][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):
`htmlSome text
Click me
`[react]: https://reactjs.org [angular]: https://angular.io [vue]: https://vuejs.org [web-components]: https://www.webcomponents.org
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:
`js 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':
`js class Example extends CompositeView { // ... renderContainer() { this.$el.html(this.template); return this; } }Example.prototype.template =
Some text
Click me
; 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][backbone-extend] 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.[backbone-extend]: https://backbonejs.org/#View-extend
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:
`js 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 =
Some text
; 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:
`js 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 =
Some text
;// 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 , and 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.
$3 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:
`html`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
. 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:
`html`and the
TableCollectionView in turn is a CollectionView with the following structure.
`html `> Side remark: it is only a small step from here to make the
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:
`html`then you can just add
and 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][bulma-panel], 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][bulma-panel] could look like this in our pseudo-HTML notation:
``html repositories