The template language
Data and effects
load, requests in handlers, and timers.
Templates describe effects, and the runtime carries them out:
loadis 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:
| Field | Holds |
|---|---|
data | null until the first answer, then the answer. While it loads again, the old data stays. |
loading | whether a request is out |
error | what went wrong, as text, or null |
- It follows what it reads. The request is computed like a
let: whenqchanges, 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.jsonis/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,234and$12.50are 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
| Option | Does |
|---|---|
path | picks part of a JSON answer: "results", "data.items", "items.0" |
format | reads the answer as "json", "csv", "tsv" or "ndjson" instead of guessing |
load | when it loads: "build", "browser" or "both" |
refresh | asks again every so many seconds |
headers | a record of headers to send |
When it loads
load | Loads |
|---|---|
"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:
| Command | Sends |
|---|---|
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 |
okanderrnameonhandlers declared in the template, and both are optional.okruns with the answer as its first parameter;errruns 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
| Form | Runs its steps |
|---|---|
every seconds when condition = steps | every so many seconds while the condition holds |
after seconds when condition = steps | once, that long after the condition turns true; again the next time it does |
every frame when condition = steps | every animation frame while the condition holds |
- Without
when, a timer always runs. - The steps are a handler's: assignments and commands, set together.
eventin a timer's steps is a record:kind("every","after"or"frame"),time(milliseconds since the timer started) anddelta(milliseconds since its last run).- The seconds are computed when the timer starts.
everywaits 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.