DEV Community

Martin Palopoli
Martin Palopoli

Posted on

Forms, payloads, and live inputs in Fitz LiveViews

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 in payload["value"]. All three land in the same place — a payload map 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>
Enter fullscreen mode Exit fullscreen mode
event remove() {
  if (payload.has("item")) {
    let target = payload["item"]
    names = names.filter(fn(it) => it != target)
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
event add() {
  if (payload.has("item")) {
    let n = payload["item"]
    if (n != "") { names.push(n) }
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
event on_name() { name = payload["value"] }
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

{#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
Enter fullscreen mode Exit fullscreen mode

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> @change that's invisible; for a live text <input> @input, the field re-mounts each keystroke — the value re-binds via value="{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 @input guarantees 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 logical and/or compile 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)