Events
wybthon.events¶
events
¶
Delegated event handling for the batched renderer.
Event delegation lives in the rendering kernel: one native listener per
event type is installed on document, walks the ancestor chain of the
event target natively, and calls into Python once per matched handler.
The call carries a small JSON payload (event type, target value/checked
state, key, mouse buttons, modifiers), so the common handler patterns
(evt.target.value, evt.key, evt.prevent_default()) never touch a
JsProxy at all.
Registering a handler is itself a batched op (LISTEN), so mounting a
list with thousands of handlers adds nothing to the bridge-crossing
count.
Public surface:
DomEvent: the event object passed to handlers, exposingtype,target,current_target, key and mouse fields, and helpers likeprevent_defaultandstop_propagation.
The remaining helpers are internal:
set_handler and
remove_handlers_for are called
by the renderer, and dispatch_event
is the entry point the kernel invokes when a native event fires.
See Also
Classes:
| Name | Description |
|---|---|
DomEvent |
The event object Wybthon passes to delegated handlers. |
DomEvent
¶
The event object Wybthon passes to delegated handlers.
Built from the kernel's dispatch payload rather than a JsProxy,
so reading it is free of bridge crossings. Use
raw to reach the native event object for
anything not covered by the payload.
Attributes:
| Name | Type | Description |
|---|---|---|
type |
Event type string (e.g., |
|
target |
Payload-backed view of the original event target,
exposing |
|
current_target |
The element whose handler is currently running
(an id-backed |
|
key |
|
|
code |
|
|
alt_key |
Whether Alt was held. |
|
ctrl_key |
Whether Ctrl was held. |
|
meta_key |
Whether Meta/Cmd was held. |
|
shift_key |
Whether Shift was held. |
|
button |
|
|
client_x |
Pointer x position, when applicable. |
|
client_y |
Pointer y position, when applicable. |
Build an event from a dispatch payload dict.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
Dict[str, Any]
|
Parsed dispatch payload from the kernel. |
required |
current_target
|
Optional[Any]
|
The |
None
|
Methods:
| Name | Description |
|---|---|
prevent_default |
Prevent the default browser action for this event. |
stop_propagation |
Stop the delegated dispatch from walking further up the tree. |
set_handler
¶
Attach, update, or remove a handler for an event property on a node.
Registration is batched: the kernel-side LISTEN/UNLISTEN ops
ride the same command buffer as the DOM mutations, so wiring
handlers costs no extra bridge crossings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
int
|
Kernel node id to attach to. |
required |
event_prop_name
|
str
|
Prop name as seen on the |
required |
handler
|
Optional[Callable]
|
Callback to invoke. Pass |
required |
remove_handlers_for
¶
remove_handlers_for(node_id: int) -> None
Drop every handler registered for node_id (Python side only).
Called by the renderer during unmount. The kernel-side listener
bookkeeping is cleared by the RELEASE op that accompanies the
subtree removal, so no per-handler ops are needed.
dispatch_event
¶
Invoke the handler for (node_id, event_type) with a payload.
This is the entry point the kernel's native root listener calls, once per matched node while walking the ancestor chain.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
int
|
Id of the node whose handler should run. |
required |
event_type
|
str
|
DOM event type (e.g., |
required |
payload_json
|
str
|
JSON-encoded payload built natively by the kernel. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Flag bits for the kernel: bit 1 stops the delegated walk and |
int
|
native propagation, bit 2 calls |
Wybthon's event system provides kernel-delegated event handling and a payload-backed DomEvent object.
DomEvent: built from a JSON payload assembled natively at dispatch time, withtype,target(payload-backed view withvalue/checked/files/element),current_target(an id-backedElement), keyboard and mouse fields (key,code, modifier flags,button,client_x,client_y),prevent_default(),stop_propagation(), andraw(the native event, escape hatch).- Handlers can be attached via props like
on_click,on_input, oronChange. Names are normalized to DOM event types. - Delegation is automatic and handlers are cleaned up on unmount. Document-level delegated listeners are installed on first use per event type and are automatically removed when no handlers remain for that type (e.g., after unmount/diff removes all handlers).
Naming and normalization¶
- Any prop starting with
on_oronis treated as an event handler and normalized to a DOM event type. - Normalization rules:
on_clickbecomes "click"onInput/on_inputbecomes "input"onClick/onclickbecomes "click"- In general: remove the
on/on_prefix and lowercase the remainder.
Handler signature:
- All handlers receive a
DomEventobject. Useevt.prevent_default()andevt.stop_propagation()as needed. Access the original JS event viaevt.rawonly if absolutely necessary, and only synchronously during dispatch.
Delegation and bubbling¶
Delegation runs inside the rendering kernel: one native document-level listener per event type walks up from the original target, and calls into Python once per node that registered a handler for that type, passing the payload as a single JSON string. Handler registration (LISTEN/UNLISTEN) rides the batched op buffer, so wiring thousands of handlers costs no extra bridge crossings. stop_propagation() prevents further delegated bubbling and stops native propagation.
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
RELEASEop that retires the subtree's 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.
Common event types¶
You can attach handlers for any standard DOM event that bubbles. Commonly used types include:
- Mouse:
click,dblclick,mousedown,mouseup,mousemove,mouseover,mouseout,contextmenu,wheel - Keyboard:
keydown,keyup(avoid deprecatedkeypress) - Input and form:
input,change,submit,reset - Focus: use
focusinandfocusout(see non-bubbling notes below) - Pointer:
pointerdown,pointerup,pointermove,pointerover,pointerout,pointercancel - Touch:
touchstart,touchmove,touchend,touchcancel - Composition/IME:
compositionstart,compositionupdate,compositionend - Drag and drop:
dragstart,dragend,dragenter,dragleave,dragover,drop
Wybthon doesn't restrict event type names; if the browser fires it and it bubbles, the delegated listener will see it.
Non-bubbling and special-case events¶
Because Wybthon uses document-level delegation, event types that don't bubble won't trigger handlers when attached via props. Use the suggested alternatives or attach a direct native listener through Pyodide using a Ref.
- Use
focusin/focusoutinstead offocus/blur(which don't bubble). - Use
mouseover/mouseoutinstead ofmouseenter/mouseleave(which don't bubble). - Many media events (e.g.,
play,pause) andscrolldon't bubble; attach direct listeners to the element or usewindow/documentas appropriate.
Direct listeners example (when you need non-bubbling events or options like passive: False). Wrap the handler in create_proxy so Pyodide keeps it alive, and remove it on cleanup:
from pyodide.ffi import create_proxy
from wybthon import Ref, component, h, on_cleanup, on_mount
@component
def Video():
ref = Ref()
proxy = create_proxy(lambda e: print("playing"))
def setup():
if ref.current is not None:
ref.current.element.addEventListener("play", proxy)
def teardown():
if ref.current is not None:
ref.current.element.removeEventListener("play", proxy)
proxy.destroy()
on_mount(setup)
on_cleanup(teardown)
return h("video", {"ref": ref})
Pyodide and cross-browser notes¶
- Event delegation relies on bubbling to
document. For non-bubbling types, prefer the alternatives above or attach direct listeners viaaddEventListenerand aRef. - Chrome/Edge may treat
touchstart/touchmovelisteners ondocumentas passive by default, makingpreventDefault()a no-op. If you need to prevent scrolling, attach a direct listener with{"passive": False}options and aRef. keypressis deprecated and may behave inconsistently across browsers; preferkeydown/keyup.- The
DomEventobject exposes a stable, Python-friendly surface backed by the dispatch payload. Accessingevt.rawis possible but not recommended for portability across Pyodide and non-browser tests.