Template syntax reference¶
| Syntax | Meaning |
|---|---|
{{ expr }} |
Interpolate a Python expression in text or attribute values |
:attr="expr" |
Bind an attribute to an expression. None/False drop the attribute, True keeps it valueless (:disabled="busy"), anything else is stringified. On a component, passes the typed value as a prop |
@click="handler" |
A bare name calls that method; an expression is evaluated, so @click="remove(item)" works; simple statements run inline, so @click="count += 1" and @click="show = not show" work without a method. Any DOM event works the same way (@input, @keydown, @submit ...) |
@event.modifier |
@submit.prevent, @keydown.enter, @click.stop, @keydown.ctrl.s.prevent: see event modifiers |
@bind="field" |
Two-way binding on inputs, textareas, selects, checkboxes and radio buttons, converting to the field's declared type. @bind.lazy, .trim, .number: see binding form controls |
<If cond="..."> / <ElseIf cond="..."> / <Else> |
Conditional rendering. Branch elements must be adjacent siblings |
<For each="item in items" key="item['id']" index="i"> |
Loop. key gives rows stable identity across reorders (always use it for dynamic lists); index is optional |
<PascalCaseTag ... /> |
Renders a child component (see below) |
Expressions are real Python, evaluated against the component's state plus any
loop variables. self is the component, so {{ self.count }} is the explicit
spelling of {{ count }}. One caveat: comprehensions and lambdas inside
{{ }} can't see scope variables (CPython eval semantics) — compute those
in the code block instead.
Inline handlers¶
An event attribute accepts three forms. Pick the smallest one that fits:
<button @click="increment">+1</button> <!-- 1. call a method -->
<button @click="remove(item)">remove</button> <!-- 2. an expression -->
<button @click="count += 1">+1</button> <!-- 3. inline statements -->
- A bare name calls that method. If the method declares a parameter it receives the event payload, otherwise it is called with no arguments.
- An expression is evaluated in the template's scope, so loop variables
and the
eventpayload are available:@click="remove(item)",@input="search(event.value)". Whatever it returns is returned from the dispatch, which matters foremit(section 4). - Statements run when the value does not parse as an expression.
Assignment, augmented assignment and
;-separated statements all work:
<button @click="show = not show">toggle</button>
<button @click="count += 1; history.append(count)">record</button>
<For each="item in items" key="item['id']">
<li @click="selected = item">{{ item['text'] }}</li>
</For>
A bare name on the left of = writes component state, so count += 1
updates self.count on the component that owns the template. Assigning
to a loop variable only changes it inside that handler; it never becomes
state. self.count += 1 is the explicit spelling if you prefer it.
After any of the three runs, the component re-renders. Keep inline handlers to a statement or two; once logic needs a name, a comment, or a test, move it to a method.
The event object¶
A handler that declares a parameter, or an expression that uses event,
gets the event's details. It is a dict that also allows attribute access, so
event.key and event["key"] both work:
| Key | On |
|---|---|
type |
every event: "click", "keydown" ... |
value |
inputs, textareas and selects: the current text |
checked |
checkboxes and radio buttons |
key, code, repeat |
keyboard events: key is the character or name ("a", "Enter", "ArrowUp"); code the physical key ("KeyA") |
ctrlKey, shiftKey, altKey, metaKey |
keyboard and mouse events |
clientX, clientY, offsetX, offsetY, button, buttons |
mouse and pointer events (deltaX, deltaY on wheel) |
localX, localY, width, height |
mouse and pointer events: the pointer's position inside the element the handler is on, and that element's size |
form |
submit: the form's fields as {name: value}, a list when several fields share a name (file inputs are left out) |
Asking for a key the event doesn't have raises an AttributeError that
lists the keys it does have.
Reacting to an element's size: @resize¶
@resize isn't a browser event: it fires when the element is first laid
out and again whenever its size changes, with event.width and
event.height in whole pixels (the content box). Use it to lay something
out for the space it really has, such as fewer labels on a narrow screen:
It needs a page, so it never fires in pre-rendering or headless tests: start with a sensible default.
Event modifiers¶
Add modifiers after the event name to handle the browser's behaviour in the template instead of in your method:
<form @submit.prevent="save">...</form> <!-- no page reload -->
<a href="/help" @click.prevent="open_help">help</a> <!-- no navigation -->
<input @keydown.enter="search" @keydown.esc="clear" />
<textarea @keydown.ctrl.enter="send"></textarea>
<div @click="close" class="backdrop">
<div @click.stop="" class="dialog">...</div> <!-- clicks inside don't close it -->
</div>
| Modifier | Effect |
|---|---|
.prevent |
preventDefault(): no form submission, link navigation, browser shortcut |
.stop |
stopPropagation(): parents don't see the event |
.self |
only when the event happened on this element itself, not a child |
.once |
at most once |
.capture, .passive |
listener options, for capture-phase and scroll-performance cases |
.enter, .tab, .esc, .space, .delete (Delete or Backspace), .up, .down, .left, .right |
key events: only for that key |
| any key name in kebab-case | .a, .f1, .page-down: KeyboardEvent.key lower-cased, PageDown becomes page-down |
.ctrl, .alt, .shift, .meta |
only while that key is held (on keyboard and mouse events) |
.exact |
and no other of those keys: @click.ctrl.exact ignores Ctrl+Shift+click |
.left, .middle, .right |
mouse events: that button. @click.right handles right-clicks (the context menu), so add .prevent to suppress the menu |
- Modifiers only:
@submit.prevent=""with an empty value prevents the default and calls nothing. - Several handlers for one event are fine when their modifiers differ:
@keydown.enter="search"and@keydown.esc="clear"on one input. - Filters come first.
.preventand.stopapply only when the key, button,.selfand.ctrl-style conditions match, whatever order you write them in.@keydown.ctrl.s.prevent="save"blocks the browser's Save dialog for Ctrl+S and leaves every other key alone. - Typos are caught:
@submit.prevntor@click.enteris a template error naming the modifier, not a handler that silently never runs. - Modifiers belong to page events. A component's own events (
@removeon<TodoItem>) take none.
Binding form controls¶
@bind="field" keeps a control and a field in sync both ways:
<input @bind="name" /> <!-- str -->
<input type="number" @bind="age" /> <!-- int -->
<textarea @bind="notes"></textarea>
<input type="checkbox" @bind="subscribed" /> <!-- bool: checked -->
<input type="radio" value="s" @bind="size" />
<input type="radio" value="m" @bind="size" /> <!-- the checked one's value -->
<select @bind="country">
<option value="in">India</option>
<option value="nl">Netherlands</option>
</select>
- Types follow the field's declaration.
age: int = 0stores anint;price: float | None = Noneafloat, orNonewhen the box is empty. Text that doesn't fit (letters in a number box, an empty box for a field that can't beNone) leaves the field at its last good value while the user is still typing, instead of turning it into a string. - Checkboxes bind a
boolto the checked state; radio buttons store thevalueof the one that is checked (converted, sovalue="2"withsize: intstores2). - Modifiers:
@bind.lazyupdates when the user leaves the field (thechangeevent) rather than on every keystroke;@bind.trimstrips surrounding spaces;@bind.numberparses a number even when the field has no declared type. - Your own handler alongside:
@bindand@inputon one element both run, the binding first, so your handler already sees the new value:
Input masks¶
data-mask shapes what's typed into an input as the user types, such as a
phone or card number:
<input @bind="phone" data-mask="(999) 999-9999" inputmode="numeric" />
<input @bind="card" data-mask="9999 9999 9999 9999" />
<input @bind="plate" data-mask="aa-9999" />
- In a mask,
9is a digit,aa letter,*a letter or digit, and\makes the next character literal; anything else is shown as it is, filled in around what's typed (but never after the last character, so Backspace works as expected). - It happens in the page, before your
@bindor@inputsees the event, so the cursor stays where the user is typing, even in the middle. Characters that don't fit (a letter where a digit goes) are refused, and extra ones are dropped. Your field gets the shaped text,"(555) 123-4567". pyweft.maskshas the same rules in Python:masks.apply("(999) 999-9999", "5551234567")shapes a starting value,masks.complete(mask, text)checks every slot is filled, andmasks.typed(mask, text)gives just the typed characters ("5551234567"), for storing.
Moving focus and scrolling: ref¶
Mark an element with ref="name", then move keyboard focus to it or scroll
to it from Python:
<template>
<form @submit.prevent="save">
<input ref="title" @bind="title" />
<If cond="error"><p ref="problem" class="error">{{ error }}</p></If>
<button>Save</button>
</form>
</template>
<code>
title: str = ""
error: str = ""
def on_init(self):
self.focus("title") # ready to type as soon as it shows
def save(self):
if not self.title:
self.error = "Give it a title"
self.scroll_to("problem") # the <p> appears in this same render
self.focus("title")
</code>
- Requests wait for the render, so they can name an element that the
same handler makes appear, like
problemabove. refon a component tag (<ui.TextField ref="email" />) names the component;focus()then goes to the first thing inside it that takes focus, such as its input.- In a loop, make each name unique:
ref="row-{{ i }}". - A name that isn't on the page when the request is carried out is ignored.
| Method | Does |
|---|---|
focus(ref) |
focuses the element (or the first focusable thing inside it) |
focus_first(ref), focus_last(ref) |
focuses the first or last focusable element inside |
scroll_to(ref) |
scrolls the page just enough to show the element; scroll_to(ref, align="start") brings it to the top of the window (or "center", "end"), as a link to a section would |
keep_in_view(ref) |
shifts the element sideways just enough to stay inside the window: for a popup opening near the edge. keep_in_view(ref, vertical=True) shifts it up or down too, for a popup placed at a point, such as a context menu at the pointer; flip=True moves a popup opened beside its parent (a submenu) to the parent's other side when it doesn't fit |
save_focus(), restore_focus() |
remembers what has focus, and later puts it back, for popups |
ref is not written to the page as an attribute.
Component styles¶
A <style> block in a .weft file is scoped to that component: its
rules match only elements this component's template renders.
<template>
<li class="row"><span>{{ item['text'] }}</span></li>
</template>
<style>
.row { display: flex; gap: .5rem; }
span:hover { text-decoration: underline; }
</style>
Write selectors as if the component were the whole page. span:hover above
cannot reach a <span> rendered by a child or a parent, so two components can
both use .row without a naming scheme. Under the hood every element gets a
data-w-<component> marker and each selector has [data-w-<component>]
appended, which you will see in the browser's element inspector.
Two rules of thumb:
- A parent can style the outermost element of a child it places, so
.list > .rowin the parent works for spacing children. It cannot reach inside them. - Page-wide rules (
body, fonts, CSS variables) go in a.cssfile in the app directory, which is included unscoped.var(--name)defined there is usable from any component's<style>.
@media, @supports and @container blocks are scoped too; @keyframes,
@font-face and @import are passed through as written.