UNPKG

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
<!DOCTYPE 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:'' }}`