Repository navigation
04 Expressions
Expressions are the small language inside {{ }} and data-tpl-*="...". They are parsed by a PEG grammar and interpreted over the data stack, and the interpreter calls no eval and builds no new Function of its own. A bare name resolves against the data stack and a #module:fn() call against the registered modules.
The interpreter is not a sandbox. Member access follows the prototype chain as javascript does, so s.constructor.constructor('…')() reaches the Function constructor and through it the whole page. An expression is as powerful as a script tag: write expressions as part of the page's source, and let untrusted values reach them as data, never as expression text. The threat model has the full rules.
| Category | Syntax |
|---|---|
| literals | numbers 3 3.1, strings 'a' "a", template strings `a{var}b`, booleans, null, undefined, dict {"a": 1}, array [1, 2, 3]
|
| member |
a.b, a?.b, a[b], a?.[b]
|
| call |
a.toLowerCase(), a.toLowerCase?.()
|
| logical |
!a, a && b, a || b, a ?? b
|
| equality |
a == b, a != b
|
| relational |
a < b, a > b, a <= b, a >= b
|
| ternary | a ? b : c |
| elvis |
a ?: b, which is a if truthy and b otherwise |
| grouping | (a || b) && c |
== and != compare strictly, as === and !== do in javascript: 1 == '1' is false.
There is no arithmetic and no unary minus: a + 1, a - 1 and -a are parse errors, and a computed value comes from the data or from a module function. A negative number literal such as -3 parses.
The elvis operator is the short "or default": title ?: 'untitled'. Template strings interpolate with {name} and skip null/undefined parts (they also serve as dict keys, evaluated: {`{k}`: 1} builds an entry named by the value of k):
<p>{{ `{user.first} {user.last}` }}</p>The placeholder is {name}, not javascript's ${name}: in an ftl template string ${name} renders a stray $ before the value. In a static template written as a javascript template literal, a ${name} never reaches ftl: javascript evaluates it once, when the class is defined, so a template there writes {{ name }}, and {name} inside an ftl template string.
data = {
parent: { nested: { text: "I'm the nested obj", empty: null, fruits: ['apple', 'pear'] } },
today: 'tue',
carToPick: { mon: 'Ferrari', tue: 'Lambo', wed: 'Porsche' },
}| Expression | Result |
|---|---|
parent.nested.text |
"I'm the nested obj" |
parent.nested.empty?.text |
undefined (null-safe chain) |
carToPick[today] |
"Lambo" (computed access) |
self |
the current context (top of the data stack) |
Plain identifiers resolve by scanning the data stack top-down: the most recently pushed overlay containing the property wins. An identifier is taken whole: nullable, falseFlag or truex are plain names, the keywords counting only as exact words, so a data field may carry any name. self is exactly the top overlay, so inside data-tpl-each="items" with no var, {{ self }} is the current item.
Method calls run on the resolved value with optional chaining:
<p>{{ parent.nested.fruits.join(', ') }}</p>Functions you cannot reach through the data are registered as modules and called as #module:name(...):
ftl.registry.defineModule('math', {
sum: (...addends) => addends.reduce((a, b) => a + b, 0),
});<p>{{ #math:sum(1, 5, 3, 65) }}</p> <!-- 74 -->Only templates bound to the registry see its modules: those made with ftl.Templates.*, and a component's this.template(). A template made with ftl.Template.* sees only the modules passed to its factory, and a bound template keeps the modules defined when it was bound, so defineModule comes first. A bare #name(...) resolves against the whole modules object. Module functions are plain javascript functions: whatever they return is the expression's value, and they run with a proxy over the data stack as this, which is how Localization reads the active messages.
-
data-tpl-*="expr": the whole attribute value is one expression. -
{{ expr }}inside text, inMODE_TEMPLATED: text and expressions can mix, with the three interpolation forms ({{ }},{{{ }}},{{{{ }}}}). -
template.evaluateExpression(expression, ...data)for one expression andtemplate.evaluateTemplated(text, ...data)for the{{ }}form, each in the template's scope widened by the data. AnExpressionEvaluatorgives the same language from javascript, for instance inside a component:
const value = this.template().evaluateExpression('config.fields.length');
const evaluator = ftl.registry.evaluator(); // registry modules + data, ready to evaluateftl.Expressions.parse caches ASTs (bounded FIFO), so repeated evaluation of the same expression is cheap.
ftl
httpc
ful
- Getting started
- Fields
- Forms
- Inputs
- Select
- Files
- Temporal inputs
- Tables
- Disclosures
- Navigation
- Async sections
- Timing and storage
- Theming
Extending
Reference
Project