Skip to content

Commit d483dc0

Browse files
committed
Support dot-notation callbacks on events
1 parent c6056fa commit d483dc0

File tree

4 files changed

+105
-2
lines changed

4 files changed

+105
-2
lines changed

src/reactpy/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from reactpy._html import html
33
from reactpy.core import hooks
44
from reactpy.core.component import component
5-
from reactpy.core.events import event
5+
from reactpy.core.events import Event, event
66
from reactpy.core.hooks import (
77
create_context,
88
use_async_effect,
@@ -27,6 +27,7 @@
2727
__version__ = "2.0.0b2"
2828

2929
__all__ = [
30+
"Event",
3031
"Layout",
3132
"Ref",
3233
"Vdom",

src/reactpy/core/events.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import contextlib
5+
import inspect
46
from collections.abc import Sequence
57
from typing import Any, Callable, Literal, overload
68

@@ -72,6 +74,18 @@ def setup(function: Callable[..., Any]) -> EventHandler:
7274
return setup(function) if function is not None else setup
7375

7476

77+
class Event(dict):
78+
def __getattr__(self, name: str) -> Any:
79+
value = self.get(name)
80+
return Event(value) if isinstance(value, dict) else value
81+
82+
def preventDefault(self) -> None:
83+
"""Prevent the default action of the event."""
84+
85+
def stopPropagation(self) -> None:
86+
"""Stop the event from propagating."""
87+
88+
7589
class EventHandler:
7690
"""Turn a function or coroutine into an event handler
7791
@@ -102,6 +116,19 @@ def __init__(
102116
target: str | None = None,
103117
) -> None:
104118
self.function = to_event_handler_function(function, positional_args=False)
119+
120+
if not (stop_propagation and prevent_default):
121+
with contextlib.suppress(Exception):
122+
func_to_inspect = function
123+
while hasattr(func_to_inspect, "__wrapped__"):
124+
func_to_inspect = func_to_inspect.__wrapped__
125+
126+
source = inspect.getsource(func_to_inspect)
127+
if not stop_propagation and ".stopPropagation()" in source:
128+
stop_propagation = True
129+
if not prevent_default and ".preventDefault()" in source:
130+
prevent_default = True
131+
105132
self.prevent_default = prevent_default
106133
self.stop_propagation = stop_propagation
107134
self.target = target
@@ -145,17 +172,21 @@ def to_event_handler_function(
145172
async def wrapper(data: Sequence[Any]) -> None:
146173
await function(*data)
147174

175+
wrapper.__wrapped__ = function
176+
148177
else:
149178

150179
async def wrapper(data: Sequence[Any]) -> None:
151180
function(*data)
152181

182+
wrapper.__wrapped__ = function
153183
return wrapper
154184
elif not asyncio.iscoroutinefunction(function):
155185

156186
async def wrapper(data: Sequence[Any]) -> None:
157187
function(data)
158188

189+
wrapper.__wrapped__ = function
159190
return wrapper
160191
else:
161192
return function

src/reactpy/core/layout.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
REACTPY_DEBUG,
3737
)
3838
from reactpy.core._life_cycle_hook import LifeCycleHook
39+
from reactpy.core.events import Event
3940
from reactpy.core.vdom import validate_vdom_json
4041
from reactpy.types import (
4142
ComponentType,
@@ -120,7 +121,8 @@ async def deliver(self, event: LayoutEventMessage | dict[str, Any]) -> None:
120121

121122
if handler is not None:
122123
try:
123-
await handler.function(event["data"])
124+
data = [Event(d) if isinstance(d, dict) else d for d in event["data"]]
125+
await handler.function(data)
124126
except Exception:
125127
logger.exception(f"Failed to execute event handler {handler}")
126128
else:

tests/test_core/test_events.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import pytest
22

33
import reactpy
4+
from reactpy import component, html
45
from reactpy.core.events import (
6+
Event,
57
EventHandler,
68
merge_event_handler_funcs,
79
merge_event_handlers,
810
to_event_handler_function,
911
)
12+
from reactpy.core.layout import Layout
1013
from reactpy.testing import DisplayFixture, poll
1114
from tests.tooling.common import DEFAULT_TYPE_DELAY
1215

@@ -315,3 +318,69 @@ def App():
315318
generated_divs = await parent.query_selector_all("div")
316319

317320
assert len(generated_divs) == 6
321+
322+
323+
def test_detect_prevent_default():
324+
def handler(event: Event):
325+
event.preventDefault()
326+
327+
eh = EventHandler(handler)
328+
assert eh.prevent_default is True
329+
330+
331+
def test_detect_stop_propagation():
332+
def handler(event: Event):
333+
event.stopPropagation()
334+
335+
eh = EventHandler(handler)
336+
assert eh.stop_propagation is True
337+
338+
339+
def test_detect_both():
340+
def handler(event: Event):
341+
event.preventDefault()
342+
event.stopPropagation()
343+
344+
eh = EventHandler(handler)
345+
assert eh.prevent_default is True
346+
assert eh.stop_propagation is True
347+
348+
349+
def test_no_detect():
350+
def handler(event: Event):
351+
pass
352+
353+
eh = EventHandler(handler)
354+
assert eh.prevent_default is False
355+
assert eh.stop_propagation is False
356+
357+
358+
def test_event_wrapper():
359+
data = {"a": 1, "b": {"c": 2}}
360+
event = Event(data)
361+
assert event.a == 1
362+
assert event.b.c == 2
363+
assert event["a"] == 1
364+
assert event["b"]["c"] == 2
365+
366+
367+
async def test_vdom_has_prevent_default():
368+
@component
369+
def MyComponent():
370+
def handler(event: Event):
371+
event.preventDefault()
372+
373+
return html.button({"onClick": handler})
374+
375+
async with Layout(MyComponent()) as layout:
376+
await layout.render()
377+
# Check layout._event_handlers
378+
# Find the handler
379+
handler = next(iter(layout._event_handlers.values()))
380+
assert handler.prevent_default is True
381+
382+
383+
def test_event_export():
384+
from reactpy import Event
385+
386+
assert Event is not None

0 commit comments

Comments
 (0)