Working with Templates
After we've constructed views from models and collections, we need to show this to our users. This section covers how to render dynamic data and the different approaches we can take using Marionette to to this.
A simple template
With a LayoutView
we can render a template quite easily:
var MyLayout = Marionette.LayoutView.extend({
template: require('mylayouttemplate.html')
});
With our template mylayouttemplate.html
as something like:
<h1>Hello, world</h1>
<p>We have something to talk about</p>
When we show
or render
our view, we can see the template that we've just
created. This isn't very interesting, however, so let's take it a step further
using some model data:
var mymodel = new Backbone.Model({
name: 'Scott'
});
var layout = new MyLayout({model: mymodel});
layout.render();
Let's change our template before compiling this:
<h1>Hello, <%- name %></h1>
<p>I now know your name</p>
Our page is now a little more interesting - we can change it based on the model data we pass into our view. With just this knowledge we can, and will, build plenty of complex web applications.
Template syntax
This syntax comes from Underscore's template engine; an extremely simple template engine that does most of what we need. We'll quickly go through its syntax here.
Output data
There are two basic ways to output data in an underscore template: escaped and unescaped.
Escaped data
The default syntax we'll use is the familiar <%- %>
syntax from above. This
will cause Underscore to escape HTML strings to make them safe. This should be
your default goto, especially for user-entered data:
<h1><%- heading %></h1>
<p><%- user_data %></p>
Let's say our user has the following model:
{
heading: 'Some text',
user_data: '<script>alert("test")</script>'
}
Once rendered, our HTML would be:
<h1>Some text</h1>
<p><script>alert("test")</script></p>
The escaping has protected us from potential cross-site scripting (XSS) attacks.
Unescaped data
On occasion, we need to output data exactly as it is entered into our model. For example, we may have pre-escaped our output, or we explicitly want to output HTML that the browser will render (for example, rendered Markdown).
In this case, we can tell Underscore to output our data as-is with <%= %>
.
<h1><%- heading %></h1>
<%= user_data %>
With the same model as above, the raw output will be:
<h1>Some text</h1>
<script>alert("test")</script>
Causing the alert
code to execute.
When to escape and when not to
By default, we should prefer <%- %>
escaping over <%= %>
except in rare,
well-defined cases. The cases are:
- When the output comes from a trusted source
- When the output has been processed by a template helper
In each of these cases, the output to be printed must contain HTML markup that
you want to render - otherwise we should just use <%- %>
and get the same
effect.
Accessing undefined fields
Some template engines (such as Django templates) allow us to attempt to display variables that haven't been defined - rendering an empty string. Underscore templates don't have this property; attempting to access an undefined variable causes an error.
For example, take the following model:
{
heading: 'Scott'
}
The following template can't render and will raise an error instead:
<h1><%- heading %></h1>
<%- user_data %>
Introducing JavaScript logic
Underscore templates allow us to execute JavaScript inline with the template
using <% %>
. With this syntax we can conditionally display data or iterate
through an array.
Let's take the following models:
{
heading: 'Some header',
data: [
'string 1',
'string 2'
]
}
and an empty model:
{
heading: null,
data: []
}
<% if (heading === null) { %>
<h1>New Item</h1>
<% } else { %>
<h1><%- heading %></h1>
<% } %>
For our populated model, the following is output:
<h1>Some header</h1>
and our empty model outputs:
<h1>New Item</h1>
We get access to the _
namespace in our templates too, making it easier to
iterate:
<ul>
<% _.each(data, function(item) { %>
<li><%- item %></li>
<% }) %>
</ul>
Again, with our populated model, the output is:
<ul>
<li>string 1</li>
<li>string 2</li>
</ul>
while our empty model simply looks like:
<ul>
</ul>
Template Helpers
As we can see, Underscore's template language is pretty basic and gives us
very little to work with. For example, setting variables or custom logic isn't
well supported, and attempting to access an undefined variable causes the entire
template to not render. In addition, if we want to start introducing some
complex logic, the template itself will quickly become unwieldy. We can mitigate
some of this complexity using views and layouts but, when we simply
want to validate data or format it, we turn to the templateHelpers
attribute
on our view.
The templateHelpers
attribute lets us assign an object - or function returning
and object - whose keys will be available in the template. Let's build a simple
template helper that outputs some information about a basket.
Our desired template is:
<p>
Your basket total is <%- toCurrency(total) %> and contains
<%- count(items) %> items.
</p>
and our model is:
{
items: [
{description: "An item", cost: "30.00"},
{description: "Another item", cost: "5.00"}
],
total: "35.00"
}
The output we'd want to get would be:
<p>
Your basket total is £35.00 and contains 2 items.
</p>
Let's define a simple template helper on our view:
var BasketView = Marionette.LayoutView.extend({
template: require('./basket.html'),
templateHelpers: {
count: function(items) {
return items.length;
},
toCurrency: function(total) {
var totalFloat = parseFloat(total);
return '£' + totalFloat.toFixed(2);
}
}
});
Now, our template can see the keys count
and toCurrency
and can call these
functions directly. Another key advantage of this method is reusability - it's
simple to just import a function call and attach it to our templateHelpers
as
such:
var currencyFormatter = require('./helpers/currency');
var arrayHelpers = require('./helpers/array');
var BasketView = Marionette.LayoutView.extend({
template: require('./basket.html'),
templateHelpers: {
count: arrayHelpers.count,
toCurrency: currencyFormatter
}
});
Returning a function
The templateHelpers
attribute can also take a function and call it for you.
This is especially useful in the case where we have potentially undefined
variables on our model:
var currencyFormatter = require('./helpers/currency');
var BasketView = Marionette.LayoutView.extend({
template: require('./basket.html'),
templateHelpers: function() {
var items = this.model.get('items');
var total = this.model.get('total');
var count = _.isUndefined(items) ? 0 : items.length;
var cleanTotal = _.isUndefined(total) ? '0.00' : total;
return {
count: count,
totalAsCurrency: currencyFormatter(cleanTotal)
};
}
});
We can modify our template slightly to look like:
<p>
Your basket total is <%- totalAsCurrency %> and contains
<%- count %> items.
</p>
as our view has pre-processed the values for us.