Build interfaces

5.0.0-beta.1 · Published beta · b06750c5. Published on npm. Match your installed version.

View Template Rendering#

Give a view a template function, then call render() to put its result in the view's element. A plain function is enough to get started; template engines and custom renderers can fit the same workflow.

The renderer evaluates the template; DomApi applies the result to the element. Projects can configure template evaluation with setRenderer() directly. Lit and Morphdom are DOM adapters configured with setDomApi().

import { View } from 'marionette';

const MyView = View.extend({
  tagName: 'h1',
  template: () => 'Contents'
});

const myView = new MyView();
myView.render();

This renders <h1>Contents</h1>, available at myView.el.

Documentation Index#

What is a template?#

A template is a function that given data returns either an HTML string or DOM. The default renderer in Marionette expects the template to return an HTML string. If your application uses Underscore, its template compiler can create that function. Install Underscore as an application dependency to use the following example; Marionette does not include it.

import _ from 'underscore';
import { View } from 'marionette';

const MyView = View.extend({
  template: _.template('<h1>Hello, world</h1>')
});

This doesn't have to be an underscore template, you can pass your own rendering function:

import Handlebars from 'handlebars';
import { View } from 'marionette';

const MyView = View.extend({
  template: Handlebars.compile('<h1>Hello, {{ name }}</h1>')
});

Setting a View Template#

Marionette views use the getTemplate method to determine which template to use for rendering into its el. By default getTemplate is predefined on the view as simply:

getTemplate() {
  return this.template
}

In most cases by using the default getTemplate you can simply set the template on the view to define the view's template, but in some circumstances you may want to set the template conditionally.

import _ from 'underscore';
import { View } from 'marionette';

const MyView = View.extend({
  template: _.template('Hello World!'),
  getTemplate() {
    if (this.Data.has(this.model, 'user')) {
      return _.template('Hello User!');
    }

    return this.template;
  }
});

Using a View Without a Template#

By default CollectionView has no defined template and will only attempt to render the template if one is defined. For View there may be some situations where you do not intend to use a template. Perhaps you only need the view's el or you are using prerendered content.

In this case setting template to false will prevent the template render. In the case of View it will also prevent the render events.

import { View } from 'marionette';

const MyIconButtonView = View.extend({
  template: false,
  tagName: 'button',
  className: 'icon-button',
  triggers: {
    'click': 'click'
  },
  onRender() {
    console.log('You will never see me!');
  }
});

Rendering the Template#

Each view class has a renderer which by default passes the view data to the template function and returns the html string it generates.

The current default renderer is essentially the following:

import { View, CollectionView } from 'marionette';

function renderer(template, data) {
  return template(data);
}

View.setRenderer(renderer);
CollectionView.setRenderer(renderer);

The default expects a function template; it does not look up script elements by selector.

Using a Custom Renderer#

You can set the renderer for a view class by using the class method setRenderer. The renderer accepts two arguments. The first is the template passed to the view, and the second argument is the data to be rendered into the template. Marionette invokes the renderer with the View as this, so use a regular function when the renderer needs access to the View instance.

Rendering is synchronous. A renderer must return content supported by the chosen DomApi immediately; returning a Promise does not make render() await it. Complete asynchronous loading before rendering, or update the View when the result becomes available under its owner's cancellation rules.

Marionette passes the renderer's return value to attachElContent, which calls Dom.setContents. The renderer evaluates the template; the DOM adapter applies its result. The native, jQuery, and Morphdom adapters treat null and undefined as empty contents. Lit accepts these values as empty content too. Returning undefined does not bypass content attachment.

Here's an example that allows for the template of a view to be an underscore template string.

import BackboneApi from '@mnjs/adapters/backbone';
import Backbone from 'backbone';
import _ from 'underscore';
import { setDataApi, View } from 'marionette';

setDataApi(BackboneApi);

View.setRenderer(function(template, data) {
  return _.template(template)(data);
});

const myView = new View({
  template: 'Hello <%- name %>!',
  model: new Backbone.Model({ name: 'World' })
});

myView.render();

// myView.el is <div>Hello World!</div>

The renderer can also be customized separately on any extended View. This standalone example uses the default plain-object DataApi and requires the application to install Handlebars.

import Handlebars from 'handlebars';
import { View } from 'marionette';

const MyHBSView = View.extend();

// Similar example as above but for handlebars
MyHBSView.setRenderer(function(template, data) {
  return Handlebars.compile(template)(data);
});

const myHBSView = new MyHBSView({
  template: 'Hello {{ name }}!',
  model: { name: 'World' }
});

myHBSView.render();

// myHBSView.el is <div>Hello World!</div>

Note These examples while functional may not be ideal. If possible it is recommend to precompile your templates which can be done for a number of templating using various plugins for bundling tools such as Browserify or Webpack.

Rendering to HTML#

The default Marionette renderer returns the HTML as a string. This string is passed to the view's attachElContent method which in turn uses the DOM API's setContents to set the contents of the view's el with DOM from the string.

Customizing attachElContent#

You can modify the way any particular view attaches a compiled template to the el by overriding attachElContent. This method always receives the result of the view's renderer, including undefined.

For instance, perhaps for one particular view you need to bypass the DOM API and set the html directly:

attachElContent(html) {
  this.Dom.setContents(this.el, html);
}

Rendering to DOM#

A DOM adapter can update existing content incrementally. The optional @mnjs/adapters package includes Morphdom and Lit HTML integrations. Install only the DOM adapter peer your application uses and configure a View subclass before creating its instances. setDomApi overlays the supplied methods and preserves unrelated operations, including jQuery queries.

For HTML string templates:

import { View } from 'marionette';
import MorphdomDomApi from '@mnjs/adapters/dom/morphdom';

const MessageView = View.extend({
  template: () => '<p id="message">Hello again.</p>'
});
MessageView.setDomApi(MorphdomDomApi);

Morphdom updates the View's contents using its normal matching rules, including element IDs. Empty roots take the direct HTML insertion path. For Lit templates, select the Lit DOM adapter:

import { View } from 'marionette';
import { html } from 'lit-html';
import LitDomApi from '@mnjs/adapters/dom/lit-html';

const MessageView = View.extend({
  template: ({ message }) => html`<p>${message}</p>`,
  templateContext: { message: 'Hello again.' }
});
MessageView.setDomApi(LitDomApi);

Both adapters apply template output through Dom.setContents. The root remains owned by the View; refresh its dynamic className, id, or attributes with renderAttributes(). A parent render still destroys its Region children. Keep Region placeholders empty so the renderer and Region do not manage the same contents.

Lit replaces preexisting contents on its first explicit render. Keep monitorViewEvents enabled and manage attachment through Regions so directives receive connection changes through Dom.notifyAttach(el) and Dom.notifyDetach(el). The View keeps the same root throughout its lifetime. Automatic directive connection management requires monitoring on the View and its ancestors. Lifecycle overrides must call their parent methods; avoid independently replacing Lit's contents or switching DOM adapters after rendering. See the render adapter guide for installation, directive cleanup, and root ownership.

Rendering configuration is separate from data and state integration. Configure DataApi and StateApi explicitly when your sources need them.

Serializing Data#

Marionette will automatically serialize the data from its model or collection through the configured DataApi for the template to use at rendering. You can override this logic and provide serialization of other data with the serializeData method. The method is called with no arguments, but has the context of the view and should return a javascript object for the template to consume. If serializeData does not return data the template may still receive added context or an empty object for rendering.

import _ from 'underscore';
import { View } from 'marionette';

const MyView = View.extend({
  template: _.template(`
    <div><%- user.name %></div>
    <ul>
    <% _.each(groups, function(group) { %>
      <li><%- group.name %></li>
    <% }) %>
    </ul>
  `),
  serializeData() {
    // For this view I need both the
    // model and collection serialized
    return {
      user: this.serializeModel(),
      groups: this.serializeCollection(),
    };
  }
});

Note You should not use this method to add arbitrary extra data to your template. Instead use templateContext to add context data to your template.

Serializing a Model#

If the view has a model, it passes DataApi.serialize(model) to the template. The default adapter returns the original plain object.

import _ from 'underscore';
import { View } from 'marionette';

const MyView = View.extend({
  template: _.template('<h1>Hello, <%- name %></h1>')
});

const myView = new MyView({ model: { name: 'world' } });

How the model is serialized can also be customized per view.

import BackboneApi from '@mnjs/adapters/backbone';
import _ from 'underscore';
import { setDataApi, View } from 'marionette';

setDataApi(BackboneApi);

const MyView = View.extend({
  serializeModel() {
    const data = _.clone(this.Data.serialize(this.model));

    // serialize a nested Backbone model through the configured adapter
    data.subModel = this.Data.serialize(data.subModel);

    return data;
  }
});

Serializing a Collection#

If the view does not have a model but has a collection, DataApi supplies its ordered models and serializes each one into an array provided as a models attribute to the template. These are the results of calling DataApi.serialize() for each model, not the raw model instances returned by DataApi.models().

import _ from 'underscore';
import { View } from 'marionette';

const MyView = View.extend({
  template: _.template(`
    <ul>
    <% _.each(models, function(data) { %>
      <li><%- data.name %></li>
    <% }) %>
    </ul>
  `)
});

const collection = [
  {name: 'Steve'}, {name: 'Helen'}
];

const myView = new MyView({ collection });

How the collection is serialized can also be customized per view.

import BackboneApi from '@mnjs/adapters/backbone';
import _ from 'underscore';
import { setDataApi, View } from 'marionette';

setDataApi(BackboneApi);

const MyView = View.extend({
  serializeCollection() {
    return _.map(this.Data.models(this.collection), model => {
      const data = _.clone(this.Data.serialize(model));

      // serialize a nested Backbone model through the configured adapter
      data.subModel = this.Data.serialize(data.subModel);

      return data;
    });
  }
});

Serializing with a CollectionView#

if you are using a template with a CollectionView that is not also given a model, your CollectionView will serialize the collection for the template. This could be costly and unnecessary. If your CollectionView has a template it is advised to either use an empty model or override the serializeData method.

Adding Context Data#

Marionette views provide a templateContext attribute that is used to add extra information to your templates. This can be either an object, or a function returning an object. The keys on the returned object will be mixed into the model or collection keys and made available to the template.

When serialized data and template context are combined, each contributes its own enumerable properties, including symbols, through object spread. Inherited and non-enumerable properties are ignored. If only one object exists, Marionette passes that original object through unchanged.

import _ from 'underscore';
import { View } from 'marionette';

const MyView = View.extend({
  template: _.template('<h1>Hello, <%- name %></h1>'),
  templateContext: {
    name: 'World'
  }
});

Additionally context data overwrites the serialized data

import BackboneApi from '@mnjs/adapters/backbone';
import _ from 'underscore';
import { setDataApi, View } from 'marionette';

setDataApi(BackboneApi);

const MyView = View.extend({
  template: _.template('<h1>Hello, <%- name %></h1>'),
  templateContext() {
    return {
      name: this.Data.get(this.model, 'name').toUpperCase()
    };
  }
});

You can also define a template context value as a method. How this method is called is determined by your templating solution. For instance with handlebars a method is called with the context of the data passed to the template.

import BackboneApi from '@mnjs/adapters/backbone';
import Handlebars from 'handlebars';
import Backbone from 'backbone';
import { setDataApi, View } from 'marionette';

setDataApi(BackboneApi);

const MyView = View.extend({
  template: Handlebars.compile(`
    <h1{{#if isDr}} class="dr"{{/if}}>Hello {{ fullName }}</h1>,
  `),
  templateContext: {
    isDr() {
      return (this.degree) === 'phd';
    },
    fullName() {
      // Because of Handlebars `this` here is the data object
      // passed to the template which is the result of the
      // templateContext mixed with the serialized data of the view
      return this.isDr() ? `Dr. ${this.name}` : this.name;
    }
  }
});

const myView = new MyView({
  model: new Backbone.Model({ degree: 'masters', name: 'Joe' })
});

Note the data object passed to the template is not deeply cloned and in some cases is not cloned at all. Take caution when modifying the data passed to the template, that you are not also modifying your model's data indirectly.

What is Context Data?#

While serializing data deals more with getting the data belonging to the view into the template, template context mixes in other needed data, or in some cases, might do extra computations that go beyond simply "serializing" the view's model or collection. This fragment assumes an application-specific Backbone model with getOrganization() and getFullName() methods, and a Backbone collection of groups; these helpers are not Marionette APIs.

import BackboneApi from '@mnjs/adapters/backbone';
import _ from 'underscore';
import { CollectionView, setDataApi } from 'marionette';
import GroupView from './group-view';

setDataApi(BackboneApi);

const MyCollectionView = CollectionView.extend({
  tagName: 'div',
  childViewContainer: 'ul',
  childView: GroupView,
  template: _.template(`
    <h1>Hello <%- name %> of <%- orgName %></h1>
    <div>You have <%- stats.public ?? 0 %> group(s).</div>
    <div>You have <%- stats.private ?? 0 %> group(s).</div>
    <h3>Groups:</h3>
    <ul></ul>
  `),
  templateContext() {
    const user = this.model;
    const organization = user.getOrganization();
    const groups = this.collection;

    return {
      orgName: organization.get('name'),
      name: user.getFullName(),
      stats: groups.countBy('type')
    };
  }
})