Skip to content

pyscript.event

Event handling for PyScript.

This module provides two complementary systems:

  1. The Event class: A simple publish-subscribe pattern for custom events within your Python code.

  2. The @when decorator: Connects Python functions to browser DOM events, or instances of the Event class, allowing you to respond to user interactions like clicks, key presses and form submissions, or to custom events defined in your Python code.

Event

A custom event that can notify multiple listeners when triggered.

Use this class to create your own event system within Python code. Listeners can be either regular functions or async functions.

from pyscript.events import Event

# Create a custom event.
data_loaded = Event()

# Add a listener.
def on_data_loaded(result):
    print(f"Data loaded: {result}")

data_loaded.add_listener(on_data_loaded)

# Time passes.... trigger the event.
data_loaded.trigger({"data": 123})
Source code in pyscript/events.py
class Event:
    """
    A custom event that can notify multiple listeners when triggered.

    Use this class to create your own event system within Python code.
    Listeners can be either regular functions or async functions.

    ```python
    from pyscript.events import Event

    # Create a custom event.
    data_loaded = Event()

    # Add a listener.
    def on_data_loaded(result):
        print(f"Data loaded: {result}")

    data_loaded.add_listener(on_data_loaded)

    # Time passes.... trigger the event.
    data_loaded.trigger({"data": 123})
    ```
    """

    def __init__(self):
        self._listeners = []

    def trigger(self, result):
        """
        Trigger the event and notify all listeners with the given `result`.
        """
        for listener in self._listeners:
            if is_awaitable(listener):
                asyncio.create_task(listener(result))
            else:
                listener(result)

    def add_listener(self, listener):
        """
        Add a function to be called when this event is triggered.

        The `listener` must be callable. It can be either a regular function
        or an async function. Duplicate listeners are ignored.
        """
        if not callable(listener):
            msg = "Listener must be callable."
            raise ValueError(msg)
        if listener not in self._listeners:
            self._listeners.append(listener)

    def remove_listener(self, *listeners):
        """
        Remove specified `listeners`. If none specified, remove all listeners.
        """
        if listeners:
            for listener in listeners:
                try:
                    self._listeners.remove(listener)
                except ValueError:
                    pass  # Silently ignore listeners not in the list.
        else:
            self._listeners = []

trigger(result)

Trigger the event and notify all listeners with the given result.

Source code in pyscript/events.py
def trigger(self, result):
    """
    Trigger the event and notify all listeners with the given `result`.
    """
    for listener in self._listeners:
        if is_awaitable(listener):
            asyncio.create_task(listener(result))
        else:
            listener(result)

add_listener(listener)

Add a function to be called when this event is triggered.

The listener must be callable. It can be either a regular function or an async function. Duplicate listeners are ignored.

Source code in pyscript/events.py
def add_listener(self, listener):
    """
    Add a function to be called when this event is triggered.

    The `listener` must be callable. It can be either a regular function
    or an async function. Duplicate listeners are ignored.
    """
    if not callable(listener):
        msg = "Listener must be callable."
        raise ValueError(msg)
    if listener not in self._listeners:
        self._listeners.append(listener)

remove_listener(*listeners)

Remove specified listeners. If none specified, remove all listeners.

Source code in pyscript/events.py
def remove_listener(self, *listeners):
    """
    Remove specified `listeners`. If none specified, remove all listeners.
    """
    if listeners:
        for listener in listeners:
            try:
                self._listeners.remove(listener)
            except ValueError:
                pass  # Silently ignore listeners not in the list.
    else:
        self._listeners = []

when(event_type, selector=None, **options)

A decorator to handle DOM events or custom Event objects.

For DOM events, specify the event_type (e.g. "click") and a selector for target elements. For custom Event objects, just pass the Event instance as the event_type. It's also possible to pass a list of Event objects. The selector is required only for DOM events. It should be a CSS selector string, Element, ElementCollection, or list of DOM elements.

For DOM events only, you can specify optional addEventListener options: capture, once, passive, or signal.

The decorated function can be either a regular function or an async function. If the function accepts an argument, it will receive the event object (for DOM events) or the Event's result (for custom events). A function does not need to accept any arguments if it doesn't require them.

from pyscript import when, display

# Handle DOM events.
@when("click", "#my-button")
def handle_click(event):
    display("Button clicked!")

# Handle DOM events with options.
@when("click", "#my-button", once=True)
def handle_click_once(event):
    display("Button clicked once!")

# Handle custom events.
my_event = Event()

@when(my_event)
def handle_custom():  # No event argument needed.
    display("Custom event triggered!")

# Handle multiple custom events.
another_event = Event()

def another_handler():
    display("Another custom event handler.")

# Attach the same handler to multiple events but not as a decorator.
when([my_event, another_event])(another_handler)

# Trigger an Event instance from a DOM event via @when.
@when("click", "#my-button")
def handle_click(event):
    another_event.trigger("Button clicked!")

# Stacked decorators also work.
@when("mouseover", "#my-div")
@when(my_event)
def handle_both(event):
    display("Either mouseover or custom event triggered!")
Source code in pyscript/events.py
def when(event_type, selector=None, **options):
    """
    A decorator to handle DOM events or custom `Event` objects.

    For DOM events, specify the `event_type` (e.g. `"click"`) and a `selector`
    for target elements. For custom `Event` objects, just pass the `Event`
    instance as the `event_type`. It's also possible to pass a list of `Event`
    objects. The `selector` is required only for DOM events. It should be a
    [CSS selector string](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors),
    `Element`, `ElementCollection`, or list of DOM elements.

    For DOM events only, you can specify optional
    [addEventListener options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options):
    `capture`, `once`, `passive`, or `signal`.

    The decorated function can be either a regular function or an async
    function. If the function accepts an argument, it will receive the event
    object (for DOM events) or the Event's result (for custom events). A
    function does not need to accept any arguments if it doesn't require them.

    ```python
    from pyscript import when, display

    # Handle DOM events.
    @when("click", "#my-button")
    def handle_click(event):
        display("Button clicked!")

    # Handle DOM events with options.
    @when("click", "#my-button", once=True)
    def handle_click_once(event):
        display("Button clicked once!")

    # Handle custom events.
    my_event = Event()

    @when(my_event)
    def handle_custom():  # No event argument needed.
        display("Custom event triggered!")

    # Handle multiple custom events.
    another_event = Event()

    def another_handler():
        display("Another custom event handler.")

    # Attach the same handler to multiple events but not as a decorator.
    when([my_event, another_event])(another_handler)

    # Trigger an Event instance from a DOM event via @when.
    @when("click", "#my-button")
    def handle_click(event):
        another_event.trigger("Button clicked!")

    # Stacked decorators also work.
    @when("mouseover", "#my-div")
    @when(my_event)
    def handle_both(event):
        display("Either mouseover or custom event triggered!")
    ```
    """
    if isinstance(event_type, str):
        # This is a DOM event to handle, so check and use the selector.
        if not selector:
            raise ValueError("Selector required for DOM event handling.")
        elements = _get_elements(selector)
        if not elements:
            raise ValueError(f"No elements found for selector: {selector}")

    def decorator(func):
        wrapper = _create_wrapper(func)
        if isinstance(event_type, Event):
            # Custom Event - add listener.
            event_type.add_listener(wrapper)
        elif isinstance(event_type, list) and all(
            isinstance(t, Event) for t in event_type
        ):
            # List of custom Events - add listener to each.
            for event in event_type:
                event.add_listener(wrapper)
        else:
            # DOM event - attach to all matched elements.
            for element in elements:
                element.addEventListener(
                    event_type,
                    create_proxy(wrapper),
                    to_js(options) if options else False,
                )
        return wrapper

    return decorator