Handling Events

When Getting Started, we saw how IDOM makes it possible to write server-side code that defines basic views and can react to client-side events. The simplest way to listen and respond to events is by assigning a callable object to a VDOM an attribute where event signals are sent. This is relatively similar to handling events in React:

import json

import idom


@idom.component
def BasicButton():
    event, set_event = idom.hooks.use_state(None)
    return idom.html.div(
        idom.html.button({"onClick": lambda e: set_event(e)}, "click to see event"),
        idom.html.pre(json.dumps(event, indent=2)),
    )


idom.run(BasicButton)

Differences With React Events

Because IDOM operates server-side, there are inevitable limitations that prevent it from achieving perfect parity with all the behaviors of React. With that said, any feature that cannot be achieved in Python with IDOM, can be done by creating Custom Javascript Components.

Preventing Default Event Actions

Instead of calling an event.preventDefault() method as you would do in React, you must declare whether to prevent default behavior ahead of time. This can be accomplished using the event() decorator and setting prevent_default. For example, we can stop a link from going to the specified URL:

import idom


@idom.component
def DoNotChangePages():
    return idom.html.div(
        idom.html.p("Normally clicking this link would take you to a new page"),
        idom.html.a(
            {
                "onClick": idom.event(lambda e: None, prevent_default=True),
                "href": "https://google.com",
            },
            "https://google.com",
        ),
    )


idom.run(DoNotChangePages)

Unfortunately this means you cannot conditionally prevent default behavior in response to event data without writing Custom Javascript Components.

Stop Event Propogation

Similarly to preventing default behavior, you can use the event() decorator to forward declare whether or not you want events from a child element propogate up through the document to parent elements by setting stop_propagation. In the example below we place a red div inside a parent blue div. When propogation is turned on, clicking the red element will cause the handler for the outer blue one to fire. Conversely, when it’s off, only the handler for the red element will fire.

import idom


@idom.component
def DivInDiv():
    stop_propagatation, set_stop_propagatation = idom.hooks.use_state(True)
    inner_count, set_inner_count = idom.hooks.use_state(0)
    outer_count, set_outer_count = idom.hooks.use_state(0)

    div_in_div = idom.html.div(
        {
            "onClick": idom.event(lambda e: set_outer_count(outer_count + 1)),
            "style": {"height": "100px", "width": "100px", "backgroundColor": "red"},
        },
        idom.html.div(
            {
                "onClick": idom.event(
                    lambda e: set_inner_count(inner_count + 1),
                    stop_propagation=stop_propagatation,
                ),
                "style": {"height": "50px", "width": "50px", "backgroundColor": "blue"},
            },
        ),
    )

    return idom.html.div(
        idom.html.button(
            {"onClick": lambda e: set_stop_propagatation(not stop_propagatation)},
            "Toggle Propogation",
        ),
        idom.html.pre(f"Will propagate: {not stop_propagatation}"),
        idom.html.pre(f"Inner click count: {inner_count}"),
        idom.html.pre(f"Outer click count: {outer_count}"),
        div_in_div,
    )


idom.run(DivInDiv)

Event Data Serialization

Not all event data is serialized. The most notable example of this is the lack of a target key in the dictionary sent back to the handler. Instead, data which is not inherhently JSON serializable must be treated on a case-by-case basis. A simple case to demonstrate this is the currentTime attribute of audio and video elements. Normally this would be accessible via event.target.currenTime, but here it’s simply passed in under the key currentTime:

import json

import idom


@idom.component
def PlayDinosaurSound():
    event, set_event = idom.hooks.use_state(None)
    return idom.html.div(
        idom.html.audio(
            {
                "controls": True,
                "onTimeUpdate": lambda e: set_event(e),
                "src": "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
            }
        ),
        idom.html.pre(json.dumps(event, indent=2)),
    )


idom.run(PlayDinosaurSound)