Skip to content

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 let that fails reads as null, 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 use others, but not itself.
  • It works in .weft files, and in templates loaded with include_template!. A template written in template! 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 <- expression sets 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 <- a swaps two states, and the order of steps doesn't change the result.
  • if condition then step, optionally else 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, so if c then a <- 1, b <- 2 always sets b.
  • event is the event that ran it (the event record).
  • Parameters take the arguments of a call: @click: remove(t.id) gives remove(id) its id. A parameter with no argument is null.
  • Commands run after the new state is set, with arguments computed from the state before it.

Commands

CommandDoes
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, deletesends 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 state declared in the template;
  • a step that's neither state <- value nor a command;
  • a name declared twice;
  • a state or let that reads a name declared at or after it;
  • a declared name that's a keyword or event, and a parameter called event;
  • a command's handler name that isn't an on in the template;
  • a fn named like a standard function, or with a parameter named like itself;
  • static params declared twice, and provide of a name that isn't declared;
  • a use whose file can't be read, uses itself, or holds more than fn, let and data.

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.