Skip to content

Events and Async

oldmangan edited this page Oct 6, 2026 · 2 revisions

Handlers

Register handlers with a widget's .on_* methods. Payloads arrive converted to Python dicts, and most handlers receive one dict:

tree.on_select(lambda p: show(p["id"], p["node"]))
table.on_action(lambda p: run(p["action"], p["selected"]))
form.on_submit(lambda values: save(values))

Each widget's page lists its payloads. Handlers run synchronously, inside the browser event that triggered them.

Calling the backend

BFF classes get an awaitable name_async() companion for every export. Call that one: the plain name() is a blocking request, deprecated through pyTincture 1.x. Handlers and load_ui() are synchronous, so schedule the coroutine, and report its exceptions, because asyncio.ensure_future swallows them:

import asyncio, traceback
import js

def spawn(coro, label):
    async def guarded():
        try:
            await coro
        except Exception:
            js.console.error(f"{label} failed:\n{traceback.format_exc()}")
    return asyncio.ensure_future(guarded())

form.on_submit(lambda values: spawn(self.save(values), "save"))

async def save(self, values):
    self.form.set_busy(True)
    try:
        result = await Store().save_async(values)
    finally:
        self.form.set_busy(False)

Never call asyncio.run in the browser. It creates and closes a second event loop and breaks Pyodide's loop for the rest of the session.

BFF state

pyTincture re-executes a BFF module's source for every call, so module-level lists, caches and locks are recreated per request. Keep state in storage, or in an ordinary module the BFF imports by name.

Pyodide gotchas

  • wapyt payloads and getters give real None for JS null (since #29).
  • Your own JS calls don't. In code that calls the DOM or JS directly, JS null arrives as JsNull, which is not None: js.document.getElementById("x") returns JsNull when the element is missing, and if el is None: never fires. Test truthiness (if not el:), or pass the value through wapyt._runtime.to_plain().
  • A missing JS property raises AttributeError instead of returning undefined. Use getAttribute() or hasattr() rather than element.dataset.foo.
  • Pass js.undefined where a JS API distinguishes "absent" from null.
  • DOM listeners you add yourself need pyodide.ffi.create_proxy(fn); keep a reference to the proxy for as long as the listener lives.

Clone this wiki locally