From bf20a530dfda1e0d91ddfb33ff86e7fac7746a0b Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 2 Nov 2023 10:21:41 -0700 Subject: [PATCH] Do not stop prop is there is no prop to stop (#2116) Check that desired event actions are defined on the object passed as the DOM event before calling them to avoid frontend errors. --- integration/test_event_actions.py | 50 +++++++++++++++++++++++++--- reflex/.templates/web/utils/state.js | 4 +-- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/integration/test_event_actions.py b/integration/test_event_actions.py index d446b6663..8f5e0788b 100644 --- a/integration/test_event_actions.py +++ b/integration/test_event_actions.py @@ -1,5 +1,6 @@ """Ensure stopPropagation and preventDefault work as expected.""" +import asyncio from typing import Callable, Coroutine, Generator import pytest @@ -21,13 +22,31 @@ def TestEventAction(): def on_click2(self): self.order.append("on_click2") - @rx.var - def token(self) -> str: - return self.get_token() + class EventFiringComponent(rx.Component): + """A component that fires onClick event without passing DOM event.""" + + tag = "EventFiringComponent" + + def _get_custom_code(self) -> str | None: + return """ + function EventFiringComponent(props) { + return ( +
props.onClick("foo")}> + Event Firing Component +
+ ) + }""" + + def get_event_triggers(self): + return {"on_click": lambda: []} def index(): return rx.vstack( - rx.input(value=EventActionState.token, is_read_only=True, id="token"), + rx.input( + value=EventActionState.router.session.client_token, + is_read_only=True, + id="token", + ), rx.button("No events", id="btn-no-events"), rx.button( "Stop Prop Only", @@ -90,6 +109,18 @@ def TestEventAction(): ).stop_propagation.prevent_default, id="link-stop-propagation-prevent-default", ), + EventFiringComponent.create( + id="custom-stop-propagation", + on_click=EventActionState.on_click( # type: ignore + "custom-stop-propagation" + ).stop_propagation, + ), + EventFiringComponent.create( + id="custom-prevent-default", + on_click=EventActionState.on_click( # type: ignore + "custom-prevent-default" + ).prevent_default, + ), rx.list( rx.foreach( EventActionState.order, # type: ignore @@ -202,6 +233,14 @@ def poll_for_order( ("link-prevent-default", ["on_click:link_prevent_default", "on_click:outer"]), ("link-prevent-default-only", ["on_click:outer"]), ("link-stop-propagation-prevent-default", ["on_click:link_both"]), + ( + "custom-stop-propagation", + ["on_click:custom-stop-propagation", "on_click:outer"], + ), + ( + "custom-prevent-default", + ["on_click:custom-prevent-default", "on_click:outer"], + ), ], ) @pytest.mark.usefixtures("token") @@ -226,6 +265,9 @@ async def test_event_actions( prev_url = driver.current_url el.click() + if "on_click:outer" not in exp_order: + # really make sure the outer event is not fired + await asyncio.sleep(0.5) await poll_for_order(exp_order) if element_id.startswith("link") and "prevent-default" not in element_id: diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index fb0208a86..673e317d3 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -488,10 +488,10 @@ export const useEventLoop = ( // Function to add new events to the event queue. const addEvents = (events, _e, event_actions) => { - if (event_actions?.preventDefault && _e) { + if (event_actions?.preventDefault && _e?.preventDefault) { _e.preventDefault(); } - if (event_actions?.stopPropagation && _e) { + if (event_actions?.stopPropagation && _e?.stopPropagation) { _e.stopPropagation(); } queueEvents(events, socket)