create-modulo
Version:
Starter projects for Modulo.html - Ready for all uses - Markdown-SSG / SSR / API-backed SPA
758 lines (594 loc) • 29.1 kB
HTML
<meta charset=utf8><script src=../static/Modulo.html></script><script type=md>---
title: Templating Language
---
This document explains the language syntax and functionality of the included
Modulo template system.
> **Modulo's Declarative Templating Approach** - 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 this documentation page was [modelled on Django's own
> documentation](https://docs.djangoproject.com/en/4.2/ref/templates/language/).
> 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!
# 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 an example template that shows off several features of templates. Each
element will be explained later in this document.
```modulo
edit:demo=modulo
<Template>
<h1>{{ script.title }}</h1>
<p>There are <em>{{ state.count }} articles.</em></p>
{# Show the articles #}
{% for article in state.articles %}
<h4 style="color: tomato">{{ article.headline|upper }}</h4>
{% if article.tease %}
<p>{{ article.tease|truncate:30 }}</p>
{% endif %}
{% endfor %}
</Template>
<!-- The data below was used to render the template above -->
<State
count:=42
articles:=[]
articles.0:={}
articles.1:={}
articles.2:={}
articles.0.headline="Modulo released"
articles.1.headline="The experts ask"
articles.2.headline="Controversy ahead"
articles.0.tease="The most exciting news of the century."
articles.1.tease="Can HTML be fun again?"
articles.2.tease="Why constructing JS is risky business."
></State>
<Script>
function prepareCallback() {
return {
title: "Modulo News",
}
}
/Script>
```
> **Whay makes Modulo a Declarative String Templating different than JSX and/or
> other frameworks?** - If you are used to frontend frameworks which permit
> mixing functions and imperative code directly with DOM elements, such as with
> React.js or Svelte, or using XML-based DOM construction syntax, like JSX,
> then there is an important difference to note: Modulo templates are
> string-based, and intended to resembled declarative mark-up. When compared to
> a general language like JavaScript (which Modulo templates compile into),
> it's intentionally limited: While the Modulo template language provides
> "template tags" that resemble programming constructs (`if`, `for`, etc), it
> does not permit any embedded JavaScript, but instead only a few constructs
> and operations. This declarative, string-based templating paradigm was
> chosen with 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).
## Variables
Variables look like this: `{{ my-variable-name }}`. They may also look like `{{
myVariable }}`. Variable names should start with an alphabetical character, and
otherwise be alphanumeric (e.g., `A-Z` and `0-9`). Importantly, variable names
cannot have spaces (e.g. ` `), special characters (e.g. `#`), or punctuation
characters outside of underscore (`_`), dash (`-`, described below in [Variable
syntax](#VariableSyntax)), or the dot (`.`, described next).
### Drilling down: The dot (.)
When using template variables, you often want to access data that has been
structured using the various JSON data structures. That is, you might be
accessing arrays of numbers for a chart you are rendering on a dashboard, or an
object containing user information from a database for a profile page.
"Digging" into different data structures to unearth the data you want is a
process sometimes nicknamed "drilling down". In Modulo, the dot (`.`) character
is used to "drill down", or 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.data.title }}`
reaches even further, by first accessing the `script` variable, then the `data`
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. 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.
### Variables from Script
If you want to create computed variables (like in Vue.js), you can create a
custom `prepareCallback` lifecycle callback in your Script CPart tag as follows.
Examine the following example:
```modulo
edit:demo=modulo
<Template>
<input state.bind name="perc" />% of
<input state.bind name="total" />
is: {{ script.calcResult }}
</Template>
<State
perc:=50
total:=30
></State>
<Script>
function prepareCallback() {
const calcResult = (state.perc / 100) * state.total;
return { calcResult };
}
/Script>
<Style>
input { display: inline; width: 25px }
</Style>
```
Note that in some cases you may instead want to make a custom template filter (see below, in the section *Filter).
#### 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`.
#### 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.](/docs/templating/tags.html#for)
```
{% for athlete in state.athletes %}
<p>{{ athlete.name }}</p>
{% endfor %}
```
## Variable Syntax
### The quandary of kebab vs camel
In naming variables in programming languages, there are several popular syntax
options. One is nicknamed "camel case", which `looksLikeThis` (kind of like
humps on a camel's back), and "kebab case", which `looks-like-this` (kind of
like food on a kebab skewer). These silly nicknames might require some
imagination, but hopefully they also make it easier to remember!
The Modulo templating language supports either of these two options. It has to
be flexible since it sits at the "crossroads" of HTML, which uses "kebab" while
ignoring capital letters, and JavaScript, which respects capital letters and
thus can use "camel" syntax. For example, `data-results` (kebab) is a valid
HTML attribute name, but not a valid JavaScript variable name, since `-` is
forbidden. In JavaScript, it would be normal to use `dataResults` (camel)
instead. However, since capitalization is ignored in both plain HTML and
Component Part definitions, ignoring case means `dataResults` looks the same as
`dataresults` and `DATARESULTS`, so there's no way to preserve that formatting.
In other words, outside of _Script_ CParts, we should use `data-results` (kebab
case) to keep it case-insensitive and HTML-friendly.
### Examples
#### Example #1: Automatic camel case
Examine the following example. Note how in Templating, you can use either
syntax, but in JavaScript, you can't write unquoted property names with `-`
characters:
```modulo
edit:demo=modulo
<Template>
<p>(State) Dashes: {{ state.i-like-long-variable-names }}</p>
<p>(State) Capitals: {{ state.iLikeLongVariableNames }}</p>
<p>(Script) Dashes: {{ script.also-here }}</p>
<p>(Script) Capitals: {{ script.alsoHere }}</p>
</Template>
<State
i-like-long-variable-names="ok"
></State>
<!-- Always incorrect: <State iLikeLongVariableNames="ok"> -->
<Script>
function prepareCallback() {
return {
alsoHere: "ok",
// Always incorrect: also-here: "ok"
}
}
/Script>
```
#### Example #2: Avoiding automatic camel case
Sometimes there is data in a JavaScript object that is written using the
HTML-style kebab case. Even though it might be awkward to write like this in
JavaScript, it's pretty common to encounter this in JSON files. In this case,
the solution is using the `|get` filter. It still supports `.` syntax to "drill
down" to access properties, but does it does not do camel case transformation.
See below:
```modulo
edit:demo=modulo
<Template>
STAT: {{ staticdata|get:"database-query-results.0.query-status" }}
</Template>
<StaticData>
{
"database-query-results": [
{
"query-status": "OK"
}
]
}
</StaticData>
```
### Syntax best practices
In _Templates_, use dash syntax as much as possible, since it "blends in" with the
surrounding HTML and Component Part definitions (e.g. `{{ script.also-here }}`). Only use `|get` as a last
resort, when the default syntax fails you (e.g. in Example #2).
In _Scripts_, use capitalization syntax as much as possible sicne it "blends
in" with the surounding JavaScript code.
# 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 to combine behavior of two or more filters. For
example, using a filter chain like
`{{ state.color|allow:"red,blue"|default:"green" }}` 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 many built-in template filters.
You can read all about them in the
[built-in filter reference](/docs/templating/filters.html).
## Common template filters
To give you an idea of what a filter might be useful for, one popular filter to
use as an example is the `|default` filter. This filter shows the variable if
it has a value. However, if a variable is false, empty, or 0, it will use a
default value instead. For example, consider the following "profile card"
snippet, and summary of template filters used:
```
<div class="profile-card">
<p>Username: <tt>@{{ user.username|lower }}</tt></p>
<p>Real name: <tt>{{ user.namesArray|join:" " }}</tt></p>
<p>Pronouns: <tt>{{ user.pronouns|default:"(None specified)" }}</tt></p>
</div>
```
* [|lower](/docs/templating/filters.html#lower) - This converts the text to
lowercase, in this case showing the username in all lowercase letters
* [|join](/docs/templating/filters.html#join) - The join filter combines an
array, in this case the user's names (e.g. first, middle, and last names)
* [|default](/docs/templating/filters.html#default) - Finally, the third
example shows off the usefulness of the `|default` filter, which helps you
add quick default values for fields that are sometimes unspecified. In this
case value, if the value of the pronouns field is a falsy or undefined value
it will showing a more human-friendly placeholder message instead of just
`undefined`, `null`, or `false`
These are just a few of the dozens of filters available. See the [Template
Filter reference](/docs/templating/filters.html) for the complete list.
## Custom filters
Registering custom filters requires little code, and can be done in
a _Configuration_ definition at the top level. See below for two examples of
registering a custom filter:
```modulo
edit:demo=modulo_embed
<script Configuration>
// This tiny custom template filter has no extra arguments,
// and simply doubles the single expected input.
modulo.templateFilter.twice = value => value * 2
modulo.templateFilter.percent = function percent(value, arg) {
// This custom template filter has more complicated math
// that takes the percentage of the value and formats it.
const num = (value / 100) * arg;
return `${ num }% of ${ arg }`;
}
<-script>
<Component name=App>
<Template>
Total * 2: {{ state.total|twice }} <hr />
A: {{ state.a|percent:state.total }}<br />
B: {{ state.b|percent:state.total }}
</Template>
<State
a:=3
b:=13
total:=42
></State>
</Component>
```
# 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](/docs/templating/tags.html).
## Common template tags
For examples, here is an example and summary of **if** and **for**, the two
most commonly used template-tags:
```
<ul>
{% for athlete in state.athletes %}
<li>{{ athlete.name }}</li>
{% endfor %}
</ul>
{% if state.athletes.length is 10 %}
Team is full (10 max)
{% endif %}
```
- **`{% for %}`** - _Summary:_ Duplicate a block of HTML code for every item in
a collection of items (e.g. an array).
- Allows a HTML code to be repeated while "looping" over each item
- Example: In this case, it displayed a list of athletes, assuming each
athlete is an Object with a property `.name`, in an Array called
`state.athletes`
- **`{% if %}`** - _Summary:_ Only shows a block of HTML code if the condition
is met or the variable is a true value
- Allows conditional rendering of HTML, or alternating appearances (e.g.
toggling collapsed and visible for a modal pop-up or accordian component)
- Example: In this case, it will only show "Team is full (10 max)" if there
are exactly 10 athletes in that Array
Again, these are just two of them, while the [rest are documented
here](/docs/templating/tags.html).
### Differences between tags and filters
The outward differences between tags and filters are the syntax and behavior:
Tags have percent-sign syntax (e.g. `{% tag example %}`) and are usually for
control flow, while template filters use curly brace and vertical pipe syntax
(e.g. `{{ example|filter }}`) and are usually for modifying or preparing values
for display.
There is a big underlying behavioral difference, as well. The most important
difference between template tags and template filters are that template tags
are interpreted at "compile-time" while filters are interpreted at
"render-time".
#### Tags: Compile-time
"Compile time" is when the template is first loaded, no matter if it was
embedded in a `<Template>` or using a `-src=`). Compile time only happens once,
and will not happen ever if you are running a "build" or production version of
a Modulo component bundle.
The pluses of "compile time" involve efficiency: Your template tag code can be
as complicated or inefficient as necessary, and as long as the code it outputs
is efficient, the final bundled JavaScript can be lean and mean, possibly
omitting the code used to generate it. The caveats are that mistakes with
template tags might stop the component from rendering or compiling JavaScript
at all, e.g. mistakes such as syntax typo or a buggy third-party template tag.
#### Filter: Render-time
"Render time" when the component is outputting it's HTML into the DOM, after
it's loaded. 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.
Filters should always get included in compiled bundles, since they get run each
time the component rerenders. The pluses are they are much easier to write. The
caveats are they might be less efficient, depending on the situation.
## Custom tags
Just like filters, you can also create your own custom template tags. Custom
tags are more difficult to write, since they are run during build time (not
during "run time", like filters). That means they need to *generate* the
JavaScript code that *does what you want*, as opposed to *do what you want
directly*. To register a template tag, be sure to register it at the top level,
since it will be needed during build time:
```modulo
<script Configuration>
modulo.templateTag.showval = function showval (text, templateCPart) {
// "OUT" is the output array, "CTX" is the local variable context:
return `
OUT.push('<p><em>' + "${ text }" + '</em>=') });
OUT.push(CTX["${ text }"]);
OUT.push('</p>');
`;
}
<-script>
```
<!--
<Component name=App>
<Template>
{% for item in state.data %}
{% showval item %}
{% endfor %}
</Template>
<State
data:='[3, 10, 42]'
></State>
</Component>
```
-->
> **Why not HTML comments?**
> In most code, HTML comments are available. For example, you can ignore an
> entire `<Template>` part like this: `<!--<Template>...-->` However, within
> a _Template_, HTML Comments are part of the template itself, and will be in the
> resulting DOM when the template renders. Furthermore, template code within the
> comment will be treated like any other template code. For example,
> `<!-- {{ state.msg|upper }} -->` will result in a comment being produced in the
> DOM with the uppercase value of `state.msg` (e.g. `<!-- HELLO WORLD -->`).
> Thus, to _fully ignore_ HTML or template-tags _within_ a `<Template>`, more
> types of comments are needed.
# Commenting and debugging
## Short comment syntax
To add a short note or comment: `{# text in here is ignored #}`
This syntax should only be used for short, single-line messages, and cannot
contain disable portions of template code.
## Comment tag
To comment out text or code: `{% comment %}text in here is ignored{% endcomment %}`
If you need to comment out _Template_ code, or a multiline portion of the
template, then use this comment tag syntax instead, which will work in more
cases.
Examples of both types:
```modulo
<h1>hello {# greeting #}</h1>
{% comment %}
{% if a %}<div>{{ b }}</div>{% endif %}
<h3>{{ state.items|first }}</h3>
{% endcomment %}
<p>Below the greeting...</p>
```
## Debugger tag
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:
```
{% for item in state.data %}
{{ item.user_id }} {# Bug: Keeps on coming back as "undefined" #}
{% debugger %}
{% endfor %}
```
**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.
> **What is safe?** All data is considered "untrusted" by default, as in, you
> can't trust that it's not from some person trying to hack a user on your
> site, or inject spam onto a platform. It's up to the developer to choose what
> is "trusted". 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. when to use `|safe`) is outside the
> scope of this document, but in general, you can trust HTML from a trusted
> source, e.g. yourself or a member of your team or organization, and,
> sometimes, you can trust user-supplied data, when it has already been
> generated or validated on a backend web server or database management system,
> such as something that allows only certain tags of HTML and strips away
> anything else.
# Escaping
Every template automatically escapes the output of every template variable. By
default in Modulo, it escapes 5 characters by replacing each with its
corresponding HTML character entitty to prevent it from being interpreted as
code. Specifically, these five characters are escaped, since they are the ones
that are "dangerous" for HTML and URL syntax: `>` (greater than), `<` (less
than), `'` (single quote), `"` (double quote), `&` (ampersand).
## The purpose of escaping
Since Modulo Templates generate HTML, which then gets intepreted into the DOM,
there's there's a risk that a variable will include characters that affect the
resulting HTML, by closing a tag or quotation early (or accidentally opening a
new one). 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. Furthermore, a malicious user could even use this
vulnerability to add in JavaScript behavior (e.g. via an "onclick") that acts
on behalf of other users, such as by sending requests to an API. To avoid this
problem, Modulo uses automatic escaping.
## Safe to turn off escaping
Sometimes you want to have HTML in variables, such as a _Prop_ that accepts
HTML for styling, or a string of HTML that might be user-entered, but it's
loaded from a trusted source, such as a staff-only database or API. If you have
HTML in a variable that that you actually intend to be rendered as HTML, then
you will need to turn off escaping. To disable auto-escaping for an individual
variable, you need to "mark it as safe". That is, you are saying to Modulo
Templating that this value may have special HTML characters, but it's safe
to assume it's valid, trustworthy HTML.
### Examples
Escaping is sometimes easiest understood with examples. Try the following
demonstrations for example use of the `|safe` filter.
#### Example #1: Escaping for tags and attributes
Note how the `(1) ESCAPED` content shows the `<em>` tag, since it escapes the
`<` and `>` symbols preventing them from being interpretted as HTML, and the
`(2) SAFE` content ends the title attribute too early.
```modulo
edit:demo=modulo
<Template>
<p>(1) ESCAPED: {{ state.content }}</p>
<p>(1) SAFE: {{ state.content|safe }}</p>
<p>(2) ESCAPED: <tt title="{{ state.content }}">HOVER</tt></p>
<p>(2) SAFE: <tt title="{{ state.content|safe }}">HOVER</tt></p>
</Template>
<State
content='There is no "there" <em>there</em>'
></State>
<Style>
p {
font-size: 0.8rem;
}
</Style>
```
#### Example #2: Escaping user inputted data
For a slightly more realistic example, where user message content is marked
safe, while usernames are left to be escaped, examine the following:
```modulo
edit:demo=modulo
<Template>
<p>User "<em>{{ state.username }}</em>" sent a message:</p>
<div class="msgcontent">
{{ state.content|safe }}
</div>
</Template>
<State
username="Lil' <Bobby> <Tables>"
content='
I <i>love</i> the classic <a target="_blank"
href="https://xkcd.com/327/">xkcd #327</a> on
the risk of trusting <b>user inputted data</b>
'
></State>
<Style>
.msgcontent {
background: #999;
padding: 10px;
margin: 10px;
}
</Style>
```
## Direct usage of Template interface
Modulo's Template language can be used as a regular JavaScript class with the
`new` constructor syntax as well. For example:
```javascript
const { Template } = modulo.part
let myTemplate = new Template('Hi {{ msg }}!')
console.log(myTemplate.render({ msg: 'there' })) // "Hi there!"
```
One use of this interface is enabling users to not only edit HTML (as with
`|safe`, described earlier), but also be able to edit template tags themselves.
Note that this will enable users to write and evaluate (potentially buggy)
custom JavaScript code, and thus should be used with even more caution than the
`|safe` filter. An example of using this to allow users to program their own
templates is below:
```modulo
edit:demo=modulo
<Template>
<input state.bind name="data.name" />
<input state.bind name="data.user" />
<textarea state.bind name="template"></textarea>
<div>{{ script.results|safe }}</div>
</Template>
<State
template="Hello <em>{{ name }}</em><br />Username: <tt>{{ user }}</tt>"
data:={}
data.name="Pedro"
data.user="pramirez"
></State>
<Script>
const { Template } = modulo.part
function prepareCallback() {
const myTemplate = new Template(state.template)
const results = myTemplate.render(state.data)
return { results }
}
<-Script>
```
> **Can I force as unsafe?** If a string was marked as safe and you want to
> "unmark" it, then simply add an empty string to add a new string:
> `{{ state.text|add:'' }}`