Chapter 6: CompositeView

In the last chapter, we discussed CollectionView. This chapter is about CompositeView, which inherits most of its functionality from CollectionView, so most of what I’ve mentioned in chapter 5 is also true for CompositeView. There are differences, obviously — otherwise we wouldn’t need both view classes — and instead of going through everything about CompositeView, this chapter will only explain what’s different between CollectionView and CompositeView.

Rendering

CollectionView cannot render templates (it simply appends the child views to its el), but CompositeView can render a template, so rendering — and the properties that affect it — is probably the biggest difference between the two view types. There are also some other aspects that affect the differences in how the two views render, so let’s take a closer look.

The Template

CompositeView renders a template in much the same way ItemView does. The first and most obvious thing you need to do is provide a template property. Go back and look at ItemView in chapter 4 for more information about how that works.

An example of a CollectionView that would need a template is building a table with headers. Each child view is a row in the table, but the first row of the table — the headers — isn’t built from data, so it should be in a template. Having a template would also allow you to add in the thead and tbody elements for more semantic goodness.

Where Do We Put the Kids?

Now, since we’ll be rendering child views, we need to inform the view where child views will be shown within our template. By default, if you don’t specify where the child views should be added, they will be appended to the view’s root el, right after the template. This can be what you want sometimes, but generally you will need to place them somewhere inside your template. To do this, we use the itemViewContainer property to specify a selector for the element we want the child elements placed into.

Composite Principles

There is something about the way CompositeView renders the template that is slightly different from ItemView. ItemView sends either the model or the collection to the template for rendering, depending on what exists (or an empty object if neither exists). CompositeView will only send the model (or an empty object if there is no model) to the template. It will then render the collection (if there is one) as child views.

This means we need both a model and a collection in order to make the CompositeView useful (though usually we only need to provide the model, as the collection will be extracted from it, as you’ll see later on). Also, if your template doesn’t require any data (such as when it’s just building the shell of a table or list), you can do without the model as well. It renders the data from the model within the template and then iterates through the collection, creating a child view for each model in that collection. If all you have is a model, it’ll pretty much act just like an ItemView. If all you have is a collection, then I hope your template doesn’t require any data.

Another interesting and unique thing about CompositeView is that although you can, you don’t need to supply an itemView property. If an itemView is provided, it’ll act like a CollectionView and render each child view as the type of view specified. But if itemView isn’t specified, it will default to recursively using its own class for each child view.

Recursion is often difficult for people to wrap their heads around, even if they are familiar with the concept and have seen it in practice. This is more than a simple function calling itself, so I’ll try to give some concrete examples, which I’ll steal from a post by Derick Bailey1, since he has a simple example that does a good job at focusing on the concept. However, as some people pointed out in the comments of that article, there are some flaws in the way he implemented the example, so I’ll actually use the code sample2 that one of those commenters offered as a replacement.

Recursive Data

Before I show how CompositeView recursively creates the views, though, I need to show you how recursive data works. Without recursive data, there is no reason to make the views recursive, since views are just visual representations of the data. When I talk about recursive data, I’m really talking about a tree of data in which a single node can have child nodes. There are plenty of great places to learn about composite data structures online3, and it’s a bit outside the scope of this book to go into it too much. If you don’t already know much about it, then I suggest you learn more about it online before continuing. I’ll still show examples of this sort of data structure so you know how it’s implemented in Backbone.

We’ll start with a collection of models. The models will simply be called “nodes” so I don’t need to contrive some strange reason for having a tree.

var Node = Backbone.Model.extend({
    initialize: function(){
        var nodes = this.get("nodes");
        // Covert nodes to a NodeCollection
        this.set('nodes', new NodeCollection(nodes));
    },

    toJSON: function() {
        // Call parent's toJSON method
        var data = Backbone.Model.prototype.toJSON.call(this);
        if (data.nodes && data.nodes.toJSON) {
            // If nodes is a collection, convert it to JSON
            data.nodes = data.nodes.toJSON();
        }

        return data;
    }
});

NodeCollection = Backbone.Collection.extend({
    model: Node
});

As you can see, when a model is created, it will convert the nodes property’s data to a NodeCollection from that data and assign it to the nodes property. Now you have a node that contains a collection of nodes. The nodes in that collection could also have their own collections, but not necessarily (no one likes infinite recursion). We also customize the toJSON method to also call toJSON on the child NodeCollection.

Here’s some sample data that we could use plus the code to set it up:

var nodeData = [
    {
        nodeName: "1",
        nodes: [
            {
                nodeName: "1.1",
                nodes: [
                    { nodeName: "1.1.1" },
                    { nodeName: "1.1.2" },
                    { nodeName: "1.1.3" }
                ]
            },
            {
                nodeName: "1.2",
                nodes: [
                    { nodeName: "1.2.1" },
                    { 
                        nodeName: "1.2.2",
                        nodes: [
                            { nodeName: "1.2.2.1" },
                            { nodeName: "1.2.2.2" },
                            { nodeName: "1.2.2.3" }
                        ]
                    },
                    { nodeName: "1.2.3" }
                ]
            }
        ]
    },
    {
        nodeName: "2",
        nodes: [
            {
                nodeName: "2.1",
                nodes: [
                    { nodeName: "2.1.1" },
                    { nodeName: "2.1.2" },
                    { nodeName: "2.1.3" }
                ]
            },
            {
                nodeName: "2.2",
                nodes: [
                    { nodeName: "2.2.1" },
                    { nodeName: "2.2.2" },
                    { nodeName: "2.2.3" }
                ]
            }
        ]
    }

];

var nodes = new NodeCollection(nodeData);

So that’s the data that will be used for the example. Let’s prepare the views.

Recursive Views

We’re going to create a CompositeView that creates a simple list out of this data, like this:

1

1.1

1.1.1

1.1.2

1.1.3

1.2

1.2.1

1.2.2

1.2.2.1

1.2.2.2

1.2.2.3

1.2.3

2

2.1

2.1.1

2.1.2

2.1.3

2.2

2.2.1

2.2.2

2.2.3

To do this, each model will be represented by a li element, and each collection of models will be represented by a ul element. Since the CompositeView itself actually represents the model, we need a way to put all of them into an initial ul element. For this, we’ll also create the following CollectionView:

var TreeRoot = Marionette.CollectionView.extend({
    tagName: "ul",
    itemView: TreeView
});

TreeView will be our CompositeView. TreeRoot will take the nodes collection we created earlier and create the two main CompositeViews, which will take it from there. First of all, we should have a template for the CompositeView. Let’s add that to the HTML right now:

<script id="node-template" type="text/template">
    <%= nodeName %>
    <ul></ul>
</script>

It’s a very simple template. It just displays the nodeName from the model and has a ul to add the child views to. Alright, let’s stop beating around the bush and finally create that CompositeView.

var TreeView = Marionette.CompositeView.extend({
    template: "#node-template",    
    tagName: "li",
    itemViewContainer: "ul",

    initialize: function(){
        // grab the child collection from the parent model
        // so that we can render the collection as children
        // of this parent node
        this.collection = this.model.get('nodes');
    },

});

Let’s step through each line:

Create TreeView by extending Marionette.CompositeView.

Assign the template that we created.

Give it a tagName of li. This is built into Backbone and changes the root element’s (this.el) type.

Set the itemViewContainer to ul, so that all the child views will be appended to the ul in the template.

Use the initialize function to set up our collection. Since each TreeView will only be receiving the models, we need to extract the collections out of the models.

None of that should have been new except the last step. That last step is very important, and you may often need to do something similar when using the CompositeView in its default recursive mode.

Now the only thing that’s left to do is render it all and put it on the page:

var tree = new TreeRoot({collection: nodes});
tree.render();
$('body').append(tree.el);

Stealing from the ItemView

Practically everything from ItemView also applies to CompositeView. In particular, templateHelpers and dynamic templates via overriding getTemplate make an appearance at the CompositeView party. There isn’t really any reason to go deeper into either one, but I thought I’d show you a way of overriding getTemplate that would make our example a little better.

Right now, we only have a single template and that template has the child ul element in it. This means that even CompositeViews that have no collection will display the ul but it will be empty. Let’s come up with a way to render a different template when we don’t need the ul.

First, let’s add the new template. It will simply remove the ul from the current template.

<script id="leaf-template" type="text/template">
    <%= nodeName %>
</script>

Now we need to change TreeView so that it will use a dynamic template:

var TreeView = Backbone.Marionette.CompositeView.extend({
    getTemplate: function() {
        if (_.isUndefined(this.collection)) {
            return "#leaf-template";
        } else {
            return "#node-template";
        }
    },

    tagName: "li",
    itemViewContainer: "ul",

    initialize: function(){
        this.collection = this.model.get('nodes');
    },

});

We don’t even need to specify a template property, but we do need to override getTemplate to choose which template to use depending on whether or not we have a collection. This may make the JavaScript code a bit more cluttered, but it cleans up the HTML, and depending on your style sheet, clearing those extra ul elements out of there may be absolutely necessary.

Events and Callbacks

As usual, CompositeView has built-in events and callbacks, most of which are inherited from CollectionView. CompositeView implements a few of its own events between the ones it inherits. The only events it adds, though, are during rendering. All the other events are inherited. Let’s take a look at the new events and callbacks.

Event Name Callback Name Description
composite:model:rendered onCompositeModelRendered After all of the before:render events, the template and model data will be rendered. This event will fire immediately after that rendering.
composite:collection:rendered onCompositeCollectionRendered This event fires following the child views being rendered.
composite:rendered onCompositeRendered This event fires immediately after composite:collection:rendered and just before the render and collection:rendered events.

Table 5: New events and callbacks in CompositeView. (View in your browser4.)

Like CollectionView, CompositeView also listens for and forwards the events of the child views. That’s a lot of events being fired. Here’s a sample of the events (in order) that you would see from a recursive CompositeView that is rendering only two child views:

before:render

collection:before:render

composite:model:rendered

before:item:added

itemview:before:render

itemview:collection:before:render

itemview:composite:model:rendered

itemview:composite:collection:rendered

itemview:composite:rendered

itemview:render

itemview:collection:rendered

after:item:added

before:item:added

itemview:before:render

itemview:collection:before:render

itemview:composite:model:rendered

itemview:composite:collection:rendered

itemview:composite:rendered

itemview:render

itemview:collection:rendered

after:item:added

composite:collection:rendered

composite:rendered

render

collection:rendered

That’s noticeably more than CollectionView, but as I said earlier, you don’t need to memorize them. Just look them up when you need them.

Summary

It’s truly amazing that something as intricate and complex as displaying a collection or a composite architecture can — for the most part — be simplified down to just a few pieces of configuration. Together, the view classes are probably the most powerful and useful pieces that Marionette provides. Of course, that doesn’t mean that Marionette isn’t chock-full of other extremely useful goodies, as you’ve already seen and will soon see again in the upcoming chapters.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset