The template language
Declarations
state, let and on: a template's own state and handlers.
A template can hold its own state, values derived from it, and the handlers that change it, so a component, page or layout can be a single .weft file. Handlers are pure transitions, in the style of Elm: each says what the state becomes, in terms of what it is.
state todos = []
state draft = ""
let left = todos |> count(t -> !t.done)
on type = draft <- event.value ?? ""
on add = todos <- todos + [{ title: draft, done: false }], draft <- ""
on toggle(i) = todos <- todos |> update_at(i, t -> { t | done: !t.done })
on remove(i) = todos <- todos |> remove_at(i), emit("removed", i)
form(@submit: add) {
input(value: draft, label: "New todo", @input: type)
button "Add"
}
for t in todos {
li {
input(type: "checkbox", checked: t.done, label: t.title, @change: toggle(event.index))
button(@click: remove(event.index)) "Remove"
}
}
p "{left} left"state
state name = expression is a value that changes.
- Its first value is computed once, when the component is created, from its props and the names declared above it:
state count = start ?? 0. A later change to the prop doesn't reset it. - It changes only through a handler's or timer's steps.
- Everything that reads it updates when it changes.
let
let name = expression is a value computed from others.
- It's computed when first read, and again only when something it reads changes.
- It can read the names declared above it, props, and the companion's values.
- A
letthat fails reads asnull, and the error is reported where it's used.
fn
fn name(params) = expression is a function of values.
fn label(n) = if n == 1 then "1 item" else "{n} items"
fn total(xs) = if len(xs) == 0 then 0 else first(xs) + total(xs |> drop(1))
p "{label(len(todos))}"- It's called like a standard function, with values:
label(count). Lambdas can't be passed to it. - It reads its parameters and the template's other names. It can call itself and the other functions, wherever they're declared.
- A call that goes more than 200 deep is an error, not a crash.
- Its name can't be a standard function's or a command's.
use
use "shared.weft" brings in another file's fn, let and data declarations, as if they were written where the use is.
- The path is relative to the file it's in. A file used twice is read once.
- A used file holds only those declarations: no elements, state or handlers. It can
useothers, but not itself. - It works in
.weftfiles, and in templates loaded withinclude_template!. A template written intemplate!can't have one.
static params and provide
Two declarations belong to pages and layouts, and are described in Pages and routing: static params = … lists the paths of a dynamic page to prerender, and provide name, other lets the pages inside a layout read its names and call its handlers.
props
props (width = 1024, height = 768, label) declares the props a template component takes. A prop with a default uses it when the caller passes none, or passes null. A prop without a default is required: the language server warns where a call leaves it out, and the prop is null there. Give an optional prop a default, label = null if nothing else fits. Defaults are expressions, and can read the props before them: props (width = 1024, height = width / 2).
props (src, alt = "", rounded = true)
img(src: src, alt: alt, class: if rounded then "rounded-xl" else "")A prop can say what it takes. For a few named choices, list them, or name a data declaration that lists them:
data Size = small | medium | large
props (size: Size = .medium, tone: info | success | danger = .info, label)
p "{label}"Callers then pass a variant, Chip(size: .large), and the language server checks it against the choices and offers them after you type .. A default must be one of the choices, and a name after : must be a data declared in the template (or num, text, bool, list or record). A variant is its name as text, so size == .large compares as you'd expect.
Declared props are offered and explained by the language server where the component is used. A template component still takes any name it reads as a prop; declaring them adds the defaults and makes them easy to find. The language server flags a name a call passes that the component neither declares nor reads (Chip(sise: 2): "Chip has no prop sise — did you mean size?"). It skips this check when a companion .rs sits beside the component, since the companion can read any prop.
Grouping declarations
state, let and props take a group in parentheses, one declaration per item, separated by commas or new lines:
state (
x = 0
y = 0
zoom = 1
)
let (centre = [x, y], scaled = zoom * 2)
div "{centre}"state (x = 0, y = 0) is state x = 0 then state y = 0.
on
on name(parameters) = steps is a handler: steps separated by commas.
state <- expressionsets a state declared in the template.- Every right-hand side reads the state as it was before the handler ran. The new values are computed first and then set together, so
a <- b, b <- aswaps two states, and the order of steps doesn't change the result. if condition then step, optionallyelse step, runs a step only when the condition holds:on move = if event.buttons == 2 then x <- x + event.movementX. The condition reads the state from before, like every right-hand side. A branch is one step; the next comma ends it, soif c then a <- 1, b <- 2always setsb.eventis the event that ran it (the event record).- Parameters take the arguments of a call:
@click: remove(t.id)givesremove(id)itsid. A parameter with no argument isnull. - Commands run after the new state is set, with arguments computed from the state before it.
Commands
| Command | Does |
|---|---|
emit("name", value) | calls the caller's @name handler, with value as its event.item |
navigate("/path") | shows another page (without a reload, in an app made from app/) |
get, post, put, patch, delete | sends a request; see Data and effects |
reload(name) | loads a load again |
prevent() | cancels the browser's default action for the event, like preventDefault(): a wheel's scroll, a key's typing, a link's navigation |
prevent(condition) | cancels it only when condition holds: prevent(event.ctrlKey) |
prevent acts on the event that ran the handler, while the browser is still handling it, so it works however many steps come after it. A form's submit is already cancelled for you: the browser doesn't reload the page.
Layout
Declarations go at a template's top level, and conventionally come first. A declaration can continue on the next line after a comma or an operator:
state n = 0
on reset = n <- 0,
emit("reset", n)
p "{n}"Checked when the template is parsed
These mistakes are reported with the line and column, and inside template! as compiler errors:
- setting something that isn't a
statedeclared in the template; - a step that's neither
state <- valuenor a command; - a name declared twice;
- a
stateorletthat reads a name declared at or after it; - a declared name that's a keyword or
event, and a parameter calledevent; - a command's handler name that isn't an
onin the template; - a
fnnamed like a standard function, or with a parameter named like itself; static paramsdeclared twice, andprovideof a name that isn't declared;- a
usewhose file can't be read, uses itself, or holds more thanfn,letanddata.
The messages are listed in Errors.
In Rust components
Declarations work in templates written with template! too. A companion's or setup function's scope can still supply data, functions and handlers, and the declarations go on top of it, hiding names they share.