Skip to content

Instantly share code, notes, and snippets.

@paulkoegel
Last active December 11, 2015 19:28
Show Gist options
  • Save paulkoegel/4648194 to your computer and use it in GitHub Desktop.
Save paulkoegel/4648194 to your computer and use it in GitHub Desktop.
Extended MarionetteLayout to which you can pass an array of region names and View instances (via .addViews()); automatically creates the required region DOM containers. SEE BELOW FOR CODE.

MixedLayout

MixedLayouts are our custom extension of MarionetteLayouts which we use to wrap a heterogeneous collection of Backbone Views. This allows us to change the contents of complex pages all at once while preventing zombie views and leaving certain static elements like Navigation, Toolbar, or FooterViews on the page.

Rationale and Raison d'être

Marionette's Composite and CollectionViews are limited to rendering all their associated collection's items with the same ItemView. For our homepage, e.g., we need to encapsulate various Views - TopView, CategoryView, QuoteOfTheDayView etc. - in dynamic order into one Marionette View construct that can be .closed() to prevent zombie views. The Marionette view component designed to encapsulate heterogeneous subviews is the Layout. However, we cannot use a vanilla MarionetteLayout for the homepage because a Layout relies on pre-existing DOM containers (usually created in the Layout's template) for all its regions. A much simpler use case for MixedLayouts is an article page, where we can have a BreakingNewsTeaserView followed by an ArticleView, followed by several CompositeViews listing related articles. To keep things flexible and dynamic, we've introduced our MixedLayouts to which we can dynamically add regions - the required DOM containers will be added on the fly and in the future we could add behaviour allowing us to later insert regions between two existing ones.

Marionette Layouts are initialised like this:

<code>
var myLayout = Backbone.Marionette.Layout.extend({
 template: myTemplate,
 regions: {
   toolbar: '#toolbar',
   content: '#content',
   footer:  '#footer'
 }
});
</code>

In order to render a MarionetteView to one of those regions, however, the relevant DOM containers, #toolbar, #content and #footer, need to be created in the Layout's template. For our homepage, we cannot know what regions (and what associated DOM containers, for that matter) we need, since the homepage's content is determined by data coming from our API. This is where MixedLayouts come in, which inherit from MarionetteLayout and are initialised like this:

<code>
var myMixedLayout = new MixedMarionetteLayout({
  template: mixedMarionetteLayoutTemplate
});
</code>

mixedMarionetteLayoutTemplate is usually empty but all MarionetteLayouts need to have a template.

To add regions to a MixedLayout call addViews:

<code>
showLayout.addViews([
  { toolbar: new ToolbarView() },
  { content: new ArticleView() },
  { footer:  new FooterView()  }
]);
</code>

A MixedLayout like the above is used on the article page. Inside its content region we're putting another MixedLayout containing the article's specific content - articles can have a BreakingNewsTeaserView above and CompositeViews listing related articles below them.

articleContentLayout = new MixedMarionetteLayout({ template: mixedMarionetteLayoutTemplate }); articleContentLayout.addViews([ { breakingNews: new BreakingNewsTeaserView(model: breakingNewsArticle) }, { content: new ArticleView(model: article) }, { relatedArticles: new CompositeView(collection: relatedArticles) }, { trendingArticles: new CompositeView(collection: trendingArticles) } ])

By encapsulating several heterogeneous subviews, MixedLayouts offer us an easy way to exchange the contents of an article page - while preventing zombie views and without the need to reinitialize common view components like the toolbar or footer.

Pre-initialising regions with placeholders

Whenever we need to add MarionetteViews to a MixedLayout after a second API call but don't know what type the MarionetteView will be, we can initialise the MixedLayout's regions with placeholders and render the relevant view in their place when they're ready. We're using this pattern in the article action when the page's loaded via a direct URL call. Since we can't distinguish articles from galleries by their URL, we cannot know what view will be attached to the content region. Similarly, we cannot know whether an article has a BreakingNewsTeaserView in front of it or not before it's been fully loaded from the API. MixedLayout's 'addView'-method at this point can only append new regions at the end of an existing MixedLayout. To put a BreakingNewsTeaserView in front of the article's contents, we need to initialise the relevant MixedLayout with a placeholder region:

<code>
var articleContentLayout = new MixedMarionetteLayout({
    template: mixedMarionetteLayoutTemplate
});
articleContentLayout.addViews([
  { breakingNews: new Backbone.View() }, // new Backbone.View() acts as a NullObject in this case
  { content: new Backbone.View() }
  // etc.
]);
</code>

Later on, when the article's full data has arrived form the API, these regions can be filled with their real content like so:

<code>
articleContentLayout.addViews([
  { breakingNews: new BreakingNewsTeaserView({model: breakingNewsArticle})},
  { article: new ArticleView({model: article}) }
]);
</code>
// a MixedLayout automatically creates all the DOM elements required for its regions to render
// we're using MixedLayouts to render a collection of heterogeneous Views (e.g. on the homepage)
Backbone.Marionette.Layout.extend({
className: 'mixed-layout-wrapper',
initialize: function() {
// Marionette usually doesn't allow setting regions on `new MixedLayout()`, only on `Marionette.Layout.extend`; the following enables us to define regions when creating new MixedLayout instances
this.regions = this.options.regions || {};
this.initializeRegions();
_.bindAll(this, 'render', 'renderRegionContainers', 'addViews');
},
render: function(attrs) {
var isFirstRender = this._firstRender; // need to store this before we call prototype.render()
var regionSelectors = _(this.regions).values();
// TODO: should we filter out attrs.renderRegionContainers?
Marionette.Layout.prototype.render.apply(this, arguments);
if (isFirstRender || (attrs && attrs.renderRegionContainers)) { // only create region containers on initial render or if desired
this.renderRegionContainers(regionSelectors);
}
$(this.el).addClass(this.options.extraClassNames);
return this;
},
renderRegionContainers: function(regionSelectors) {
var idRegexp = new RegExp('^#(.*)');
var classRegexp = new RegExp('^\.(.*)');
regionContainers = _(regionSelectors).map(function(regionSelector) {
if (regionSelector.match(idRegexp)) {
return '<div id="' + regionSelector.match(idRegexp)[1] + '"></div>';
} else if (regionSelector.match(classRegexp)) {
return '<div class="' + regionSelector.match(classRegexp)[1] + '"></div>';
} else {
console.error("MixedMarionetteLayout.renderRegionContainers: Couldn't add region DOM elements. A DOM selector was probably too complex to parse, only plain IDs or classes are allowed - no nesting!");
}
}).join('\n');
$(this.$el).append($(regionContainers));
},
// regionsArray: [{regionNameA: viewA}, {regionNameB: viewB}] wrapped in an array b/c in an object we can't rely on the order of object keys (cf. http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop)
// TODO: future feature requirement: has to be possible to dynamically add new regions at a certain index
// for now, we can initialise placeholder mixedLayout regions with empty Backbone.View instances
addViews: function(regionsArray) {
var that = this;
_(regionsArray).each(function(regionObject) {
var regionName = _(regionObject).keys()[0];
var regionView = regionObject[regionName];
that.regions[regionName] = '#mixed-layout-' + regionName + '-' + (regionView.cid);
});
this.initializeRegions();
this.render({ renderRegionContainers: true });
_(regionsArray).each(function(regionObject) {
var regionName = _(regionObject).keys()[0];
var regionView = regionObject[regionName];
that[regionName].show(regionView);
});
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment