Modulo Templating Language
This document explains the language syntax and functionality of the included
Modulo template system.
Modulo's templating language is designed to be fairly versatile in
capabilities, while still feel comfortable to those only used to working with
HTML. If you have any exposure to other similar template languages, such as
Django (Python), Shopify's Liquid (Ruby), Hugo (Go), Jinja2 (Python), Nunjucks
(JavaScript), or Twig (PHP), you should feel right at home with Modulo's
templates. In fact, Modulo's templating was modeled so closely on Django's,
that much of this documentation was
heavily inspired by Django's own documentation. This also means that most
text editors already can highlight Modulo template code: Just configure your
editor to syntax highlight Django HTML templates, and you'll be all
set for editing Modulo template or library files. Thanks, Django!
Philosophy
In approach, Modulo's tempting language joins the ranks of the "Djangolike"
templating languages: Intentionally limited, and resembling markup more
than programming code. If you're used to languages which permit mixing
programming code directly with DOM elements, such as with React or Svelte,
you'll want to bear in mind that the Modulo template system is not simply
JavaScript embedded into HTML. This is by design: the template system is
meant to express presentation, not program logic.
The Modulo template system provides "tags" which function similarly to some
programming constructs—an if tag for boolean tests, a for tag for
looping, etc.—but the template system will not execute arbitrary
JavaScript expressions. Only the tags, filters and syntax listed here is
supported by default (although you can add your own extensions to the template
language as needed).
Why use a text-based template instead of an XML-based one (like React's
JSX), or an attribute DOM-based one (like Vue, Angular, or Riot)? The same
reason behind the MVC paradigm: "To separate internal representations of
information from the ways information is presented to and accepted from the
user". Supporting pure-text output enforces this clear wall of separation
between presentation and business logic. Furthermore, this will be more
familiar to backend developers more familiar with string-based templating
in general (such as Ruby, Python, or PHP developers).
#
Templates
A "template" is the text within a Template CPart. Most Components defined
within Modulo will also define a Template CPart in order to render their
contents.
A template contains variables, which get replaced with values when the
template is evaluated, and tags, which control the logic of the template.
Below is a minimal template that illustrates a few basics. Each element will
be explained later in this document.
There are 42
articles
on ModuloNews.
MODULO RELEASED!
The most exciting news of the…
CAN JS BE FUN AGAIN?
MTL CONSIDERED HARMFUL
Why constructing JS is risky …
#
Variables
Variables look like this: {{ variable }}
. When the template
engine encounters a variable, it evaluates that variable and replaces it with
the result. Variable names consist of any combination of alphanumeric
characters and the underscore ("_") but may not start with an underscore, and
may not be a number. The dot (".") also appears in variable sections, although
that has a different meaning, as indicated below. Importantly, you cannot have
spaces or punctuation characters in variable names.
The dot (.)
Use a dot (.) to access sub-properties of a variable. This syntax is much
like JavaScript.
In the above example, {{ state.count }}
will be replaced with
the value of the "count" property of the state data. Similarly,
{{ script.exports.title }}
reaches even further, by first
accessing the script
variable, then the exports
subproperty, then the title
subproperty.
If you use a variable that doesn't exist, the template system will insert
the value of undefined
, much like in JavaScript.
Note that "bar" in a template expression like {{ foo.bar }}
will be interpreted as a literal string property of "foo', and not a variable
"bar", even if one exists in the template context. If you wish to to do the
opposite behavior, that is, actually resolve "bar" into it's own value, and
then use that value to access a property of bar, consider using the
get
filter: {{ foo|get:bar }}
. This is
equivalent to the "square brackets" notation of JavaScript (e.g.
foo[bar]
), and thus is also useful for accessing numeric indices.
For an example, if we have an array in our state like
foo:='["a", "b", "c"]'
, then we can access
"b"
as follows: {{ state.foo|get:1 }}
.
Variable attributes that begin with an underscore should generally not be
accessed, as that can be used to indicate private.
What variables are available
Variables come from the component's renderObj
that is produced
in the prepare
lifecycle phase. More on the "renderObj" in the section
on lifecycle. In practicality, this is another way to say that most
variables typically "come from" the CParts of a component. That is,
state
will provide the state values, props
will
provide the values passed down to this component, and finally
script
will provide variables from script tags. In other words,
each CPart "decides" which variables it supplies to the Template based on it's
"prepare" lifecycle behavior. See the "Variables from CParts" section below for
a more thorough look at this.
Custom variables using the Script "prepare" callback
If you want to create computed variables (like in Vue.js), you can create a
custom prepare
lifecycle callback in your Script CPart tag as
follows. Examine the following example:
Variables from CParts
Each CPart within a given component may contribute a variable to
the Template. Of the built-in CParts, the following contribute a
variable with useful data:
- Props - If the Props CPart is present, then
you will have a
{{ props }}
variable available. This gives
access to the current value of props data. For example,
{{ props.title }}
gives access to the current value of the
"title" attribute of the component.
- State - If the State CPart is present, then
you will have a
{{ state }}
variable available. This gives
access to the current value of state data, the object that represents
state. For example, {{ state.number }}
gives access to the
current value of the "number" property of state.
- Script - If the Script CPart is present, then
you will have a
{{ script }}
variable available. The main
uses of this for access to computed variables from the
prepareCallback
. Another use is to access
script.exports
variable as a way to store global, static data
shared between all instances of a component.
Variables from templatetags
We'll go over template-tags later, but some template-tags can add variables
into the mix as well. As a sneak-peak, examine the following code:
Note how athlete
declares a new variable, which can be reused
in {{ athlete.name }}
. For more on for-loops, see the
built-in
template-tag reference.
#
Filters
You can modify variables for display by using filters.
Filters look like this: {{ name|lower }}
. This displays the
value of the {{ name }}
variable after being filtered through the
lower filter, which converts text to lowercase. Use a pipe (|) to apply a
filter.
Filters can be "chained." The output of one filter is applied to the next.
This chaining feature can be applied, like in
{{ state.color|allow:"red,blue"|default:"green" }}
,
which is a way to conditionally allow for only certain strings to be in
state.color
, while providing a default if none were specified.
Some filters take arguments. A filter argument looks like this:
{{ state.bio|truncate:30 }}
.
This will display the first 30 characters of the bio variable, possibly
appending an ellipsis if its full length exceeds that.
Filter arguments that contain spaces must be quoted; for example, to join a
list with commas and spaces you'd use {{ state.list|join:", " }}
.
Modulo's templating language provides several dozen built-in template
filters. You can read all about them in the
built-in filter reference.
To give you a taste of what's available, here are some of the more commonly
used template filters:
Common filters
default
If a variable is false or empty, use given default. Otherwise, use the value of
the variable. For example:
{{ value|default:"nothing" }}
If value isn't provided or is empty, the above will display "nothing".
length
Returns the length of the value. This works for both strings and lists. For
example:
{{ state.articles|length }}
If state.items
is ['a', 'b', 'c', 'd']
, the output
will be 4.
Again, these are just a few examples; see the built-in filter reference.
for the complete list.
You can also create your own custom template filters; see Custom template
tags and filters.
#
Tags
Tags look like this: {% tag %}
(except with the word "tag"
replaced with something else). Tags can be more complex than variables: Some
create text in the output, some control flow by performing loops or logic, and
some load external information into the template to be used by later
variables.
Most built-in tags require beginning and ending tags, in the format of:
{% tag %}...contents...{% endtag %}
Modulo ships with several built-in template-tags. You can read all about
them in the built-in template-tag reference.
Common Template-Tags
Here are a couple commonly used template-tags:
for
Duplicate a block of HTML code for every item in a collection of items (e.g.
an array). This allows a HTML code to be repeated while looping over each item
in an array. For example, to display a list of athletes, assuming one was
stored as an array of objects within state.athletes
:
if
Evaluates a variable, and if that variable is "true" the contents of the
block are displayed:
In the above, if state.athletes is not empty, the number of athletes will be
displayed by the {{ state.athletes|length }} variable.
You can also use filters and various operators in the if tag:
Final notes on tags
The most important difference between template tags and template filters are
that template tags are interpreted at "compile-time" (when the template is
first loaded or when you are building a JavaScript build or bundle), while
filters are interpreted at "render-time" (when the component is outputting it's
HTML into the DOM). This means that mistakes with template tags might stop the
component from rendering at all (mistakes such as syntax typo or a buggy
third-party template tag), while mistakes with filters may only be detected
when it tries using it, and could even be overlooked if the filter doesn't get
used, e.g. it's only within an if statement that you haven't tested.
Also, while the above examples work, be aware that some template filters
return strings, so mathematical comparisons using filters will generally not
work as you expect. |length
is an exception. You can use
|number
in other cases.
Finally, just like filters, you can also create your own custom template
tags; see Templating Reference.
Comments
To comment-out part of a line in a template, use the comment syntax:
{# text in here is ignored #}
. This syntax should only be used for
single-line comments. Due to this, it's not intended for "commenting out" or
disabling portions of template code.
If you need to comment out a multiline portion of the template, especially a
block of other template code, use the comment tag instead syntax instead. This
looks like: {% comment %}text in here is ignored{% endcomment %}
Examples of both are below:
hello
Below the greeting...
#
Debugging
Modulo templates generate JavaScript code which mirrors the logic found in
the template. Like all generated code in Modulo, it gets appended to the
<head>
as a <script>
tag. When inspecting
the resulting JavaScript code, note the JavaScript comments to each line
indicating the Templating text that corresponds, so if a Modulo template is
behaving unexpectedly, you can examine the actual code getting generated.
Modulo templates also come equipped with the
{% debugger %}
template tag. This will insert a
debugger;
JavaScript statement. This "freezes" your app in time.
After halting execution at the given statement, your browser's Developer Tools
will allow you to inspect the values of local variables in the generated
Template code itself. Note that the CTX
variable refers to the
Template render context (which, in the case of Modulo components, refers to the
renderObj
, or the object that has script
,
state
etc. properties generated by component parts). This is
typically what you'll want to poke around on to figure out why something is
broken. Example below, with a comment hinting at a story as to why the debugger
was used:
Note: For obvious reasons, it's very important to remember
to delete all your "debugger" statements before you release your code, or your
app may freeze for other people as well.
#
Escaping
By default in Modulo, every template automatically escapes the output of
every variable value. Specifically, these five characters are escaped:
- > is converted to <
- < is converted to >
- ' (single quote) is converted to '
- " (double quote) is converted to "
- & is converted to &
Why escape
When generating HTML from templates, there's always a risk that a variable
will include characters that affect the resulting HTML. For example, consider
this template fragment:
<p>Hello, {{ state.name }}</p>
Imagine if a user were to enter their name as
</p><h1>Big
. If the template system were to insert
that value directly, without modification, it would result in the P tag getting
closed prematurely, and a H1 tag taking over, making the text large.
Clearly, user-submitted data shouldn't be inserted directly into your
Web pages, at least without some sort of validation. A malicious user could
even use this vulnerability to add in JavaScript behavior that acts on behalf
of other users, such as by sending requests to an API. To avoid this problem,
Modulo uses automatic escaping.
How to turn it off
Sometimes template variables contain trusted data that you actually intend
to be rendered as raw HTML, in which case you don't want their contents to be
escaped. For example, you might want to render a string of HTML in your state
that was generated by JS on the front-end, or a string of HTML loaded from a
trusted source, such as a staff-only database.
To disable auto-escaping for an individual variable, use the safe
filter:
This will be escaped: {{ state.content }}
This will not be escaped: {{ state.content|safe }}
Think of safe as shorthand for safe from further escaping or can be safely
interpreted as HTML. In this example, if state.content
was defined
like content="<b>"'
, the output will be:
This will be escaped: <b>
This will not be escaped: <b>
Keep in mind that you should only mark trusted data as |safe
.
You don't want anyone trying to slip in malicious JS behavior! How to validate
if a bit of HTML is safe to include verbatim (e.g. with |safe
) is
outside the scope of this document, but generally you should only trust HTML
that has been validated on the backend or database, whitelisting only certain
features of HTML and checking for "red flags" like embedded JavaScript code, or
if it's a "staff-only" feature like a team blog, and thus the content generated
is only editable by a trusted source (e.g. yourself or a member of your team).
Examine the code below for a concrete example of how this escaping behaves:
User "Little <Bobby> <Drop> &tables" sent a message:
I
love the classic
xkcd #327 on
the risk of trusting
user inputted data