Spacebars is a Meteor template language inspired by Handlebars. It shares some of the spirit and syntax of Handlebars, but it has been tailored to produce reactive Meteor templates when compiled.
Getting Started
A Spacebars template consists of HTML interspersed with template tags, which are
delimited by {{
and }}
(two curly braces).
1 | <template name="myPage"> |
As illustrated by the above example, there are four major types of template tags:
{{pageTitle}}
- Double-braced template tags are used to insert a string of text. The text is automatically made safe. It may contain any characters (like<
) and will never produce HTML tags.{{> nav}}
- Inclusion template tags are used to insert another template by name.{{#each}}
- Block template tags are notable for having a block of content. The block tags#if
,#each
,#with
, and#unless
are built in, and it is also possible define custom ones. Some block tags, like#each
and#with
, establish a new data context for evaluating their contents. In the above example,{{title}}
and{{content}}
most likely refer to properties of the current post (though they could also refer to template helpers).{{{content}}}
- Triple-braced template tags are used to insert raw HTML. Be careful with these! It’s your job to make sure the HTML is safe, either by generating it yourself or sanitizing it if it came from a user input.
Reactivity Model
Spacebars templates update reactively at a fine-grained level in response to changing data.
Each template tag’s DOM is updated automatically when it evaluates to a new
value, while avoiding unnecessary re-rendering as much as possible. For
example, a double-braced tag replace its text node when its text value changes.
An #if
re-renders its contents only when the condition changes from truthy to
falsy or vice versa.
Identifiers and Paths
A Spacebars identifier is either a JavaScript identifier name or any string
enclosed in square brackets ([
and ]
). There are also the special
identifiers this
(or equivalently, .
) and ..
. Brackets are required to
use one of the following as the first element of a path: else
, this
, true
,
false
, and null
. Brackets are not required around JavaScript keywords and
reserved words like var
and for
.
A Spacebars path is a series of one or more identifiers separated by either .
or /
, such as foo
, foo.bar
, this.name
, ../title
, or foo.[0]
(numeric indices must be enclosed in brackets).
Name Resolution
The first identifier in a path is resolved in one of two ways:
Indexing the current data context. The identifier
foo
refers to thefoo
property of the current data context object.As a template helper. The identifier
foo
refers to a helper function (or constant value) that is accessible from the current template.
Template helpers take priority over properties of the data context.
If a path starts with ..
, then the enclosing data context is used instead of
the current one. The enclosing data context might be the one outside the
current #each
, #with
, or template inclusion.
Path Evaluation
When evaluating a path, identifiers after the first are used to index into the
object so far, like JavaScript’s .
. However, an error is never thrown when
trying to index into a non-object or an undefined value.
In addition, Spacebars will call functions for you, so {{foo.bar}}
may be
taken to mean foo().bar
, foo.bar()
, or foo().bar()
as appropriate.
Similarly, if the accessed object is wrapped in a Promise
, Spacebars will
defer the path evaluation in a Promise
as well. That is,
{{foo.bar}}
will evaluate to foo().then(x => x.bar)
.
Both pending and rejected states will result in undefined
.
Helper Arguments
An argument to a helper can be any path or identifier, or a string, boolean, or number literal, or null.
Double-braced and triple-braced template tags take any number of positional and keyword arguments:
1 | {{frob a b c verily=true}} |
calls:
1 | frob(a, b, c, Spacebars.kw({verily: true})) |
Spacebars.kw
constructs an object that is instanceof Spacebars.kw
and whose
.hash
property is equal to its argument.
The helper’s implementation can access the current data context as this
.
If any of the arguments is a Promise
, Spacebars will defer the call as long as
all of the arguments will resolve. That is, {{foo x y z}}
will evaluate to Promise.all([x, y, z]).then(args => foo(...args))
. Both
pending and rejected states will result in undefined
.
Inclusion and Block Arguments
Inclusion tags ({{> foo}}
) and block tags ({{#foo}}
) take a single
data argument, or no argument. Any other form of arguments will be interpreted
as an object specification or a nested helper:
Object specification: If there are only keyword arguments, as in
{{#with x=1 y=2}}
or{{> prettyBox color=red}}
, the keyword arguments will be assembled into a data object with properties named after the keywords.Nested Helper: If there is a positional argument followed by other (positional or keyword arguments), the first argument is called on the others using the normal helper argument calling convention.
Template Tag Placement Limitations
Unlike purely string-based template systems, Spacebars is HTML-aware and designed to update the DOM automatically. As a result, you can’t use a template tag to insert strings of HTML that don’t stand on their own, such as a lone HTML start tag or end tag, or that can’t be easily modified, such as the name of an HTML element.
There are three main locations in the HTML where template tags are allowed:
- At element level (i.e. anywhere an HTML tag could go)
- In an attribute value
- In a start tag in place of an attribute name/value pair
The behavior of a template tag is affected by where it is located in the HTML, and not all tags are allowed at all locations.
Double-braced Tags
A double-braced tag at element level or in an attribute value typically evalutes
to a string. If it evalutes to something else, the value will be cast to a
string, unless the value is null
, undefined
, or false
, which results in
nothing being displayed. Promise
s are also supported – see below.
Values returned from helpers must be pure text, not HTML. (That is, strings
should have <
, not <
.) Spacebars will perform any necessary escaping if
a template is rendered to HTML.
Async Content
The values can be wrapped in a Promise
. When that happens, it will be treated
as undefined
while it’s pending or rejected. Once resolved, the resulting
value is used. To have more fine-grained handling of non-resolved states, use
#let
and the async state helpers (e.g., @pending
).
Please note that the values rendered this way create new
View
objects on everyPromise
change and it may result in flickering, i.e., disappearing for a brief moment before appearing again. We plan to optimize it in the next versions. As a workaround, use#let
to unpack the value.
SafeString
If a double-braced tag at element level evalutes to an object created with
Spacebars.SafeString("<span>Some HTML</span>")
, the HTML is inserted at the
current location. The code that calls SafeString
is asserting that this HTML
is safe to insert.
In Attribute Values
A double-braced tag may be part of, or all of, an HTML attribute value:
1 | <input type="checkbox" class="checky {{moreClasses}}" checked={{isChecked}}> |
An attribute value that consists entirely of template tags that return null
,
undefined
, or false
is considered absent; otherwise, the attribute is
considered present, even if its value is empty.
Async Attributes
The values can be wrapped in a Promise
. When that happens, it will be treated
as undefined
while it’s pending or rejected. Once resolved, the resulting
value is used. To have more fine-grained handling of non-resolved states, use
#let
and the async state helpers (e.g., @pending
).
Dynamic Attributes
A double-braced tag can be used in an HTML start tag to specify an arbitrary set of attributes:
1 | <div {{attrs}}>...</div> |
The tag must evaluate to an object that serves as a dictionary of attribute name
and value strings. For convenience, the value may also be a string or null. An
empty string or null expands to {}
. A non-empty string must be an attribute
name, and expands to an attribute with an empty value; for example, "checked"
expands to {checked: ""}
(which, as far as HTML is concerned, means the
checkbox is checked).
To summarize:
Return Value | Equivalent HTML |
---|---|
"" or null or {} | |
"checked" or {checked: ""} | checked |
{checked: "", 'class': "foo"} | checked class=foo |
{checked: false, 'class': "foo"} | class=foo |
"checked class=foo" | ERROR, string is not an attribute name |
You can combine multiple dynamic attributes tags with other attributes:
1 | <div id=foo class={{myClass}} {{attrs1}} {{attrs2}}>...</div> |
Attributes from dynamic attribute tags are combined from left to right, after
normal attributes, with later attribute values overwriting previous ones.
Multiple values for the same attribute are not merged in any way, so if attrs1
specifies a value for the class
attribute, it will overwrite {{myClass}}
.
As always, Spacebars takes care of recalculating the element’s attributes if any
of myClass
, attrs1
, or attrs2
changes reactively.
Async Dynamic Attributes
The dynamic attributes can be wrapped in a Promise
. When that happens, they
will be treated as undefined
while it’s pending or rejected. Once resolved,
the resulting value is used. To have more fine-grained handling of non-resolved
states, use #let
and the async state helpers (e.g., @pending
).
Triple-braced Tags
Triple-braced tags are used to insert raw HTML into a template:
1 | <div class="snippet"> |
The inserted HTML must consist of balanced HTML tags. You can’t, for example,
insert "</div><div>"
to close an existing div and open a new one.
This template tag cannot be used in attributes or in an HTML start tag.
Async Content
The raw HTML can be wrapped in a Promise
. When that happens, it will not
render anything if it’s pending or rejected. Once resolved, the resulting value
is used. To have more fine-grained handling of non-resolved states, use #let
and the async state helpers (e.g., @pending
).
Inclusion Tags
An inclusion tag takes the form {{> templateName}}
or {{> templateName
dataObj}}
. Other argument forms are syntactic sugar for constructing a data
object (see Inclusion and Block Arguments).
An inclusion tag inserts an instantiation of the given template at the current location. If there is an argument, it becomes the data context, much as if the following code were used:
1 | {{#with dataObj}} |
Instead of simply naming a template, an inclusion tag can also specify a path that evalutes to a template object, or to a function that returns a template object.
Note that the above two points interact in a way that can be surprising!
If foo
is a template helper function that returns another template, then
{{>foo bar}}
will first push bar
onto the data context stack then call
foo()
, due to the way this line is expanded as shown above. You will need to
use Template.parentData(1)
to access the original context. This differs
from regular helper calls like {{foo bar}}
, in which bar
is passed as a
parameter rather than pushed onto the data context stack.
Function Returning a Template
If an inclusion tag resolves to a function, the function must return a template
object or null
. The function is reactively re-run, and if its return value
changes, the template will be replaced.
Block Tags
Block tags invoke built-in directives or custom block helpers, passing a block of template content that may be instantiated once, more than once, or not at all by the directive or helper.
1 | {{#block}} |
Block tags may also specify “else” content, separated from the main content by
the special template tag {{else}}
.
A block tag’s content must consist of HTML with balanced tags.
Block tags can be used inside attribute values:
1 | <div class="{{#if done}}done{{else}}notdone{{/if}}"> |
You can chain block tags:
1 | {{#foo}} |
This is equivalent to:
1 | {{#foo}} |
If/Unless
An #if
template tag renders either its main content or its “else” content,
depending on the value of its data argument. Any falsy JavaScript value
(including null
, undefined
, 0
, ""
, and false
) is considered false, as
well as the empty array, while any other value is considered true.
1 | {{#if something}} |
#unless
is just #if
with the condition inverted.
Async Conditions
The condition can be wrapped in a Promise
. When that happens, both #if
and
#unless
will not render anything if it’s pending or rejected. Once resolved,
the resulting value is used. To have more fine-grained handling of non-resolved
states, use #let
and the async state helpers (e.g., @pending
).
With
A #with
template tag establishes a new data context object for its contents.
The properties of the data context object are where Spacebars looks when
resolving template tag names.
1 | {{#with employee}} |
We can take advantage of the object specification form of a block tag to define an object with properties we name:
1 | {{#with x=1 y=2}} |
If the argument to #with
is falsy (by the same rules as for #if
), the
content is not rendered. An “else” block may be provided, which will be
rendered instead.
If the argument to #with
is a string or other non-object value, it may be
promoted to a JavaScript wrapper object (also known as a boxed value) when
passed to helpers, because JavaScript traditionally only allows an object for
this
. Use String(this)
to get an unboxed string value or Number(this)
to
get an unboxed number value.
Each
An #each
template tag takes a sequence argument and inserts its content for
each item in the sequence, setting the data context to the value of that item:
1 | <ul> |
The newer variant of #each
doesn’t change the data context but introduces a
new variable that can be used in the body to refer to the current item:
1 | <ul> |
The argument is typically a Meteor cursor (the result of collection.find()
,
for example), but it may also be a plain JavaScript array, null
, or
undefined
.
An “else” section may be provided, which is used (with no new data context) if there are zero items in the sequence at any time.
You can use a special variable @index
in the body of #each
to get the
0-based index of the currently rendered value in the sequence.
Async Sequences
The sequence argument can be wrapped in a Promise
. When that happens, #each
will render the “else” if it’s pending or rejected. Once resolved, the resulting
sequence is used. To have more fine-grained handling of non-resolved states, use
#let
and the async state helpers (e.g., @pending
).
Reactivity Model for Each
When the argument to #each
changes, the DOM is always updated to reflect the
new sequence, but it’s sometimes significant exactly how that is achieved. When
the argument is a Meteor live cursor, the #each
has access to fine-grained
updates to the sequence – add, remove, move, and change callbacks – and the
items are all documents identified by unique ids. As long as the cursor itself
remains constant (i.e. the query doesn’t change), it is very easy to reason
about how the DOM will be updated as the contents of the cursor change. The
rendered content for each document persists as long as the document is in the
cursor, and when documents are re-ordered, the DOM is re-ordered.
Things are more complicated if the argument to the #each
reactively changes
between different cursor objects, or between arrays of plain JavaScript objects
that may not be identified clearly. The implementation of #each
tries to be
intelligent without doing too much expensive work. Specifically, it tries to
identify items between the old and new array or cursor with the following
strategy:
- For objects with an
_id
field, use that field as the identification key - For objects with no
_id
field, use the array index as the identification key. In this case, appends are fast but prepends are slower. - For numbers or strings, use their value as the identification key.
In case of duplicate identification keys, all duplicates after the first are
replaced with random ones. Using objects with unique _id
fields is the way to
get full control over the identity of rendered elements.
Let
The #let
tag creates a new alias variable for a given expression. While it
doesn’t change the data context, it allows to refer to an expression (helper,
data context, another variable) with a short-hand within the template:
1 | {{#let name=person.bio.firstName color=generateColor}} |
Variables introduced this way take precedence over names of templates, global helpers, fields of the current data context and previously introduced variables with the same name.
Additionally, #let
is capable of unwrapping Promise
objects. That is, if
any of the bindings is to one, the bound value won’t be a Promise
, but the
resolved value instead. Both pending and rejected states will result in
undefined
.
Async States
There are three global helpers used to query the state of the bound Promise
s:
@pending
, which checks whether any of the given bindings is still pending.@rejected
, which checks whether any of the given bindings has rejected.@resolved
, which checks whether any of the given bindings has resolved.
1 | {{#let name=getNameAsynchronously}} |
All of them accept a list of names to check. Passing no arguments is the same as
passing all bindings from the inner-most #let
.
1 | {{#let name=getNameAsynchronously}} |
Async Synchronization
The bindings are not synchronized. That means, bindings store that latest
resolved value, not value of the latest Promise
. If the resolution time
varies (e.g., involves network), it may result in desynchronized UI. In the
below example, the rendered text is not guaranteed to be the result of the
latest getName
execution.
1 | <template name="example"> |
1 | Template.example.helpers({ |
If a well-defined order of resolutions is required, consider using an external synchronization mechanism, e.g., a queue of pending async operations.
Custom Block Helpers
To define your own block helper, simply declare a template, and then invoke it
using {{#someTemplate}}
(block) instead of {{> someTemplate}}
(inclusion)
syntax.
When a template is invoked as a block helper, it can use {{>
Template.contentBlock}}
and {{> Template.elseBlock}}
to include the block
content it was passed.
Here is a simple block helper that wraps its content in a div:
1 | <template name="note"> |
You would invoke it as:
1 | {{#note}} |
Here is an example of implementing #unless
in terms of #if
(ignoring for the
moment that unless
is a built-in directive):
1 | <template name="unless"> |
Note that the argument to #unless
(the condition) becomes the data context in
the unless
template and is accessed via this
. However, it would not work
very well if this data context was visible to Template.contentBlock
, which is
supplied by the user of unless
.
Therefore, when you include {{> Template.contentBlock}}
, Spacebars hides the
data context of the calling template, and any data contexts established in the
template by #each
and #with
. They are not visible to the content block,
even via ..
. Put another way, it’s as if the {{> Template.contentBlock}}
inclusion occurred at the location where {{#unless}}
was invoked, as far as
the data context stack is concerned.
You can pass an argument to {{> Template.contentBlock}}
or {{>
Template.elseBlock}}
to invoke it with a data context of your choice. You can
also use {{#if Template.contentBlock}}
to see if the current template was
invoked as a block helper rather than an inclusion.
Comment Tags
Comment template tags begin with {{!
and can contain any characters except for
}}
. Comments are removed upon compilation and never appear in the compiled
template code or the generated HTML.
1 | {{! Start of a section}} |
Comment tags also come in a “block comment” form. Block comments may contain
{{
and }}
:
1 | {{!-- This is a block comment. |
Comment tags can be used wherever other template tags are allowed.
Nested sub-expressions
Sometimes an argument to a helper call is best expressed as a return value of some other expression. For this and other cases, one can use parentheses to express the evaluation order of nested expressions.
1 | {{capitalize (getSummary post)}} |
In this example, the result of the getSummary
helper call will be passed to
the capitalize
helper.
Sub-expressions can be used to calculate key-word arguments, too:
1 | {{> tmpl arg=(helper post)}} |
HTML Dialect
Spacebars templates are written in standard HTML extended with additional syntax (i.e. template tags).
Spacebars validates your HTML as it goes and will throw a compile-time error if you violate basic HTML syntax in a way that prevents it from determining the structure of your code.
Spacebars is not lenient about malformed markup the way a web browser
is. While the latest HTML spec standardizes how browsers should
recover from parse errors, these cases are still not valid HTML. For
example, a browser may recover from a bare <
that does not begin a
well-formed HTML tag, while Spacebars will not. However, gone are the
restrictions of the XHTML days; attribute values do not have to
quoted, and tags are not case-sensitive, for example.
You must close all HTML tags except the ones specified to have no end
tag, like BR, HR, IMG and INPUT. You can write these tags as <br>
or equivalently <br/>
.
The HTML spec allows omitting some additional end tags, such as P and LI, but Spacebars doesn’t currently support this.
Top-level Elements in a .html
file
Technically speaking, the <template>
element is not part of the Spacebars
language. A foo.html
template file in Meteor consists of one or more of the
following elements:
<template name="myName">
- The<template>
element contains a Spacebars template (as defined in the rest of this file) which will be compiled to theTemplate.myName
component.<head>
- Static HTML that will be inserted into the<head>
element of the default HTML boilerplate page. Cannot contain template tags. If<head>
is used multiple times (perhaps in different files), the contents of all of the<head>
elements are concatenated.<body>
- A template that will be inserted into the<body>
of the main page. It will be compiled to theTemplate.body
component. If<body>
is used multiple times (perhaps in different files), the contents of all of the<body>
elements are concatenated.
Escaping Curly Braces
To insert a literal {{
, {{{
, or any number of curly braces, put a
vertical bar after it. So {{|
will show up as {{
, {{{|
will
show up as {{{
, and so on.