Responding to Events#
ReactPy lets you add event handlers to your parts of the interface. These events handlers are functions which can be assigned to a part of a UI such that, when a user iteracts with the interface, those functions get triggered. Examples of interaction include clicking, hovering, of focusing on form inputs, and more.
Adding Event Handlers#
To start out weβll just display a button that, for the moment, doesnβt do anything:
from reactpy import component, html, run
@component
def Button():
return html.button("I don't do anything yet")
run(Button)
To add an event handler to this button weβll do three things:
Declare a function called
handle_event(event)
inside the body of ourButton
componentAdd logic to
handle_event
that will print theevent
it receives to the console.Add an
"onClick": handle_event
attribute to the<button>
element.
from reactpy import component, html, run
@component
def Button():
def handle_event(event):
print(event)
return html.button({"on_click": handle_event}, "Click me!")
run(Button)
Note
Normally print statements will only be displayed in the terminal where you launched ReactPy.
It may feel weird to define a function within a function like this, but doing so allows
the handle_event
function to access information from within the scope of the
component. Thatβs important if you want to use any arguments that may have been passed
your component in the handler:
from reactpy import component, html, run
@component
def PrintButton(display_text, message_text):
def handle_event(event):
print(message_text)
return html.button({"on_click": handle_event}, display_text)
@component
def App():
return html.div(
PrintButton("Play", "Playing"),
PrintButton("Pause", "Paused"),
)
run(App)
With all that said, since our handle_event
function isnβt doing that much work, if
we wanted to streamline our component definition, we could pass in our event handler as a
lambda:
html.button({"onClick": lambda event: print(message_text)}, "Click me!")
Supported Event Types#
Since ReactPyβs event information comes from React, most the the information (with some exceptions) about how React handles events translates directly to ReactPy. Follow the links below to learn about each category of event:
Passing Handlers to Components#
A common pattern when factoring out common logic is to pass event handlers into a more
generic component definition. This allows the component to focus on the things which are
common while still giving its usages customizablity. Consider the case below where we
want to create a generic Button
component that can be used for a variety of purpose:
from reactpy import component, html, run
@component
def Button(display_text, on_click):
return html.button({"on_click": on_click}, display_text)
@component
def PlayButton(movie_name):
def handle_click(event):
print(f"Playing {movie_name}")
return Button(f"Play {movie_name}", on_click=handle_click)
@component
def FastForwardButton():
def handle_click(event):
print("Skipping ahead")
return Button("Fast forward", on_click=handle_click)
@component
def App():
return html.div(
PlayButton("Buena Vista Social Club"),
FastForwardButton(),
)
run(App)
Async Event Handlers#
Sometimes event handlers need to execute asynchronous tasks when they are triggered.
Behind the scenes, ReactPy is running an asyncio
event loop for just this purpose.
By defining your event handler as an asynchronous function instead of a normal
synchronous one. In the layout below we sleep for several seconds before printing out a
message in the first button. However, because the event handler is asynchronous, the
handler for the second button is still able to respond:
import asyncio
from reactpy import component, html, run
@component
def ButtonWithDelay(message, delay):
async def handle_event(event):
await asyncio.sleep(delay)
print(message)
return html.button({"on_click": handle_event}, message)
@component
def App():
return html.div(
ButtonWithDelay("print 3 seconds later", delay=3),
ButtonWithDelay("print immediately", delay=0),
)
run(App)
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
inherently 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.currentTime
, but here
itβs simply passed in under the key currentTime
:
import json
import reactpy
@reactpy.component
def PlayDinosaurSound():
event, set_event = reactpy.hooks.use_state(None)
return reactpy.html.div(
reactpy.html.audio(
{
"controls": True,
"on_time_update": lambda e: set_event(e),
"src": "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
}
),
reactpy.html.pre(json.dumps(event, indent=2)),
)
reactpy.run(PlayDinosaurSound)
Client-side Event Behavior#
Because ReactPy 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 ReactPy, 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:
from reactpy import component, event, html, run
@component
def DoNotChangePages():
return html.div(
html.p("Normally clicking this link would take you to a new page"),
html.a(
{
"on_click": event(lambda event: None, prevent_default=True),
"href": "https://google.com",
},
"https://google.com",
),
)
run(DoNotChangePages)
Unfortunately this means you cannot conditionally prevent default behavior in response to event data without writing Custom Javascript Components.
Stop Event Propagation#
Similarly to preventing default behavior, you
can use the event()
decorator to prevent events originating in a
child element from propagating to parent elements by setting stop_propagation
. In
the example below we place a red div
inside a parent blue div
. When propagation
is turned on, clicking the red element will cause the handler for the outer blue one to
trigger. Conversely, when itβs off, only the handler for the red element will trigger.
from reactpy import component, event, hooks, html, run
@component
def DivInDiv():
stop_propagatation, set_stop_propagatation = hooks.use_state(True)
inner_count, set_inner_count = hooks.use_state(0)
outer_count, set_outer_count = hooks.use_state(0)
div_in_div = html.div(
{
"on_click": lambda event: set_outer_count(outer_count + 1),
"style": {"height": "100px", "width": "100px", "background_color": "red"},
},
html.div(
{
"on_click": event(
lambda event: set_inner_count(inner_count + 1),
stop_propagation=stop_propagatation,
),
"style": {
"height": "50px",
"width": "50px",
"background_color": "blue",
},
}
),
)
return html.div(
html.button(
{"on_click": lambda event: set_stop_propagatation(not stop_propagatation)},
"Toggle Propagation",
),
html.pre(f"Will propagate: {not stop_propagatation}"),
html.pre(f"Inner click count: {inner_count}"),
html.pre(f"Outer click count: {outer_count}"),
div_in_div,
)
run(DivInDiv)