Skip to content

The template language

Data and effects

load, requests in handlers, and timers.

Templates describe effects, and the runtime carries them out:

  • load is data that loads, and loads again when what its request reads changes.
  • Requests in handlers are commands whose answer comes back to a handler they name.
  • Timers run steps while a condition holds.

Handlers stay pure transitions throughout.

state q = ""
state draft = ""
state saving = false
state problem = null
load found = get("/api/search?q=" + q, { path: "results" })
load todos = get("/api/todos", { refresh: 30 })

on search = q <- event.value ?? ""
on save = saving <- true, post("/api/todos", { title: draft }, saved, failed)
on saved(todo) = saving <- false, draft <- "", reload(todos)
on failed(message) = saving <- false, problem <- message
after 3 when problem != null = problem <- null

input(value: q, label: "Search", @input: search)
if todos.loading { p "Loading…" } else if todos.error { p "Couldn't load: {todos.error}" }
for t in todos.data ?? [] { li "{t.title}" }
if problem { p(role: "alert") "{problem}" }

load

load name = get(url, options) declares data. name is a record:

FieldHolds
datanull until the first answer, then the answer. While it loads again, the old data stays.
loadingwhether a request is out
errorwhat went wrong, as text, or null
  • It follows what it reads. The request is computed like a let: when q changes, it asks again. An answer that arrives after a newer request was sent is dropped.
  • post(url, body, options) is for a read that needs a body, such as a GraphQL query.
  • Relative URLs are under the app's base: in an app served at /ngo/, data/events.json is /ngo/data/events.json.

Answers

An answer is read by what it holds:

  • JSON becomes records and lists;
  • CSV, TSV and NDJSON become a list of row records, with each column typed (1,234 and $12.50 are numbers);
  • anything else is text.

A status outside 200–299 is an error: error says the status, and the server's message if it sent one.

Options

OptionDoes
pathpicks part of a JSON answer: "results", "data.items", "items.0"
formatreads the answer as "json", "csv", "tsv" or "ndjson" instead of guessing
loadwhen it loads: "build", "browser" or "both"
refreshasks again every so many seconds
headersa record of headers to send

When it loads

loadLoads
"build"while the site builds. The answer travels in the page, so the browser never asks.
"browser"in each visitor's browser
"both"at build, and again in the browser

Without the option, the site's own files (from public/) load at build, and anything else loads both at build and in the browser. Loading a remote URL at build time needs weft's blocking-http feature. A page with a load that runs in the browser loads the runtime.

Requests in handlers

The request commands send a request from a handler's steps, after the step's batch, like emit:

CommandSends
get(url, ok, err)a GET
delete(url, ok, err)a DELETE
post(url, body, ok, err)a POST, with body as JSON
put(url, body, ok, err)a PUT, with body as JSON
patch(url, body, ok, err)a PATCH, with body as JSON
  • ok and err name on handlers declared in the template, and both are optional.
  • ok runs with the answer as its first parameter; err runs with the message.
  • Without err, a failure is reported to the console.
  • A request is only ever sent in the browser, or in a test: never while the site builds.

reload(name) loads a load again, keeping its data meanwhile.

Timers

FormRuns its steps
every seconds when condition = stepsevery so many seconds while the condition holds
after seconds when condition = stepsonce, that long after the condition turns true; again the next time it does
every frame when condition = stepsevery animation frame while the condition holds
  • Without when, a timer always runs.
  • The steps are a handler's: assignments and commands, set together.
  • event in a timer's steps is a record: kind ("every", "after" or "frame"), time (milliseconds since the timer started) and delta (milliseconds since its last run).
  • The seconds are computed when the timer starts. every waits at least a frame between runs.
  • Timers stop when their component goes away. A page with a timer loads the runtime.
state angle = 0
state playing = true
on toggle = playing <- !playing
every frame when playing = angle <- (angle + event.delta * 0.05) % 360

button(@click: toggle) "{if playing then \"Pause\" else \"Play\"}"
p "{round(angle)}°"

after isn't a debounce: it restarts only when its condition goes false and true again, not each time something it reads changes.

Testing

weft::ui::testing::mount sends requests through weft_data's loader, so a test can fake the server with weft_data::source::set_loader. Timers wait on a virtual clock, which weft::ui::testing::advance(seconds) moves on. Prerendering never moves it, so no timer runs at build time. weft::ui::testing::window_event(&Event::new("resize")) sends a window event to every mounted component listening for it.