Monday, December 15, 2014

Marionette: Layout Views For Applications

Marionette Application instances are kind of like layout views. You can give them regions, and use these regions to show smaller views. There's one important difference, between a layout view an an application. With the latter, there's generally only one. Layout views on the other hand, generally exist in larger numbers. Another difference is with where they get their markup from. The application works with that's already in the DOM while a layout view works with a template that it renders.

The regions we give to the application instance are the main components of the layout. Things, like header, content, and so on. There's probably not very many regions on a given application. Of course, that all depends on how similar each page of the application is — if they're all the same, then the application might define several regions. Regardless, the application is expecting certain elements to exist in the DOM, depending on how it's regions are configured.

This can be a challenge, since whatever the server returns as the page content, is what the application instance has to work with. Layout views have the advantage of templates. For example, it we don't like the look of a given layout view, we can swap out the template for another with a different design. Or we can swap out the layout view for a different view altogether — the choice is ours. Application instances don't have such freedoms and there are times where this can be useful.

Let's make a basic application and some regions to illustrate the limitation. Here's the markup on the page served to the user, and what our application has to work with:

<head>
    <template id="header-template">Header</template>
    <template id="content-template">Content</template>
    <template id="footer-template">Footer</template>
</head>
<body>
    <div class="main">
        <div class="header"></div>
        <div class="content"></div>
        <div class="footer"></div>
    </div>
</body>

As you can see, there's three natural regions for this content — header, content, and footer. You'll also notice there's three templates defined in the header we can use to render the content for these regions:

var MyApp = Marionette.Application.extend({
    regions: {
        header: '.header',
        content: '.content',
        footer: '.footer'
    }
});

var app = new MyApp();

app.header.show(new Marionette.ItemView({
    template: '#header-template'    
}));

app.content.show(new Marionette.ItemView({
    template: '#content-template'
}));

app.footer.show(new Marionette.ItemView({
    template: '#footer-template'
}));

Nice and simple, we've sliced our page content into regions, and filled those regions with content. Notice that the application behaves a lot like a layout view. We can add regions, and show views in them later on. That's because like a layout view, an Application is instantiated with a region manager, something that takes care of instantiating new regions and placing them into the DOM. What the application is missing is the ability to render a template, and use that as the available region management markup.

This poses a problem because the application is now tightly-coupled to the page content, and how it's structured. Most of the time, this works, because we have total control over the page content, and when/how it changes. But in the event that we don't, or in the event that changing the page content is tricky, it's better to render a template.

It's also better to have all your content rendered by the same template engine. If the rest of your layout views and item views are using Underscore or Handlebars templates, you might as well render the page content using the same tool. Here's a slight modification to our code that allows for this:

var MyApp = Marionette.Application.extend({
    getRegionManager: function() {
        var layout = this.getOption('layout');
        if (layout) {
            new Marionette.Region({
                el: 'body'
            }).show(layout);
            return layout.regionManager;
        } else {
            return new Marionette.RegionManager();
        }
    },
    regions: {
        header: '.header',
        content: '.content',
        footer: '.footer'
    }
});

var layout = new Marionette.LayoutView({
    className: 'main new',
    template: '#new-layout-template'
});

var app = new MyApp({ layout: layout });

Application calls the getRegionManager() method to instantiate the region manager. By default, this method returns a new instance of the RegionManager class, and is easy to override. Our implementation of getRegionManager() looks of a layout option. If it's there, it means the caller specified a layout view they want to use for the application. We show the layout view in a new region who's element is the document body. Now, we can return the layout.regionManager property.

You can see that the #new-layout-template template is used to render the body, and we're no longer coupled to whatever the server returns. We simply pass in this new layout view as an option to our application. The rest of the code that populates the various regions remains unchanged, because app.content actually points to layout.content. Same with the other regions.