Skip to content

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, refresh is the caller's handler.
  • Events. @press: refresh handles the component's press event with the caller's refresh.
  • 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/*.weft files (date-badge.weft is DateBadge), 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 on declared 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, and event.

The event record

Inside a handler, and in a handler's arguments, event is a record:

FieldHolds
kindthe event's name: "click", "input"…
valuethe control's value, as text, for input and change (and what a component emits with it)
keythe key, for keyboard events: "Enter", "a"
x, ythe pointer's position, for pointer events
indexinside a for, the item's position (from 0); for some components, the item chosen
iteminside 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)
  • toasts is a list, and each toast is a record with a message (and a tone: success, danger, warning or info). Plain text works too: toasts + ["Sent"].
  • Keep the brackets: + only adds a list to a list, so toasts + "Sent" is an error, reported in the browser's console. If toasts is given something that isn't a list of toasts, Toaster reports that too.
  • close gives the toast's position as event.index, and remove_at takes 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: measure calls a handler, and @keydown: press(event.key) passes it arguments, as on an element. The handler can be an on declared anywhere in the template, one from the Rust scope, or, in a template-only component, the caller's: @scroll: moved calls 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…).
  • event has the browser event's values by their DOM names (key, clientX, shiftKey…), and the window's innerWidth, innerHeight, scrollX and scrollY.
  • @load runs 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 on handler'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 label and value. A prop the caller doesn't give is null.
  • Its events are the handler names it uses but doesn't declare: more calls the caller's @more. emit("more", value) in an on does 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.rs beside 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.