Events
Events¶
Event handlers are delegated at the document root.
from wybthon import h
def Button(props):
return h("button", {"on_click": lambda evt: print("clicked")}, "Click")
Supported prop names: on_click, on_input, on_change, etc. Both on_foo and onFoo styles are supported; they normalize to DOM event names.
DomEvent¶
Handlers receive a DomEvent object built from a small payload the
dispatcher assembles natively, so reading it never crosses the
Python-to-JS bridge:
type: the event type string (e.g.,"click","input").target: a payload-backed view of the original event target. The properties handlers actually read (value,checked,files) are exposed directly, mirroring the JS DOM API. The raw JS node is available viatarget.elementas an escape hatch.current_target: anElementfor the node whose handler is currently running during delegated bubbling. This is set for you before your handler is called.key,code,alt_key,ctrl_key,meta_key,shift_key,button,client_x,client_y: keyboard and mouse fields, straight from the payload.prevent_default(): marks the event so the dispatcher callspreventDefault()on the native event. Safe to call in non-browser tests.stop_propagation(): stops delegated propagation for this event, including native propagation above the handled node.raw: the native browser event object, for anything not covered by the payload. Only valid synchronously during dispatch.
Read input values exactly like in JS/React/SolidJS:
Example:
from wybthon import h
def Form(props):
def on_submit(evt):
evt.prevent_default()
print("submitted from", evt.current_target)
return h("form", {"on_submit": on_submit},
h("input", {"name": "q", "on_input": lambda e: print("input", e.target)}),
h("button", {"type": "submit"}, "Go"))
Delegation model¶
Delegation lives in the rendering kernel (the JavaScript side of the
batched renderer). The kernel installs one document-level listener per
event type on first use, walks up from the original target natively,
and calls into Python once per node that actually registered a handler
for that type. The payload crosses the bridge as one JSON string, so a
click on a row in a 10,000-row table costs a single Python call.
Registering a handler is itself a batched op (LISTEN), riding the
same command buffer as the DOM mutations; mounting a list with
thousands of handlers adds nothing to the bridge-crossing count.
Cleanup guarantees:
- When a node is unmounted, its handlers are dropped on the Python side and the kernel's listener bookkeeping is cleared by the same
RELEASEop that retires the node ids. - When the last handler for an event type is removed across the entire document (e.g., via unmount or by diffing a handler to
None), the document-level listener for that event type is automatically removed.
Naming and normalization¶
on_clickbecomes "click"onInput/on_inputbecomes "input"onClick/onclickbecomes "click"- Any prop starting with
on_oronis treated as an event handler; non-callable values are ignored.
Event types that work best with delegation¶
Prefer events that bubble:
- Mouse:
click,dblclick,mousedown,mouseup,mousemove,mouseover,mouseout,contextmenu,wheel - Keyboard:
keydown,keyup(avoid deprecatedkeypress) - Input and form:
input,change,submit,reset - Pointer:
pointerdown,pointerup,pointermove,pointerover,pointerout,pointercancel
Non-bubbling alternatives:
- Use
focusin/focusoutinstead offocus/blur. - Use
mouseover/mouseoutinstead ofmouseenter/mouseleave.
When you need non-bubbling events or special options (e.g., passive: False), attach a native listener directly through Pyodide using a Ref. Wrap the handler in create_proxy so it survives garbage collection, and remove it on cleanup:
from pyodide.ffi import create_proxy
from wybthon import Ref, component, div, on_cleanup, on_mount
@component
def HoverDemo():
ref = Ref()
proxy = create_proxy(lambda e: print("entered"))
def setup():
if ref.current is not None:
ref.current.element.addEventListener("mouseenter", proxy)
def teardown():
if ref.current is not None:
ref.current.element.removeEventListener("mouseenter", proxy)
proxy.destroy()
on_mount(setup)
on_cleanup(teardown)
return div("Hover me", ref=ref, class_="box")
Pyodide cross-browser notes¶
- Delegation depends on bubbling to
document. For non-bubbling types, use the alternatives above or a directaddEventListenerviaRef. - Chrome/Edge may treat
touchstart/touchmoveondocumentas passive, sopreventDefault()may be ignored. Use a direct listener with{"passive": False}options if you need to prevent scrolling. keypressis deprecated; preferkeydown/keyup.
Next steps¶
- Read Forms for higher-level controlled-input patterns.
- See DOM Interop for the underlying
ElementandRefAPIs. - Browse the
eventsAPI reference for delegation internals.