Skip to content

Commit ec40c48

Browse files
authored
add event_action flags to rx.event decorator (#5574)
1 parent 29c2ef5 commit ec40c48

3 files changed

Lines changed: 226 additions & 8 deletions

File tree

reflex/event.py

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def stop_propagation(self) -> Self:
109109
"""
110110
return dataclasses.replace(
111111
self,
112-
event_actions={"stopPropagation": True, **self.event_actions},
112+
event_actions={**self.event_actions, "stopPropagation": True},
113113
)
114114

115115
@property
@@ -121,7 +121,7 @@ def prevent_default(self) -> Self:
121121
"""
122122
return dataclasses.replace(
123123
self,
124-
event_actions={"preventDefault": True, **self.event_actions},
124+
event_actions={**self.event_actions, "preventDefault": True},
125125
)
126126

127127
def throttle(self, limit_ms: int) -> Self:
@@ -135,7 +135,7 @@ def throttle(self, limit_ms: int) -> Self:
135135
"""
136136
return dataclasses.replace(
137137
self,
138-
event_actions={"throttle": limit_ms, **self.event_actions},
138+
event_actions={**self.event_actions, "throttle": limit_ms},
139139
)
140140

141141
def debounce(self, delay_ms: int) -> Self:
@@ -149,7 +149,7 @@ def debounce(self, delay_ms: int) -> Self:
149149
"""
150150
return dataclasses.replace(
151151
self,
152-
event_actions={"debounce": delay_ms, **self.event_actions},
152+
event_actions={**self.event_actions, "debounce": delay_ms},
153153
)
154154

155155
@property
@@ -161,7 +161,7 @@ def temporal(self) -> Self:
161161
"""
162162
return dataclasses.replace(
163163
self,
164-
event_actions={"temporal": True, **self.event_actions},
164+
event_actions={**self.event_actions, "temporal": True},
165165
)
166166

167167

@@ -2211,7 +2211,15 @@ class EventNamespace:
22112211

22122212
@overload
22132213
def __new__(
2214-
cls, func: None = None, *, background: bool | None = None
2214+
cls,
2215+
func: None = None,
2216+
*,
2217+
background: bool | None = None,
2218+
stop_propagation: bool | None = None,
2219+
prevent_default: bool | None = None,
2220+
throttle: int | None = None,
2221+
debounce: int | None = None,
2222+
temporal: bool | None = None,
22152223
) -> Callable[
22162224
[Callable[[BASE_STATE, Unpack[P]], Any]], EventCallback[Unpack[P]] # pyright: ignore [reportInvalidTypeVarUse]
22172225
]: ...
@@ -2222,13 +2230,23 @@ def __new__(
22222230
func: Callable[[BASE_STATE, Unpack[P]], Any],
22232231
*,
22242232
background: bool | None = None,
2233+
stop_propagation: bool | None = None,
2234+
prevent_default: bool | None = None,
2235+
throttle: int | None = None,
2236+
debounce: int | None = None,
2237+
temporal: bool | None = None,
22252238
) -> EventCallback[Unpack[P]]: ...
22262239

22272240
def __new__(
22282241
cls,
22292242
func: Callable[[BASE_STATE, Unpack[P]], Any] | None = None,
22302243
*,
22312244
background: bool | None = None,
2245+
stop_propagation: bool | None = None,
2246+
prevent_default: bool | None = None,
2247+
throttle: int | None = None,
2248+
debounce: int | None = None,
2249+
temporal: bool | None = None,
22322250
) -> (
22332251
EventCallback[Unpack[P]]
22342252
| Callable[[Callable[[BASE_STATE, Unpack[P]], Any]], EventCallback[Unpack[P]]]
@@ -2238,6 +2256,11 @@ def __new__(
22382256
Args:
22392257
func: The function to wrap.
22402258
background: Whether the event should be run in the background. Defaults to False.
2259+
stop_propagation: Whether to stop the event from bubbling up the DOM tree.
2260+
prevent_default: Whether to prevent the default behavior of the event.
2261+
throttle: Throttle the event handler to limit calls (in milliseconds).
2262+
debounce: Debounce the event handler to delay calls (in milliseconds).
2263+
temporal: Whether the event should be temporal.
22412264
22422265
Raises:
22432266
TypeError: If background is True and the function is not a coroutine or async generator. # noqa: DAR402
@@ -2246,6 +2269,30 @@ def __new__(
22462269
The wrapped function.
22472270
"""
22482271

2272+
def _build_event_actions():
2273+
"""Build event_actions dict from decorator parameters.
2274+
2275+
Returns:
2276+
Dict of event actions to apply, or empty dict if none specified.
2277+
"""
2278+
if not any(
2279+
[stop_propagation, prevent_default, throttle, debounce, temporal]
2280+
):
2281+
return {}
2282+
2283+
event_actions = {}
2284+
if stop_propagation is not None:
2285+
event_actions["stopPropagation"] = stop_propagation
2286+
if prevent_default is not None:
2287+
event_actions["preventDefault"] = prevent_default
2288+
if throttle is not None:
2289+
event_actions["throttle"] = throttle
2290+
if debounce is not None:
2291+
event_actions["debounce"] = debounce
2292+
if temporal is not None:
2293+
event_actions["temporal"] = temporal
2294+
return event_actions
2295+
22492296
def wrapper(
22502297
func: Callable[[BASE_STATE, Unpack[P]], T],
22512298
) -> EventCallback[Unpack[P]]:
@@ -2281,8 +2328,22 @@ def wrapper(
22812328
object.__setattr__(func, "__name__", name)
22822329
object.__setattr__(func, "__qualname__", name)
22832330
state_cls._add_event_handler(name, func)
2284-
return getattr(state_cls, name)
2331+
event_callback = getattr(state_cls, name)
2332+
2333+
# Apply decorator event actions
2334+
event_actions = _build_event_actions()
2335+
if event_actions:
2336+
# Create new EventCallback with updated event_actions
2337+
event_callback = dataclasses.replace(
2338+
event_callback, event_actions=event_actions
2339+
)
2340+
2341+
return event_callback
22852342

2343+
# Store decorator event actions on the function for later processing
2344+
event_actions = _build_event_actions()
2345+
if event_actions:
2346+
func._rx_event_actions = event_actions # pyright: ignore [reportFunctionMemberAccess]
22862347
return func # pyright: ignore [reportReturnType]
22872348

22882349
if func is not None:

reflex/state.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1100,7 +1100,12 @@ def _create_event_handler(cls, fn: Any):
11001100
Returns:
11011101
The event handler.
11021102
"""
1103-
return EventHandler(fn=fn, state_full_name=cls.get_full_name())
1103+
# Check if function has stored event_actions from decorator
1104+
event_actions = getattr(fn, "_rx_event_actions", {})
1105+
1106+
return EventHandler(
1107+
fn=fn, state_full_name=cls.get_full_name(), event_actions=event_actions
1108+
)
11041109

11051110
@classmethod
11061111
def _create_setvar(cls):

tests/units/test_event.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import reflex as rx
66
from reflex.constants.compiler import Hooks, Imports
77
from reflex.event import (
8+
BACKGROUND_TASK_MARKER,
89
Event,
910
EventChain,
1011
EventHandler,
@@ -493,6 +494,157 @@ def get_handler(self, arg: Var[str]):
493494
_ = rx.input(on_change=w.get_handler)
494495

495496

497+
def test_event_decorator_with_event_actions():
498+
"""Test that @rx.event decorator can accept event action parameters."""
499+
500+
class MyTestState(BaseState):
501+
# Test individual event actions
502+
@event(stop_propagation=True)
503+
def handle_stop_prop(self):
504+
pass
505+
506+
@event(prevent_default=True)
507+
def handle_prevent_default(self):
508+
pass
509+
510+
@event(throttle=500)
511+
def handle_throttle(self):
512+
pass
513+
514+
@event(debounce=300)
515+
def handle_debounce(self):
516+
pass
517+
518+
@event(temporal=True)
519+
def handle_temporal(self):
520+
pass
521+
522+
# Test multiple event actions combined
523+
@event(stop_propagation=True, prevent_default=True, throttle=1000)
524+
def handle_multiple(self):
525+
pass
526+
527+
# Test with background parameter (existing functionality)
528+
@event(background=True, temporal=True)
529+
async def handle_background_temporal(self):
530+
pass
531+
532+
# Test no event actions (existing behavior)
533+
@event
534+
def handle_no_actions(self):
535+
pass
536+
537+
# Test individual event actions are applied
538+
stop_prop_handler = MyTestState.handle_stop_prop
539+
assert isinstance(stop_prop_handler, EventHandler)
540+
assert stop_prop_handler.event_actions == {"stopPropagation": True}
541+
542+
prevent_default_handler = MyTestState.handle_prevent_default
543+
assert prevent_default_handler.event_actions == {"preventDefault": True}
544+
545+
throttle_handler = MyTestState.handle_throttle
546+
assert throttle_handler.event_actions == {"throttle": 500}
547+
548+
debounce_handler = MyTestState.handle_debounce
549+
assert debounce_handler.event_actions == {"debounce": 300}
550+
551+
temporal_handler = MyTestState.handle_temporal
552+
assert temporal_handler.event_actions == {"temporal": True}
553+
554+
# Test multiple event actions are combined correctly
555+
multiple_handler = MyTestState.handle_multiple
556+
assert multiple_handler.event_actions == {
557+
"stopPropagation": True,
558+
"preventDefault": True,
559+
"throttle": 1000,
560+
}
561+
562+
# Test background + event actions work together
563+
bg_temporal_handler = MyTestState.handle_background_temporal
564+
assert bg_temporal_handler.event_actions == {"temporal": True}
565+
assert hasattr(bg_temporal_handler.fn, BACKGROUND_TASK_MARKER) # pyright: ignore [reportAttributeAccessIssue]
566+
567+
# Test no event actions (existing behavior preserved)
568+
no_actions_handler = MyTestState.handle_no_actions
569+
assert no_actions_handler.event_actions == {}
570+
571+
572+
def test_event_decorator_actions_can_be_overridden():
573+
"""Test that decorator event actions can still be overridden by chaining."""
574+
575+
class MyTestState(BaseState):
576+
@event(throttle=500, stop_propagation=True)
577+
def handle_with_defaults(self):
578+
pass
579+
580+
# Get the handler with default actions
581+
handler = MyTestState.handle_with_defaults
582+
assert handler.event_actions == {"throttle": 500, "stopPropagation": True}
583+
584+
# Chain additional actions - should combine
585+
handler_with_prevent_default = handler.prevent_default
586+
assert handler_with_prevent_default.event_actions == {
587+
"throttle": 500,
588+
"stopPropagation": True,
589+
"preventDefault": True,
590+
}
591+
592+
# Chain throttle with different value - should override
593+
handler_with_new_throttle = handler.throttle(1000)
594+
assert handler_with_new_throttle.event_actions == {
595+
"throttle": 1000, # New value overrides default
596+
"stopPropagation": True,
597+
}
598+
599+
# Original handler should be unchanged
600+
assert handler.event_actions == {"throttle": 500, "stopPropagation": True}
601+
602+
603+
def test_event_decorator_with_none_values():
604+
"""Test that None values in decorator don't create event actions."""
605+
606+
class MyTestState(BaseState):
607+
@event(stop_propagation=None, prevent_default=None, throttle=None)
608+
def handle_all_none(self):
609+
pass
610+
611+
@event(stop_propagation=True, prevent_default=None, throttle=500, debounce=None)
612+
def handle_mixed(self):
613+
pass
614+
615+
# All None should result in no event actions
616+
all_none_handler = MyTestState.handle_all_none
617+
assert all_none_handler.event_actions == {}
618+
619+
# Only non-None values should be included
620+
mixed_handler = MyTestState.handle_mixed
621+
assert mixed_handler.event_actions == {"stopPropagation": True, "throttle": 500}
622+
623+
624+
def test_event_decorator_backward_compatibility():
625+
"""Test that existing code without event action parameters continues to work."""
626+
627+
class MyTestState(BaseState):
628+
@event
629+
def handle_old_style(self):
630+
pass
631+
632+
@event(background=True)
633+
async def handle_old_background(self):
634+
pass
635+
636+
# Old style without parameters should work unchanged
637+
old_handler = MyTestState.handle_old_style
638+
assert isinstance(old_handler, EventHandler)
639+
assert old_handler.event_actions == {}
640+
assert not hasattr(old_handler.fn, BACKGROUND_TASK_MARKER) # pyright: ignore [reportAttributeAccessIssue]
641+
642+
# Old background parameter should work unchanged
643+
bg_handler = MyTestState.handle_old_background
644+
assert bg_handler.event_actions == {}
645+
assert hasattr(bg_handler.fn, BACKGROUND_TASK_MARKER) # pyright: ignore [reportAttributeAccessIssue]
646+
647+
496648
def test_event_var_in_rx_cond():
497649
"""Test that EventVar and EventChainVar cannot be used in rx.cond()."""
498650
from reflex.components.core.cond import cond as rx_cond

0 commit comments

Comments
 (0)