Skip to content

Events

wybthon.events

events

Event delegation utilities for VDOM event handling in the browser.

Wybthon installs one root listener per event type on document and dispatches to per-node handlers using a stable data-wybid attribute. This keeps the number of native listeners small even for large trees and lets the renderer add or remove handlers cheaply during reconciliation.

Public surface:

The remaining helpers are internal: set_handler, remove_all_for, and the listener ref-counting utilities are exercised by the renderer.

See Also

Classes:

Name Description
DomEvent

Thin wrapper around a JS event with convenience helpers.

DomEvent

DomEvent(js_event: Any)

Thin wrapper around a JS event with convenience helpers.

Attributes:

Name Type Description
type

Event type string (e.g. "click").

target

The original event target as an Element, or None if unavailable.

current_target Optional[Element]

The currently-dispatched element while bubbling through delegated handlers; updated by the dispatcher.

Wrap a raw JS event object.

Parameters:

Name Type Description Default
js_event Any

The native browser event object.

required

Methods:

Name Description
prevent_default

Prevent the default browser action for this event, if possible.

stop_propagation

Stop propagation through both the JS and the delegated chains.

prevent_default

prevent_default() -> None

Prevent the default browser action for this event, if possible.

stop_propagation

stop_propagation() -> None

Stop propagation through both the JS and the delegated chains.

Sets an internal flag that causes Wybthon's dispatcher to stop walking the DOM ancestors, and also calls the native stopPropagation so other JS listeners do not fire.

set_handler

set_handler(el: Element, event_prop_name: str, handler: Optional[Callable]) -> None

Attach, update, or remove a handler for an event property on an element.

Parameters:

Name Type Description Default
el Element

Wrapped element 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 element.

required

remove_all_for

remove_all_for(el: Element) -> None

Remove every delegated handler registered for el.

Called by the renderer during unmount to drop the handler table and decrement the active-listener counters so unused root listeners are released.

Wybthon's event system provides delegated event handling and a thin DomEvent wrapper.

  • DomEvent: wrapper with type, target (Element|None), current_target (Element|None), prevent_default(), stop_propagation().
  • 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._js_event only if absolutely necessary.

Delegation and bubbling

Wybthon installs one document-level listener per event type on first use and walks up from the original target to parent nodes, invoking any handlers registered for that event_type. stop_propagation() prevents further bubbling within Wybthon's dispatcher.

Cleanup guarantees:

  • When a node is unmounted, all of its event handlers are removed from the delegation map.
  • 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 does not 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 do not bubble will not trigger handlers when attached via props. Use the suggested alternatives or attach a direct listener via wybthon.dom.Element.on using a Ref.

  • Use focusin/focusout instead of focus/blur (which do not bubble).
  • Use mouseover/mouseout instead of mouseenter/mouseleave (which do not bubble).
  • Many media events (e.g., play, pause) and scroll do not 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):

from wybthon import component, h, on_mount, Ref

@component
def Video():
    ref = Ref()

    def setup():
        if ref.current is not None:
            ref.current.on("play", lambda e: print("playing"))

    on_mount(setup)

    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 Element.on.
  • 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 options={"passive": False} using Element.on and a Ref.
  • keypress is deprecated and may behave inconsistently across browsers; prefer keydown/keyup.
  • The DomEvent wrapper exposes a stable, Python-friendly surface. Accessing evt._js_event is possible but not recommended for portability across Pyodide and non-browser tests.