The template language
Components and events
Props, slots, events, the event record, and template-only components.
A capitalised name in a template is a component. Components are how pages are built from parts: Weft's own (Button, Card, BarChart…) and your app's.
Using a component
Card(class: "max-w-sm") {
CardHeader {
CardTitle "Monthly report"
CardDescription "Updated hourly."
}
Button(variant: "gradient", @press: refresh) "Refresh"
}- Props. Each attribute is a prop. A prop that reads state is updated when the state changes, and the component renders again.
- Children fill the component's
slot. They render in the caller's scope: in the example,refreshis the caller's handler. - Events.
@press: refreshhandles the component'spressevent with the caller'srefresh. - Where they come from. Components are looked up by name in the app's registry: Weft's built-in ones (see the component catalogue), the app's
components/*.weftfiles (date-badge.weftisDateBadge), and any made in Rust.
Events
@event: handler runs a handler when the event happens. On an HTML element the event is a DOM event, such as click, input, change, submit or keydown; on a component it's one the component emits.
Any DOM event works, by its DOM name: @contextmenu, @mouseenter, @scroll, @dragstart, @paste, @transitionend. An event that bubbles in the DOM reaches the handlers of the element's ancestors too, up to the first one that handles it; one that doesn't bubble (mouseenter, scroll, focus) runs only its own element's handler. To stop the browser's own response, such as the right-click menu, add prevent(): on menu = prevent(), then @contextmenu: menu.
state draft = ""
state todos = []
on type = draft <- event.value ?? ""
on add = todos <- todos + [draft], draft <- ""
on remove(i) = todos <- todos |> remove_at(i)
form(@submit: add) {
input(value: draft, label: "New todo", @input: type)
button "Add"
}
ul {
for t in todos {
li {
"{t} "
button(@click: remove(event.index)) "Remove"
}
}
}- The handler is an
ondeclared in the template (Declarations), or one the component's Rust scope provides. - Arguments.
@click: remove(event.index)passes values to the handler's parameters. They're computed when the event happens, from the template's names, the loop's variables, andevent.
The event record
Inside a handler, and in a handler's arguments, event is a record:
| Field | Holds |
|---|---|
kind | the event's name: "click", "input"… |
value | the control's value, as text, for input and change (and what a component emits with it) |
key | the key, for keyboard events: "Enter", "a" |
x, y | the pointer's position, for pointer events |
index | inside a for, the item's position (from 0); for some components, the item chosen |
item | inside a for, the item; for emit, the value sent |
These fields are always there, in the browser, when prerendering and in tests. One that doesn't apply to the event is null (key on a click).
In the browser, event also has everything the browser's event has, by the names MDN uses: every number, text and true/false on it. So a wheel event has event.deltaX and event.deltaY, a pointer event event.clientX, event.pointerType and event.pressure, and any event event.shiftKey, event.timeStamp and event.type. Objects (event.target) aren't copied. Where a name is the same, the field in the table wins: event.x is relative to the element, not the window (the window's is event.clientX).
state zoom = 1
on wheel = prevent(event.ctrlKey), zoom <- zoom * (if event.deltaY < 0 then 1.1 else 0.9)
div(@wheel: wheel) "Zoom: {zoom}"prevent(event.ctrlKey) stops the page scrolling while Ctrl is held, as maps do; see Commands.
Reading a name the event doesn't have is an error, and the message lists the fields it does have. Off the browser only the fields in the table are there, so a handler that reads event.deltaY is one to try in the browser.
A timer's steps see a different event; see Data and effects.
Submitting forms
In a browser, clicking a form's submit button submits the form. Handle that once, with @submit on the form: a submit button that also has @press or @click for the same handler runs it twice. The browser doesn't reload the page; Weft handles the submit instead.
Give the button type: "submit". A plain button with no type inside a form is a submit button, but the Button component defaults to type: "button", which doesn't submit anything.
A form that thanks the visitor with a toast:
state toasts = []
on submit = toasts <- toasts + [{ message: "Thanks, we'll be in touch.", tone: "success" }]
on close = toasts <- remove_at(toasts, event.index)
form(@submit: submit) {
Field {
Label "Email"
Input(type: "email", required)
}
Button(type: "submit") "Send"
}
Toaster(toasts: toasts, @close: close)toastsis a list, and each toast is a record with amessage(and atone:success,danger,warningorinfo). Plain text works too:toasts + ["Sent"].- Keep the brackets:
+only adds a list to a list, sotoasts + "Sent"is an error, reported in the browser's console. Iftoastsis given something that isn't a list of toasts,Toasterreports that too. closegives the toast's position asevent.index, andremove_attakes that one away.
Window events
@event: steps at the top level of a template, with the declarations, runs each time the window fires that event, while the component is on the page. The steps are an on handler's: state <- value, if … then … and commands, separated by commas.
state width = 0
state scrolled = false
state open = true
@resize: width <- event.innerWidth
@scroll: scrolled <- event.scrollY > 40
@keydown: if event.key == "Escape" then open <- false
@online: reload(posts)
load posts = get("/api/posts")
header(class: if scrolled then "shadow" else "") "{width}px"- Or a handler.
@resize: measurecalls a handler, and@keydown: press(event.key)passes it arguments, as on an element. The handler can be anondeclared anywhere in the template, one from the Rust scope, or, in a template-only component, the caller's:@scroll: movedcalls the caller's@moved. - Any window event: the window's own (
load,resize,scroll,hashchange,popstate,online,offline,beforeunload,storage…) and the ones that bubble up to it from the page (keydown,click,pointermove…). eventhas the browser event's values by their DOM names (key,clientX,shiftKey…), and the window'sinnerWidth,innerHeight,scrollXandscrollY.@loadruns once. If the page had already loaded when the app started, it runs straight after the component mounts.- Lifetime. The listener starts when the component mounts and stops when it goes, so a page's window events stop when you leave the page. Prerendering never runs them, and a page with one loads the runtime.
- Mistakes caught when the template parses: the same event bound twice in one template (put both steps in one, separated by a comma), and the mistakes an
onhandler's steps can have. The language server warns about a name the window never fires (@resze).
Template-only components
A component can be a template alone: a components/*.weft file, or Component::from_template in Rust.
// components/stat-card.weft
div(class: "rounded-2xl p-5 ring-1 ring-slate-200") {
p(class: "text-sm text-slate-500") "{label}"
p(class: "text-3xl font-black") "{value}"
button(@click: more) "More"
}- Its props are the names it reads but doesn't declare: here
labelandvalue. A prop the caller doesn't give isnull. - Its events are the handler names it uses but doesn't declare:
morecalls the caller's@more.emit("more", value)in anondoes the same, with a value. - It can hold its own state, with declarations. That state survives the caller's re-renders, because component instances are kept by their position in the template.
- A companion (
stat-card.rsbeside it) can add data and functions in Rust:pub fn scope(props: &Props) -> Scope.
Rust components
A component made in Rust (Component::new(name, template, setup)) builds its scope in a setup function. It reads its props from Props, and emits events with props.emit(…). Templates written with template! can use every declaration too, on top of what setup provides. The Rust side is in the API reference.