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 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:

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

DomEvent(payload: Dict[str, Any], current_target: Optional[Any] = 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 Optional[Any]

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.

set_handler

set_handler(node_id: int, event_prop_name: str, handler: Optional[Callable]) -> 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 Optional[Callable]

Callback to invoke. 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 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., "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.

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, with type, target (payload-backed view with value/checked/files/element), current_target (an id-backed Element), keyboard and mouse fields (key, code, modifier flags, button, client_x, client_y), prevent_default(), stop_propagation(), and raw (the native event, escape hatch).
  • Handlers can be attached via props like on_click, on_input, or onChange. 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_ or on is treated as an event handler and normalized to a DOM event type.
  • Normalization rules:
  • on_click becomes "click"
  • onInput/on_input becomes "input"
  • onClick/onclick becomes "click"
  • In general: remove the on/on_ prefix and lowercase the remainder.

Handler signature:

  • All handlers receive a DomEvent object. Use evt.prevent_default() and evt.stop_propagation() as needed. Access the original JS event via evt.raw only 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 RELEASE op 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 deprecated keypress)
  • Input and form: input, change, submit, reset
  • Focus: use focusin and focusout (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/focusout instead of focus/blur (which don't bubble).
  • Use mouseover/mouseout instead of mouseenter/mouseleave (which don't bubble).
  • Many media events (e.g., play, pause) and scroll don't bubble; attach direct listeners to the element or use window/document as 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 via addEventListener and a Ref.
  • Chrome/Edge may treat touchstart/touchmove listeners on document as passive by default, making preventDefault() a no-op. If you need to prevent scrolling, attach a direct listener with {"passive": False} options and a Ref.
  • keypress is deprecated and may behave inconsistently across browsers; prefer keydown/keyup.
  • The DomEvent object exposes a stable, Python-friendly surface backed by the dispatch payload. Accessing evt.raw is possible but not recommended for portability across Pyodide and non-browser tests.