TL;DR — Events in Fitz LiveViews carry data three ways: a click payload (
data-flv-value-*) tags a button with the value it should send; a form submit (data-flv-submit) reads the form's named inputs; and a live value (@input/@change) delivers a control's current value inpayload["value"]. All three land in the same place — apayloadmap your handler reads. This post builds a live name list (add / remove / count) that runs both server-rendered and as WebAssembly. (Part 3 of the FitzLiveViews series.)
Parts 1 and 2 covered the pitch and the counter. A counter only reads +1/-1 — no data flows in. Real UIs take input: text, selections, form fields. Here's how that data reaches your handlers.
The payload
Every event handler has a payload in scope — a Map<Str, Str>. The three mechanisms below all fill it; your handler reads it with payload["key"] (guard with payload.has("key")):
1. Click payload — a button that carries a value
Tag any element with data-flv-value-<key>="{expr}", and when a data-flv-click on it (or an ancestor) fires, that value rides along:
<button data-flv-click="remove" data-flv-value-item="{it}">×</button>
event remove() {
if (payload.has("item")) {
let target = payload["item"]
names = names.filter(fn(it) => it != target)
}
}
The delete button knows which row it is because the row's value is stamped on it. No IDs threaded through a callback, no closure capture.
2. Form submit — the whole form at once
data-flv-submit="handler" on a <form> reads each named input into the payload on submit; data-flv-clear resets a field afterward:
<form data-flv-submit="add">
<input name="item" placeholder="Add a name" data-flv-clear />
<button type="submit">Add</button>
</form>
event add() {
if (payload.has("item")) {
let n = payload["item"]
if (n != "") { names.push(n) }
}
}
payload["item"] is the input's value at submit time. No preventDefault, no FormData, no fetch.
3. Live value — @input / @change
For a control that reports as you type or on selection, @input (every keystroke) and @change (on blur/selection) deliver the current value in payload["value"]:
<input @input="on_name" value="{name}" />
<select @change="on_color"> … </select>
event on_name() { name = payload["value"] }
The same payload["value"] covers <input>, <select>, and <textarea>. On the server-rendered target this is the classic data-flv-change attribute; on the client-WASM target it's the @input / @change decorator — same payload, same handler, so one .fitzv serves both.
Putting it together — a live name list
component NameList {
state {
names: List<Str> = ["Ada", "Grace", "Margaret"]
}
event add() {
if (payload.has("item")) {
let n = payload["item"]
if (n != "") { names.push(n) }
}
}
event remove() {
if (payload.has("item")) {
let target = payload["item"]
names = names.filter(fn(it) => it != target)
}
}
<template>
<div class="names">
<form data-flv-submit="add">
<input name="item" placeholder="Add a name" data-flv-clear />
<button type="submit">Add</button>
</form>
<ul>
{#for it in names}
<li>{it} <button data-flv-click="remove" data-flv-value-item="{it}">×</button></li>
{/for}
</ul>
<p>{names.len()} total</p>
</div>
</template>
}
{#for it in names} iterates; {it} interpolates each item; the remove button stamps its own value. Add a name, remove a row, watch the count — no client code, no API.
Wire it into a main.fitz (the same shape as the counter in part 2) and it runs:
from fitz_liveviews import Html, html, live_layout, html_response,
LiveFrame, diff_html, component, dispatch_component_events, flv_register
from NameList import NameList, NameList_render, NameList_add, NameList_remove
@get("/")
fn page() -> Response {
return html_response(live_layout("/live/names", "names-app",
component("NameList", "root")))
}
@ws("/live/names")
async fn socket(ws: WsConn<LiveFrame>) {
let last = component("NameList", "root").raw
loop {
let frame = ws.recv()?
let _ = dispatch_component_events(frame)
let new_html = component("NameList", "root").raw
ws.send(LiveFrame { html: new_html, patches: diff_html(last, new_html) })?
last = new_html
}
}
@server(3000) fn main() => 0
fitz run, open http://localhost:3000, add and remove names. (I ran exactly this — the page renders the seeded list, the count, and the wired form/buttons.) The client-WASM build of the same list is running in the live gallery — add/remove/count entirely in your browser, offline.
Two honest caveats
-
Live text inputs re-mount. The current rendering model is dirty-flag + naive re-render: a state change rebuilds the component's DOM. For a
<select> @changethat's invisible; for a live text<input> @input, the field re-mounts each keystroke — the value re-binds viavalue="{name}", but the caret jumps to the end. Fine-grained reactivity (patch in place) is the next rendering-model step, and it's what fixes this. The value always reaches the handler reliably; that's what@inputguarantees today. -
String methods on WASM — now supported. A case-insensitive filter (
names.filter(fn(x) => q == "" or x.lower().contains(q.lower()))) used to be server-rendered only; as of Fitz core v0.29.8 the string methods (.lower/.contains/ …) and logicaland/orcompile on the client-WASM target too, so the filter runs offline. The only remaining wrinkle is the caret caveat above for the filter box itself while you type.
What's next in this series
- #4+ — Building the flagship. A complete admin panel in Fitz + Fitz LiveViews: cookie-based auth (Argon2id + JWT), live DataGrids querying Postgres over WebSockets, the packaged UI library, i18n, and a one-command Docker setup.
If typed events without a client framework sound good, star the repo. Next: a real app.
Top comments (0)