Skip to content

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 each render root (the container passed to render, plus any Portal target), walks the ancestor chain of the event target natively, and calls into Python once for the bubbling route. Non-bubbling events and explicit native listener options use direct listeners. 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 costs no bridge crossing: a template declares its delegated listeners when it's registered, so cloning a row marks them natively, and other handlers ride the same command buffer as the DOM mutations (LISTEN).

A handler may take the DomEvent or no arguments at all: on_click=lambda: set_open(False).

Public surface:

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
EventHandler

A handler with native listener options, constructed with event.

DomEvent

The event object Wybthon passes to delegated handlers.

Functions:

Name Description
event

Configure a native listener. Ordinary handlers use batched delegation.

EventHandler dataclass

EventHandler(callback: Callable[..., Any], capture: bool = False, passive: bool = False, once: bool = False)

A handler with native listener options, constructed with event.

DomEvent

DomEvent(payload: dict[str, Any], current_target: Any | None = None)

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., "click").

target

Payload-backed view of the original event target, exposing value, checked, files, and element.

current_target

The element whose handler is currently running (an id-backed Element).

key

KeyboardEvent.key, or None for non-keyboard events.

code

KeyboardEvent.code, or None.

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

MouseEvent.button (0 for primary).

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 Any | None

The Element whose handler is being invoked.

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.

raw property

raw: Any

The native browser event object (escape hatch).

Only valid synchronously during dispatch; returns None afterwards.

prevent_default

prevent_default() -> None

Prevent the default browser action for this event.

stop_propagation

stop_propagation() -> None

Stop the delegated dispatch from walking further up the tree.

Also stops native propagation so other JS listeners above don't fire.

event

event(handler: Callable[..., Any], *, capture: bool = False, passive: bool = False, once: bool = False) -> EventHandler

Configure a native listener. Ordinary handlers use batched delegation.

bind_delegated

bind_delegated(node_id: int, key: str, prop: str, callback: Any) -> None

Record a delegated handler whose listener the node's template already declared.

set_handler

set_handler(node_id: int, event_prop_name: str, handler: Callable[..., Any] | None) -> None

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 VNode (e.g. "on_click"); normalized to the underlying DOM event type ("on_click" → "click").

required
handler Callable[..., Any] | None

Callback to invoke, with the event or no arguments. Pass None to remove an existing handler for this event type on this node.

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

dispatch_event(node_id: int, event_type: str, payload_json: str) -> int

Invoke the handler for (node_id, event_type) with a payload.

This is the entry point the kernel's native root listener calls, once for the matching bubbling route, or once for a direct native listener.

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., "click").

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 preventDefault.

What's in this module

Event handling is delegated at the render root. The JS kernel installs one native listener per event type on each container passed to render (falling back to document when no root is registered), walks the ancestor chain natively when an event fires, and calls into Python once for the matching bubbling route with a small JSON payload. Handlers receive a DomEvent built from that payload, so the common reads (evt.target.value, evt.key) never touch a JsProxy. Registering a handler costs no bridge crossing: handlers on template-mounted nodes are declared by the template itself, so a cloned row marks its listeners natively, and other handlers ride the command buffer as a batched LISTEN op.

Name Description
DomEvent The event object: type, target (.value, .checked, .files, .element), current_target, key, code, alt_key, ctrl_key, meta_key, shift_key, button, client_x, client_y, prevent_default(), stop_propagation(), raw.
event event(handler, *, capture=False, passive=False, once=False): attach a direct native listener with options instead of delegating.
EventHandler What event returns: the callback plus its listener options.

set_handler, remove_handlers_for, and dispatch_event are internal entry points used by the renderer and the kernel.

Handler props

  • Any prop named on_<type> (or on<Type>) is a handler: on_click, on_input, on_keydown, onChange. The prefix is stripped and the remainder lower-cased to get the DOM event type.
  • A handler takes the DomEvent or no arguments at all (on_click=lambda: set_open(False)); the arity is checked once, at registration. Signal writes made inside a handler are flushed automatically when it returns, so the DOM updates before the browser paints.
  • evt.raw is the native event and is valid only synchronously during dispatch.
from wybthon import DomEvent, button, create_signal, event, form, input_, li, ul

items, set_items = create_signal([])
draft, set_draft = create_signal("")


def submit(evt: DomEvent) -> None:
    evt.prevent_default()
    set_items(lambda xs: [*xs, draft.peek()])
    set_draft("")


def keydown(evt: DomEvent) -> None:
    if evt.key == "Escape":
        set_draft("")


view = form(
    input_(value=draft, on_input=lambda e: set_draft(e.target.value), on_keydown=keydown),
    button("Clear", type="button", on_click=lambda: set_items([])),  # no event needed
    ul(lambda: [li(x) for x in items()]),
    on_submit=submit,
    on_touchstart=event(lambda: None, passive=True),  # direct native listener
)

Delegation notes

  • Delegation relies on bubbling, so non-bubbling types (focus, blur, mouseenter, mouseleave, pointerenter, pointerleave, scroll, load, error, invalid, and toggle) get a direct native listener on the element instead. For listener options such as passive or capture, wrap the handler in event, which also attaches a direct listener.
  • stop_propagation() stops both the delegated walk and native propagation; prevent_default() is applied by the kernel after the handler returns.
  • Unmounting a subtree drops its handlers on the Python side, and the kernel's DISPOSE_RANGE or DISPOSE command clears its listener bookkeeping natively. Root.dispose() unregisters the container as a delegation root.
  • In CPython tests, wybthon.testing fires events through the real dispatcher: fire.click(node), fire.input(node, value), fire.key_down(node, key).

See also