From 211dc15995accb1b31d58a276a65119453cedc49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Thu, 21 Sep 2023 18:47:22 +0200 Subject: [PATCH] New API to define triggers (#1820) --- .../jinja/web/pages/index.js.jinja2 | 4 +- .../jinja/web/utils/context.js.jinja2 | 4 +- reflex/.templates/web/pages/_app.js | 4 +- reflex/.templates/web/utils/state.js | 12 +- reflex/compiler/compiler.py | 2 +- reflex/components/__init__.py | 1 - reflex/components/base/script.py | 11 +- reflex/components/component.py | 120 ++++++++++++------ reflex/components/forms/__init__.py | 1 - reflex/components/forms/checkbox.py | 10 +- reflex/components/forms/copytoclipboard.py | 25 ---- reflex/components/forms/copytoclipboard.pyi | 28 ---- reflex/components/forms/editable.py | 16 ++- reflex/components/forms/form.py | 10 +- reflex/components/forms/form.pyi | 2 +- reflex/components/forms/input.py | 18 +-- reflex/components/forms/input.pyi | 2 +- reflex/components/forms/multiselect.py | 20 +-- reflex/components/forms/numberinput.py | 9 +- reflex/components/forms/numberinput.pyi | 4 +- reflex/components/forms/pininput.py | 12 +- reflex/components/forms/radio.py | 13 +- reflex/components/forms/rangeslider.py | 14 +- reflex/components/forms/select.py | 15 ++- reflex/components/forms/slider.py | 16 ++- reflex/components/forms/switch.py | 11 +- reflex/components/forms/textarea.py | 19 +-- reflex/components/forms/upload.py | 10 +- reflex/components/graphing/plotly.pyi | 2 +- reflex/components/media/avatar.py | 10 +- reflex/components/media/avatar.pyi | 2 +- reflex/components/media/image.py | 10 +- reflex/components/overlay/alertdialog.py | 16 ++- reflex/components/overlay/alertdialog.pyi | 2 +- reflex/components/overlay/drawer.py | 16 ++- reflex/components/overlay/drawer.pyi | 2 +- reflex/components/overlay/menu.py | 11 +- reflex/components/overlay/menu.pyi | 2 +- reflex/components/overlay/modal.py | 18 +-- reflex/components/overlay/modal.pyi | 2 +- reflex/components/overlay/popover.py | 13 +- reflex/components/overlay/popover.pyi | 2 +- reflex/components/overlay/tooltip.py | 11 +- reflex/components/overlay/tooltip.pyi | 2 +- reflex/constants.py | 33 ++++- reflex/event.py | 100 ++++++++++----- reflex/utils/format.py | 21 ++- reflex/utils/types.py | 3 + scripts/pyi_generator.py | 2 +- tests/components/base/test_script.py | 11 +- tests/components/test_component.py | 70 +++++++++- tests/test_event.py | 44 ++++--- tests/utils/test_utils.py | 11 +- 53 files changed, 513 insertions(+), 316 deletions(-) delete mode 100644 reflex/components/forms/copytoclipboard.py delete mode 100644 reflex/components/forms/copytoclipboard.pyi diff --git a/reflex/.templates/jinja/web/pages/index.js.jinja2 b/reflex/.templates/jinja/web/pages/index.js.jinja2 index 29de4cecd..92b4abd7f 100644 --- a/reflex/.templates/jinja/web/pages/index.js.jinja2 +++ b/reflex/.templates/jinja/web/pages/index.js.jinja2 @@ -14,7 +14,7 @@ export default function Component() { const focusRef = useRef(); // Main event loop. - const [Event, connectError] = useContext(EventLoopContext) + const [addEvents, connectError] = useContext(EventLoopContext) // Set focus to the specified element. useEffect(() => { @@ -25,7 +25,7 @@ export default function Component() { // Route after the initial page hydration. useEffect(() => { - const change_complete = () => Event(initialEvents.map((e) => ({...e}))) + const change_complete = () => addEvents(initialEvents.map((e) => ({...e}))) {{const.router}}.events.on('routeChangeComplete', change_complete) return () => { {{const.router}}.events.off('routeChangeComplete', change_complete) diff --git a/reflex/.templates/jinja/web/utils/context.js.jinja2 b/reflex/.templates/jinja/web/utils/context.js.jinja2 index 657c64e27..db34c41ae 100644 --- a/reflex/.templates/jinja/web/utils/context.js.jinja2 +++ b/reflex/.templates/jinja/web/utils/context.js.jinja2 @@ -1,10 +1,10 @@ import { createContext } from "react" -import { E, hydrateClientStorage } from "/utils/state.js" +import { Event, hydrateClientStorage } from "/utils/state.js" export const initialState = {{ initial_state|json_dumps }} export const StateContext = createContext(null); export const EventLoopContext = createContext(null); export const clientStorage = {{ client_storage|json_dumps }} export const initialEvents = [ - E('{{state_name}}.{{const.hydrate}}', hydrateClientStorage(clientStorage)), + Event('{{state_name}}.{{const.hydrate}}', hydrateClientStorage(clientStorage)), ] \ No newline at end of file diff --git a/reflex/.templates/web/pages/_app.js b/reflex/.templates/web/pages/_app.js index 1f4873a57..ea7a0e0d3 100644 --- a/reflex/.templates/web/pages/_app.js +++ b/reflex/.templates/web/pages/_app.js @@ -15,13 +15,13 @@ const GlobalStyles = css` `; function EventLoopProvider({ children }) { - const [state, Event, connectError] = useEventLoop( + const [state, addEvents, connectError] = useEventLoop( initialState, initialEvents, clientStorage, ) return ( - + {children} diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 116a507a2..8c3e2ac65 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -364,7 +364,7 @@ export const uploadFiles = async (handler, files) => { * @param handler The client handler to process event. * @returns The event object. */ -export const E = (name, payload = {}, handler = null) => { +export const Event = (name, payload = {}, handler = null) => { return { name, payload, handler }; }; @@ -440,9 +440,9 @@ const applyClientStorageDelta = (client_storage, delta) => { * @param initial_events The initial app events. * @param client_storage The client storage object from context.js * - * @returns [state, Event, connectError] - + * @returns [state, addEvents, connectError] - * state is a reactive dict, - * Event is used to queue an event, and + * addEvents is used to queue an event, and * connectError is a reactive js error from the websocket connection (or null if connected). */ export const useEventLoop = ( @@ -456,7 +456,7 @@ export const useEventLoop = ( const [connectError, setConnectError] = useState(null) // Function to add new events to the event queue. - const Event = (events, _e) => { + const addEvents = (events, _e) => { preventDefault(_e); queueEvents(events, socket) } @@ -465,7 +465,7 @@ export const useEventLoop = ( // initial state hydrate useEffect(() => { if (router.isReady && !sentHydrate.current) { - Event(initial_events.map((e) => ({ ...e }))) + addEvents(initial_events.map((e) => ({ ...e }))) sentHydrate.current = true } }, [router.isReady]) @@ -488,7 +488,7 @@ export const useEventLoop = ( } })() }) - return [state, Event, connectError] + return [state, addEvents, connectError] } /*** diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 862a92fd5..1799f9883 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -25,7 +25,7 @@ DEFAULT_IMPORTS: imports.ImportDict = { "next/router": {ImportVar(tag="useRouter")}, f"/{constants.STATE_PATH}": { ImportVar(tag="uploadFiles"), - ImportVar(tag="E"), + ImportVar(tag="Event"), ImportVar(tag="isTrue"), ImportVar(tag="spreadArraysOrObjects"), ImportVar(tag="preventDefault"), diff --git a/reflex/components/__init__.py b/reflex/components/__init__.py index 80e2bc662..075700d69 100644 --- a/reflex/components/__init__.py +++ b/reflex/components/__init__.py @@ -93,7 +93,6 @@ button = Button.create button_group = ButtonGroup.create checkbox = Checkbox.create checkbox_group = CheckboxGroup.create -copy_to_clipboard = CopyToClipboard.create date_picker = DatePicker.create date_time_picker = DateTimePicker.create debounce_input = DebounceInput.create diff --git a/reflex/components/base/script.py b/reflex/components/base/script.py index 6b62d552a..9f693790d 100644 --- a/reflex/components/base/script.py +++ b/reflex/components/base/script.py @@ -4,6 +4,8 @@ https://nextjs.org/docs/app/api-reference/components/script """ from __future__ import annotations +from typing import Any, Union + from reflex.components.component import Component from reflex.event import EventChain from reflex.vars import BaseVar, Var @@ -57,13 +59,18 @@ class Script(Component): raise ValueError("Must provide inline script or `src` prop.") return super().create(*children, **props) - def get_triggers(self) -> set[str]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers for the component. Returns: The event triggers. """ - return super().get_triggers() | {"on_load", "on_ready", "on_error"} + return { + **super().get_event_triggers(), + "on_load": lambda: [], + "on_ready": lambda: [], + "on_error": lambda: [], + } def client_side(javascript_code) -> Var[EventChain]: diff --git a/reflex/components/component.py b/reflex/components/component.py index 0731cb34d..d6e3fea62 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -10,9 +10,8 @@ from typing import Any, Callable, Dict, List, Optional, Set, Type, Union from reflex import constants from reflex.base import Base from reflex.components.tags import Tag +from reflex.constants import EventTriggers from reflex.event import ( - EVENT_ARG, - EVENT_TRIGGERS, EventChain, EventHandler, EventSpec, @@ -21,7 +20,7 @@ from reflex.event import ( get_handler_args, ) from reflex.style import Style -from reflex.utils import format, imports, types +from reflex.utils import console, format, imports, types from reflex.vars import BaseVar, ImportVar, Var @@ -126,7 +125,7 @@ class Component(Base, ABC): # Get the component fields, triggers, and props. fields = self.get_fields() - triggers = self.get_triggers() + triggers = self.get_event_triggers().keys() props = self.get_props() # Add any events triggers. @@ -220,8 +219,7 @@ class Component(Base, ABC): ValueError: If the value is not a valid event chain. """ # Check if the trigger is a controlled event. - controlled_triggers = self.get_controlled_triggers() - is_controlled_event = event_trigger in controlled_triggers + triggers = self.get_event_triggers() # If it's an event chain var, return it. if isinstance(value, Var): @@ -229,27 +227,28 @@ class Component(Base, ABC): raise ValueError(f"Invalid event chain: {value}") return value - arg = controlled_triggers.get(event_trigger, EVENT_ARG) + arg_spec = triggers.get(event_trigger, lambda: []) + wrapped = False # If the input is a single event handler, wrap it in a list. if isinstance(value, (EventHandler, EventSpec)): + wrapped = True value = [value] # If the input is a list of event handlers, create an event chain. if isinstance(value, List): + if not wrapped: + console.deprecate( + feature_name="EventChain", + reason="to avoid confusion, only use yield API", + deprecation_version="0.2.8", + removal_version="0.2.9", + ) events = [] for v in value: if isinstance(v, EventHandler): # Call the event handler to get the event. - event = call_event_handler(v, arg) - - # Check that the event handler takes no args if it's uncontrolled. - if not is_controlled_event and ( - event.args is not None and len(event.args) > 0 - ): - raise ValueError( - f"Event handler: {v.fn} for uncontrolled event {event_trigger} should not take any args." - ) + event = call_event_handler(v, arg_spec) # type: ignore # Add the event to the chain. events.append(event) @@ -258,45 +257,93 @@ class Component(Base, ABC): events.append(v) elif isinstance(v, Callable): # Call the lambda to get the event chain. - events.extend(call_event_fn(v, arg)) + events.extend(call_event_fn(v, arg_spec)) # type: ignore else: raise ValueError(f"Invalid event: {v}") # If the input is a callable, create an event chain. elif isinstance(value, Callable): - events = call_event_fn(value, arg) + events = call_event_fn(value, arg_spec) # type: ignore # Otherwise, raise an error. else: raise ValueError(f"Invalid event chain: {value}") # Add args to the event specs if necessary. - if is_controlled_event: - events = [ - EventSpec( - handler=e.handler, - args=get_handler_args(e, arg), - ) - for e in events - ] + events = [ + EventSpec( + handler=e.handler, + args=get_handler_args(e), + client_handler_name=e.client_handler_name, + ) + for e in events + ] # Return the event chain. - return EventChain(events=events) + if isinstance(arg_spec, Var): + return EventChain(events=events, args_spec=None) + else: + return EventChain(events=events, args_spec=arg_spec) # type: ignore - def get_triggers(self) -> Set[str]: + def get_event_triggers(self) -> Dict[str, Any]: """Get the event triggers for the component. Returns: The event triggers. """ - return ( - EVENT_TRIGGERS - | set(self.get_controlled_triggers()) - | set((constants.ON_MOUNT, constants.ON_UNMOUNT)) - ) + deprecated_triggers = self.get_triggers() + if deprecated_triggers: + console.deprecate( + feature_name=f"get_triggers ({self.__class__.__name__})", + reason="replaced by get_event_triggers", + deprecation_version="0.2.8", + removal_version="0.2.9", + ) + deprecated_triggers = { + trigger: lambda: [] for trigger in deprecated_triggers + } + else: + deprecated_triggers = {} + + deprecated_controlled_triggers = self.get_controlled_triggers() + if deprecated_controlled_triggers: + console.deprecate( + feature_name=f"get_controlled_triggers ({self.__class__.__name__})", + reason="replaced by get_event_triggers", + deprecation_version="0.2.8", + removal_version="0.2.9", + ) + + return { + EventTriggers.ON_FOCUS: lambda: [], + EventTriggers.ON_BLUR: lambda: [], + EventTriggers.ON_CLICK: lambda: [], + EventTriggers.ON_CONTEXT_MENU: lambda: [], + EventTriggers.ON_DOUBLE_CLICK: lambda: [], + EventTriggers.ON_MOUSE_DOWN: lambda: [], + EventTriggers.ON_MOUSE_ENTER: lambda: [], + EventTriggers.ON_MOUSE_LEAVE: lambda: [], + EventTriggers.ON_MOUSE_MOVE: lambda: [], + EventTriggers.ON_MOUSE_OUT: lambda: [], + EventTriggers.ON_MOUSE_OVER: lambda: [], + EventTriggers.ON_MOUSE_UP: lambda: [], + EventTriggers.ON_SCROLL: lambda: [], + EventTriggers.ON_MOUNT: lambda: [], + EventTriggers.ON_UNMOUNT: lambda: [], + **deprecated_triggers, + **deprecated_controlled_triggers, + } + + def get_triggers(self) -> Set[str]: + """Get the triggers for non controlled events [DEPRECATED]. + + Returns: + A set of non controlled triggers. + """ + return set() def get_controlled_triggers(self) -> Dict[str, Var]: - """Get the event triggers that pass the component's value to the handler. + """Get the event triggers that pass the component's value to the handler [DEPRECATED]. Returns: A dict mapping the event trigger to the var that is passed to the handler. @@ -436,7 +483,6 @@ class Component(Base, ABC): The dictionary for template of component. """ tag = self._render() - rendered_dict = dict( tag.add_props( **self.event_triggers, @@ -577,8 +623,8 @@ class Component(Base, ABC): """ # pop on_mount and on_unmount from event_triggers since these are handled by # hooks, not as actually props in the component - on_mount = self.event_triggers.pop(constants.ON_MOUNT, None) - on_unmount = self.event_triggers.pop(constants.ON_UNMOUNT, None) + on_mount = self.event_triggers.pop(EventTriggers.ON_MOUNT, None) + on_unmount = self.event_triggers.pop(EventTriggers.ON_UNMOUNT, None) if on_mount: on_mount = format.format_event_chain(on_mount) if on_unmount: diff --git a/reflex/components/forms/__init__.py b/reflex/components/forms/__init__.py index 40640c1c8..198231992 100644 --- a/reflex/components/forms/__init__.py +++ b/reflex/components/forms/__init__.py @@ -8,7 +8,6 @@ from .colormodeswitch import ( ColorModeSwitch, color_mode_cond, ) -from .copytoclipboard import CopyToClipboard from .date_picker import DatePicker from .date_time_picker import DateTimePicker from .debounce import DebounceInput diff --git a/reflex/components/forms/checkbox.py b/reflex/components/forms/checkbox.py index eba369e52..5ded5eeba 100644 --- a/reflex/components/forms/checkbox.py +++ b/reflex/components/forms/checkbox.py @@ -1,9 +1,10 @@ """A checkbox component.""" +from __future__ import annotations -from typing import Dict +from typing import Any, Union -from reflex.components.component import EVENT_ARG from reflex.components.libs.chakra import ChakraComponent +from reflex.constants import EventTriggers from reflex.vars import Var @@ -48,14 +49,15 @@ class Checkbox(ChakraComponent): # The spacing between the checkbox and its label text (0.5rem) spacing: Var[str] - def get_controlled_triggers(self) -> Dict[str, Var]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers that pass the component's value to the handler. Returns: A dict mapping the event trigger to the var that is passed to the handler. """ return { - "on_change": EVENT_ARG.target.checked, + **super().get_event_triggers(), + EventTriggers.ON_CHANGE: lambda e0: [e0.target.checked], } diff --git a/reflex/components/forms/copytoclipboard.py b/reflex/components/forms/copytoclipboard.py deleted file mode 100644 index 2a2c62189..000000000 --- a/reflex/components/forms/copytoclipboard.py +++ /dev/null @@ -1,25 +0,0 @@ -"""A copy to clipboard component.""" - -from typing import Set - -from reflex.components import Component -from reflex.vars import Var - - -class CopyToClipboard(Component): - """Component to copy text to clipboard.""" - - library = "react-copy-to-clipboard" - - tag = "CopyToClipboard" - - # The text to copy when clicked. - text: Var[str] - - def get_controlled_triggers(self) -> Set[str]: - """Get the event triggers that pass the component's value to the handler. - - Returns: - The controlled event triggers. - """ - return {"on_copy"} diff --git a/reflex/components/forms/copytoclipboard.pyi b/reflex/components/forms/copytoclipboard.pyi deleted file mode 100644 index 64b9c7ed2..000000000 --- a/reflex/components/forms/copytoclipboard.pyi +++ /dev/null @@ -1,28 +0,0 @@ -"""Stub file for copytoclipboard.py""" -# ------------------- DO NOT EDIT ---------------------- -# This file was generated by `scripts/pyi_generator.py`! -# ------------------------------------------------------ - -from typing import Optional, Set, Union, overload -from reflex.components.component import Component -from reflex.vars import Var, BaseVar, ComputedVar -from reflex.event import EventHandler, EventChain, EventSpec - -class CopyToClipboard(Component): - @overload - @classmethod - def create(cls, *children, text: Optional[Union[Var[str], str]] = None, on_blur: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_context_menu: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_copy: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_double_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_focus: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_down: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_enter: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_leave: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_move: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_out: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_over: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_up: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_scroll: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_unmount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, **props) -> "CopyToClipboard": # type: ignore - """Create the component. - - Args: - *children: The children of the component. - text: The text to copy when clicked. - **props: The props of the component. - - Returns: - The component. - - Raises: - TypeError: If an invalid child is passed. - """ - ... diff --git a/reflex/components/forms/editable.py b/reflex/components/forms/editable.py index cf004d2ff..e963be2ad 100644 --- a/reflex/components/forms/editable.py +++ b/reflex/components/forms/editable.py @@ -1,9 +1,10 @@ """An editable component.""" +from __future__ import annotations -from typing import Dict +from typing import Any, Union from reflex.components.libs.chakra import ChakraComponent -from reflex.event import EVENT_ARG +from reflex.constants import EventTriggers from reflex.vars import Var @@ -36,17 +37,18 @@ class Editable(ChakraComponent): # The initial value of the Editable in both edit and preview mode. default_value: Var[str] - def get_controlled_triggers(self) -> Dict[str, Var]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers that pass the component's value to the handler. Returns: A dict mapping the event trigger to the var that is passed to the handler. """ return { - "on_change": EVENT_ARG, - "on_edit": EVENT_ARG, - "on_submit": EVENT_ARG, - "on_cancel": EVENT_ARG, + **super().get_event_triggers(), + EventTriggers.ON_CHANGE: lambda e0: [e0], + EventTriggers.ON_EDIT: lambda e0: [e0], + EventTriggers.ON_SUBMIT: lambda e0: [e0], + EventTriggers.ON_CANCEL: lambda e0: [e0], } diff --git a/reflex/components/forms/form.py b/reflex/components/forms/form.py index 6de09850d..2f5dc57f7 100644 --- a/reflex/components/forms/form.py +++ b/reflex/components/forms/form.py @@ -1,9 +1,10 @@ """Form components.""" -from typing import Dict +from typing import Any, Dict from reflex.components.component import Component from reflex.components.libs.chakra import ChakraComponent +from reflex.constants import EventTriggers from reflex.vars import Var @@ -15,7 +16,7 @@ class Form(ChakraComponent): # What the form renders to. as_: Var[str] = "form" # type: ignore - def get_controlled_triggers(self) -> Dict[str, Dict]: + def get_event_triggers(self) -> Dict[str, Any]: """Get the event triggers that pass the component's value to the handler. Returns: @@ -33,7 +34,10 @@ class Form(ChakraComponent): else: form_refs[ref[4:]] = Var.create(f"getRefValue({ref})", is_local=False) - return {"on_submit": form_refs} + return { + **super().get_event_triggers(), + EventTriggers.ON_SUBMIT: lambda e0: [form_refs], + } class FormControl(ChakraComponent): diff --git a/reflex/components/forms/form.pyi b/reflex/components/forms/form.pyi index c2d75606b..dc6a89ec1 100644 --- a/reflex/components/forms/form.pyi +++ b/reflex/components/forms/form.pyi @@ -3,7 +3,7 @@ # This file was generated by `scripts/pyi_generator.py`! # ------------------------------------------------------ -from typing import Dict, Optional, Union, overload +from typing import Any, Dict, Optional, Union, overload from reflex.components.libs.chakra import ChakraComponent from reflex.components.component import Component from reflex.vars import Var, BaseVar, ComputedVar diff --git a/reflex/components/forms/input.py b/reflex/components/forms/input.py index 6c9c8f421..c21d6d5cb 100644 --- a/reflex/components/forms/input.py +++ b/reflex/components/forms/input.py @@ -1,10 +1,11 @@ """An input component.""" -from typing import Dict +from typing import Any, Dict -from reflex.components.component import EVENT_ARG, Component +from reflex.components.component import Component from reflex.components.forms.debounce import DebounceInput from reflex.components.libs.chakra import ChakraComponent +from reflex.constants import EventTriggers from reflex.utils import imports from reflex.vars import ImportVar, Var @@ -56,18 +57,19 @@ class Input(ChakraComponent): {"/utils/state": {ImportVar(tag="set_val")}}, ) - def get_controlled_triggers(self) -> Dict[str, Var]: + def get_event_triggers(self) -> Dict[str, Any]: """Get the event triggers that pass the component's value to the handler. Returns: A dict mapping the event trigger to the var that is passed to the handler. """ return { - "on_change": EVENT_ARG.target.value, - "on_focus": EVENT_ARG.target.value, - "on_blur": EVENT_ARG.target.value, - "on_key_down": EVENT_ARG.key, - "on_key_up": EVENT_ARG.key, + **super().get_event_triggers(), + EventTriggers.ON_CHANGE: lambda e0: [e0.target.value], + EventTriggers.ON_FOCUS: lambda e0: [e0.target.value], + EventTriggers.ON_BLUR: lambda e0: [e0.target.value], + EventTriggers.ON_KEY_DOWN: lambda e0: [e0.key], + EventTriggers.ON_KEY_UP: lambda e0: [e0.key], } @classmethod diff --git a/reflex/components/forms/input.pyi b/reflex/components/forms/input.pyi index d1d4d656b..0d620dada 100644 --- a/reflex/components/forms/input.pyi +++ b/reflex/components/forms/input.pyi @@ -3,7 +3,7 @@ # This file was generated by `scripts/pyi_generator.py`! # ------------------------------------------------------ -from typing import Dict, Optional, Union, overload +from typing import Any, Dict, Optional, Union, overload from reflex.components.libs.chakra import ChakraComponent from reflex.components.component import Component from reflex.vars import Var, BaseVar, ComputedVar diff --git a/reflex/components/forms/multiselect.py b/reflex/components/forms/multiselect.py index 510056c4a..9625e1598 100644 --- a/reflex/components/forms/multiselect.py +++ b/reflex/components/forms/multiselect.py @@ -1,10 +1,11 @@ """Provides a feature-rich Select and some (not all) related components.""" +from __future__ import annotations from typing import Any, Dict, List, Optional, Set, Union from reflex.base import Base from reflex.components.component import Component -from reflex.event import EVENT_ARG +from reflex.constants import EventTriggers from reflex.vars import Var @@ -298,19 +299,20 @@ class Select(Component): # How the options should be displayed in the menu. menu_position: Var[str] = "fixed" # type: ignore - def get_controlled_triggers(self) -> Dict[str, Var]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers that pass the component's value to the handler. Returns: A dict mapping the event trigger to the var that is passed to the handler. """ - # A normal select returns the value. - value = EVENT_ARG.value - - # Multi-select returns a list of values. - if self.is_multi: - value = Var.create_safe(f"{EVENT_ARG}.map(e => e.value)", is_local=True) - return {"on_change": value} + return { + **super().get_event_triggers(), + EventTriggers.ON_CHANGE: ( + lambda e0: [Var.create_safe(f"{e0}.map(e => e.value)", is_local=True)] + if self.is_multi + else lambda e0: [e0] + ), + } @classmethod def get_initial_props(cls) -> Set[str]: diff --git a/reflex/components/forms/numberinput.py b/reflex/components/forms/numberinput.py index c5a15ff72..79f74d562 100644 --- a/reflex/components/forms/numberinput.py +++ b/reflex/components/forms/numberinput.py @@ -1,11 +1,11 @@ """A number input component.""" from numbers import Number -from typing import Dict +from typing import Any, Dict from reflex.components.component import Component from reflex.components.libs.chakra import ChakraComponent -from reflex.event import EVENT_ARG +from reflex.constants import EventTriggers from reflex.vars import Var @@ -65,14 +65,15 @@ class NumberInput(ChakraComponent): # "outline" | "filled" | "flushed" | "unstyled" variant: Var[str] - def get_controlled_triggers(self) -> Dict[str, Var]: + def get_event_triggers(self) -> Dict[str, Any]: """Get the event triggers that pass the component's value to the handler. Returns: A dict mapping the event trigger to the var that is passed to the handler. """ return { - "on_change": EVENT_ARG, + **super().get_event_triggers(), + EventTriggers.ON_CHANGE: lambda e0: [e0], } @classmethod diff --git a/reflex/components/forms/numberinput.pyi b/reflex/components/forms/numberinput.pyi index b9a1e0e9b..25de0323b 100644 --- a/reflex/components/forms/numberinput.pyi +++ b/reflex/components/forms/numberinput.pyi @@ -3,7 +3,7 @@ # This file was generated by `scripts/pyi_generator.py`! # ------------------------------------------------------ -from typing import Dict, Optional, Union, overload +from typing import Any, Dict, Optional, Union, overload from reflex.components.libs.chakra import ChakraComponent from reflex.components.component import Component from reflex.vars import Var, BaseVar, ComputedVar @@ -12,7 +12,7 @@ from reflex.event import EventHandler, EventChain, EventSpec class NumberInput(ChakraComponent): @overload @classmethod - def create(cls, *children, value: Optional[Union[Var[int], int]] = None, allow_mouse_wheel: Optional[Union[Var[bool], bool]] = None, clamped_value_on_blur: Optional[Union[Var[bool], bool]] = None, default_value: Optional[Union[Var[int], int]] = None, error_border_color: Optional[Union[Var[str], str]] = None, focus_border_color: Optional[Union[Var[str], str]] = None, focus_input_on_change: Optional[Union[Var[bool], bool]] = None, input_mode: Optional[Union[Var[str], str]] = None, is_disabled: Optional[Union[Var[bool], bool]] = None, is_invalid: Optional[Union[Var[bool], bool]] = None, is_read_only: Optional[Union[Var[bool], bool]] = None, is_required: Optional[Union[Var[bool], bool]] = None, is_valid_character: Optional[Union[Var[str], str]] = None, keep_within_range: Optional[Union[Var[bool], bool]] = None, max_: Optional[Union[Var[int], int]] = None, min_: Optional[Union[Var[int], int]] = None, variant: Optional[Union[Var[str], str]] = None, on_blur: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_change: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_context_menu: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_double_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_focus: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_down: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_enter: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_leave: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_move: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_out: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_over: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_up: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_scroll: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_unmount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, **props) -> "NumberInput": # type: ignore + def create(cls, *children, value: Optional[Union[Var[Number], Number]] = None, allow_mouse_wheel: Optional[Union[Var[bool], bool]] = None, clamped_value_on_blur: Optional[Union[Var[bool], bool]] = None, default_value: Optional[Union[Var[Number], Number]] = None, error_border_color: Optional[Union[Var[str], str]] = None, focus_border_color: Optional[Union[Var[str], str]] = None, focus_input_on_change: Optional[Union[Var[bool], bool]] = None, input_mode: Optional[Union[Var[str], str]] = None, is_disabled: Optional[Union[Var[bool], bool]] = None, is_invalid: Optional[Union[Var[bool], bool]] = None, is_read_only: Optional[Union[Var[bool], bool]] = None, is_required: Optional[Union[Var[bool], bool]] = None, is_valid_character: Optional[Union[Var[str], str]] = None, keep_within_range: Optional[Union[Var[bool], bool]] = None, max_: Optional[Union[Var[Number], Number]] = None, min_: Optional[Union[Var[Number], Number]] = None, variant: Optional[Union[Var[str], str]] = None, on_blur: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_change: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_context_menu: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_double_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_focus: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_down: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_enter: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_leave: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_move: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_out: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_over: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_up: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_scroll: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_unmount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, **props) -> "NumberInput": # type: ignore """Create a number input component. If no children are provided, a default stepper will be used. diff --git a/reflex/components/forms/pininput.py b/reflex/components/forms/pininput.py index 0e889cc23..c5d312a81 100644 --- a/reflex/components/forms/pininput.py +++ b/reflex/components/forms/pininput.py @@ -1,11 +1,12 @@ """A pin input component.""" +from __future__ import annotations -from typing import Dict, Optional +from typing import Any, Optional, Union from reflex.components.component import Component from reflex.components.layout import Foreach from reflex.components.libs.chakra import ChakraComponent -from reflex.event import EVENT_ARG +from reflex.constants import EventTriggers from reflex.utils import format from reflex.vars import Var @@ -57,15 +58,16 @@ class PinInput(ChakraComponent): # "outline" | "flushed" | "filled" | "unstyled" variant: Var[str] - def get_controlled_triggers(self) -> Dict[str, Var]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers that pass the component's value to the handler. Returns: A dict mapping the event trigger to the var that is passed to the handler. """ return { - "on_change": EVENT_ARG, - "on_complete": EVENT_ARG, + **super().get_event_triggers(), + EventTriggers.ON_CHANGE: lambda e0: [e0], + EventTriggers.ON_COMPLETE: lambda e0: [e0], } def get_ref(self): diff --git a/reflex/components/forms/radio.py b/reflex/components/forms/radio.py index 9b4eeee79..3ee8e7b4e 100644 --- a/reflex/components/forms/radio.py +++ b/reflex/components/forms/radio.py @@ -1,14 +1,14 @@ """A radio component.""" -from typing import Any, Dict, List +from typing import Any, Dict, List, Union from reflex.components.component import Component from reflex.components.layout.foreach import Foreach from reflex.components.libs.chakra import ChakraComponent from reflex.components.typography.text import Text -from reflex.event import EVENT_ARG -from reflex.utils import types +from reflex.constants import EventTriggers +from reflex.utils.types import _issubclass from reflex.vars import Var @@ -23,14 +23,15 @@ class RadioGroup(ChakraComponent): # The default value. default_value: Var[Any] - def get_controlled_triggers(self) -> Dict[str, Var]: + def get_event_triggers(self) -> Dict[str, Union[Var, Any]]: """Get the event triggers that pass the component's value to the handler. Returns: A dict mapping the event trigger to the var that is passed to the handler. """ return { - "on_change": EVENT_ARG, + **super().get_event_triggers(), + EventTriggers.ON_CHANGE: lambda e0: [e0], } @classmethod @@ -49,7 +50,7 @@ class RadioGroup(ChakraComponent): if ( len(children) == 1 and isinstance(children[0], Var) - and types._issubclass(children[0].type_, List) + and _issubclass(children[0].type_, List) ): children = [Foreach.create(children[0], lambda item: Radio.create(item))] return super().create(*children, **props) diff --git a/reflex/components/forms/rangeslider.py b/reflex/components/forms/rangeslider.py index 6efc8e36b..f1758e3e5 100644 --- a/reflex/components/forms/rangeslider.py +++ b/reflex/components/forms/rangeslider.py @@ -1,10 +1,11 @@ """A range slider component.""" +from __future__ import annotations -from typing import Dict, List, Optional +from typing import Any, List, Optional, Union from reflex.components.component import Component from reflex.components.libs.chakra import ChakraComponent -from reflex.event import EVENT_ARG +from reflex.constants import EventTriggers from reflex.utils import format from reflex.vars import Var @@ -44,16 +45,17 @@ class RangeSlider(ChakraComponent): # The minimum distance between slider thumbs. Useful for preventing the thumbs from being too close together. min_steps_between_thumbs: Var[int] - def get_controlled_triggers(self) -> Dict[str, Var]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers that pass the component's value to the handler. Returns: A dict mapping the event trigger to the var that is passed to the handler. """ return { - "on_change": EVENT_ARG, - "on_change_end": EVENT_ARG, - "on_change_start": EVENT_ARG, + **super().get_event_triggers(), + EventTriggers.ON_CHANGE: lambda e0: [e0], + EventTriggers.ON_CHANGE_END: lambda e0: [e0], + EventTriggers.ON_CHANGE_START: lambda e0: [e0], } def get_ref(self): diff --git a/reflex/components/forms/select.py b/reflex/components/forms/select.py index 5c437e6e0..a2a840964 100644 --- a/reflex/components/forms/select.py +++ b/reflex/components/forms/select.py @@ -1,12 +1,13 @@ """A select component.""" -from typing import Any, Dict, List +from typing import Any, Dict, List, Union -from reflex.components.component import EVENT_ARG, Component +from reflex.components.component import Component from reflex.components.layout.foreach import Foreach from reflex.components.libs.chakra import ChakraComponent from reflex.components.typography.text import Text -from reflex.utils import types +from reflex.constants import EventTriggers +from reflex.utils.types import _issubclass from reflex.vars import Var @@ -45,15 +46,15 @@ class Select(ChakraComponent): # The size of the select. size: Var[str] - @classmethod - def get_controlled_triggers(cls) -> Dict[str, Var]: + def get_event_triggers(self) -> Dict[str, Union[Var, Any]]: """Get the event triggers that pass the component's value to the handler. Returns: A dict mapping the event trigger to the var that is passed to the handler. """ return { - "on_change": EVENT_ARG.target.value, + **super().get_event_triggers(), + EventTriggers.ON_CHANGE: lambda e0: [e0.target.value], } @classmethod @@ -75,7 +76,7 @@ class Select(ChakraComponent): if ( len(children) == 1 and isinstance(children[0], Var) - and types._issubclass(children[0].type_, List) + and _issubclass(children[0].type_, List) ): children = [Foreach.create(children[0], lambda item: Option.create(item))] return super().create(*children, **props) diff --git a/reflex/components/forms/slider.py b/reflex/components/forms/slider.py index c2878e3b9..23307d943 100644 --- a/reflex/components/forms/slider.py +++ b/reflex/components/forms/slider.py @@ -1,10 +1,11 @@ """A slider component.""" +from __future__ import annotations -from typing import Dict +from typing import Any, Union from reflex.components.component import Component from reflex.components.libs.chakra import ChakraComponent -from reflex.event import EVENT_ARG +from reflex.constants import EventTriggers from reflex.vars import Var @@ -64,17 +65,18 @@ class Slider(ChakraComponent): # Maximum width of the slider. max_w: Var[str] - def get_controlled_triggers(self) -> Dict[str, Var]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers that pass the component's value to the handler. Returns: A dict mapping the event trigger to the var that is passed to the handler. """ return { - "on_change": EVENT_ARG, - "on_change_end": EVENT_ARG, - "on_change_start": EVENT_ARG, - } + **super().get_event_triggers(), + EventTriggers.ON_CHANGE: lambda e0: [e0], + EventTriggers.ON_CHANGE_END: lambda e0: [e0], + EventTriggers.ON_CHANGE_START: lambda e0: [e0], + } # type: ignore @classmethod def create(cls, *children, **props) -> Component: diff --git a/reflex/components/forms/switch.py b/reflex/components/forms/switch.py index 656fbe7c8..bbb429265 100644 --- a/reflex/components/forms/switch.py +++ b/reflex/components/forms/switch.py @@ -1,8 +1,10 @@ """A switch component.""" -from typing import Dict +from __future__ import annotations + +from typing import Any, Union -from reflex.components.component import EVENT_ARG from reflex.components.libs.chakra import ChakraComponent +from reflex.constants import EventTriggers from reflex.vars import Var @@ -41,12 +43,13 @@ class Switch(ChakraComponent): # The color scheme of the switch (e.g. "blue", "green", "red", etc.) color_scheme: Var[str] - def get_controlled_triggers(self) -> Dict[str, Var]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers that pass the component's value to the handler. Returns: A dict mapping the event trigger to the var that is passed to the handler. """ return { - "on_change": EVENT_ARG.target.checked, + **super().get_event_triggers(), + EventTriggers.ON_CHANGE: lambda e0: [e0.target.checked], } diff --git a/reflex/components/forms/textarea.py b/reflex/components/forms/textarea.py index d28dd82c4..329f1db47 100644 --- a/reflex/components/forms/textarea.py +++ b/reflex/components/forms/textarea.py @@ -1,10 +1,12 @@ """A textarea component.""" +from __future__ import annotations -from typing import Dict +from typing import Any, Union -from reflex.components.component import EVENT_ARG, Component +from reflex.components.component import Component from reflex.components.forms.debounce import DebounceInput from reflex.components.libs.chakra import ChakraComponent +from reflex.constants import EventTriggers from reflex.vars import Var @@ -43,18 +45,19 @@ class TextArea(ChakraComponent): # "outline" | "filled" | "flushed" | "unstyled" variant: Var[str] - def get_controlled_triggers(self) -> Dict[str, Var]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers that pass the component's value to the handler. Returns: A dict mapping the event trigger to the var that is passed to the handler. """ return { - "on_change": EVENT_ARG.target.value, - "on_focus": EVENT_ARG.target.value, - "on_blur": EVENT_ARG.target.value, - "on_key_down": EVENT_ARG.key, - "on_key_up": EVENT_ARG.key, + **super().get_event_triggers(), + EventTriggers.ON_CHANGE: lambda e0: [e0.target.value], + EventTriggers.ON_FOCUS: lambda e0: [e0.target.value], + EventTriggers.ON_BLUR: lambda e0: [e0.target.value], + EventTriggers.ON_KEY_DOWN: lambda e0: [e0.key], + EventTriggers.ON_KEY_UP: lambda e0: [e0.key], } @classmethod diff --git a/reflex/components/forms/upload.py b/reflex/components/forms/upload.py index d882ee304..0dc089888 100644 --- a/reflex/components/forms/upload.py +++ b/reflex/components/forms/upload.py @@ -1,11 +1,12 @@ """A file upload component.""" from __future__ import annotations -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional, Union -from reflex.components.component import EVENT_ARG, Component +from reflex.components.component import Component from reflex.components.forms.input import Input from reflex.components.layout.box import Box +from reflex.constants import EventTriggers from reflex.event import EventChain from reflex.vars import BaseVar, Var @@ -89,14 +90,15 @@ class Upload(Component): # Create the component. return super().create(zone, on_drop=upload_file, **upload_props) - def get_controlled_triggers(self) -> Dict[str, Var]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers that pass the component's value to the handler. Returns: A dict mapping the event trigger to the var that is passed to the handler. """ return { - "on_drop": EVENT_ARG, + **super().get_event_triggers(), + EventTriggers.ON_DROP: lambda e0: [e0], } def _render(self): diff --git a/reflex/components/graphing/plotly.pyi b/reflex/components/graphing/plotly.pyi index 7ee75c73f..c16e76b82 100644 --- a/reflex/components/graphing/plotly.pyi +++ b/reflex/components/graphing/plotly.pyi @@ -31,7 +31,7 @@ class PlotlyLib(NoSSRComponent): class Plotly(PlotlyLib): @overload @classmethod - def create(cls, *children, data: Optional[Union[Var[Any], Any]] = None, layout: Optional[Union[Var[Dict], Dict]] = None, width: Optional[Union[Var[str], str]] = None, height: Optional[Union[Var[str], str]] = None, use_resize_handler: Optional[Union[Var[bool], bool]] = None, on_blur: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_context_menu: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_double_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_focus: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_down: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_enter: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_leave: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_move: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_out: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_over: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_up: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_scroll: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_unmount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, **props) -> "Plotly": # type: ignore + def create(cls, *children, data: Optional[Union[Var[Figure], Figure]] = None, layout: Optional[Union[Var[Dict], Dict]] = None, width: Optional[Union[Var[str], str]] = None, height: Optional[Union[Var[str], str]] = None, use_resize_handler: Optional[Union[Var[bool], bool]] = None, on_blur: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_context_menu: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_double_click: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_focus: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_down: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_enter: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_leave: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_move: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_out: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_over: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_mouse_up: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_scroll: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, on_unmount: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, **props) -> "Plotly": # type: ignore """Create the component. Args: diff --git a/reflex/components/media/avatar.py b/reflex/components/media/avatar.py index fa1f4dd43..74e9c9c14 100644 --- a/reflex/components/media/avatar.py +++ b/reflex/components/media/avatar.py @@ -1,6 +1,7 @@ """Avatar components.""" +from __future__ import annotations -from typing import Set +from typing import Any, Union from reflex.components.libs.chakra import ChakraComponent from reflex.vars import Var @@ -35,13 +36,16 @@ class Avatar(ChakraComponent): # "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "full" size: Var[str] - def get_triggers(self) -> Set[str]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers for the component. Returns: The event triggers. """ - return super().get_triggers() | {"on_error"} + return { + **super().get_event_triggers(), + "on_error": lambda: [], + } class AvatarBadge(ChakraComponent): diff --git a/reflex/components/media/avatar.pyi b/reflex/components/media/avatar.pyi index 629362425..65d148efd 100644 --- a/reflex/components/media/avatar.pyi +++ b/reflex/components/media/avatar.pyi @@ -3,7 +3,7 @@ # This file was generated by `scripts/pyi_generator.py`! # ------------------------------------------------------ -from typing import Optional, Set, Union, overload +from typing import Dict, Optional, Union, overload from reflex.components.libs.chakra import ChakraComponent from reflex.components.component import Component from reflex.vars import Var, BaseVar, ComputedVar diff --git a/reflex/components/media/image.py b/reflex/components/media/image.py index 0f25801e1..2c76bb6b7 100644 --- a/reflex/components/media/image.py +++ b/reflex/components/media/image.py @@ -3,7 +3,7 @@ from __future__ import annotations import base64 import io -from typing import Any, Optional +from typing import Any, Optional, Union from reflex.components.component import Component from reflex.components.libs.chakra import ChakraComponent @@ -53,13 +53,17 @@ class Image(ChakraComponent): # Learn more _[here](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images)_ src_set: Var[str] - def get_triggers(self) -> set[str]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers for the component. Returns: The event triggers. """ - return super().get_triggers() | {"on_error", "on_load"} + return { + **super().get_event_triggers(), + "on_error": lambda: [], + "on_load": lambda: [], + } def _render(self) -> Tag: self.src.is_string = True diff --git a/reflex/components/overlay/alertdialog.py b/reflex/components/overlay/alertdialog.py index ecf8d3c91..afce87851 100644 --- a/reflex/components/overlay/alertdialog.py +++ b/reflex/components/overlay/alertdialog.py @@ -1,6 +1,7 @@ """Alert dialog components.""" +from __future__ import annotations -from typing import Set +from typing import Any, Union from reflex.components.component import Component from reflex.components.libs.chakra import ChakraComponent @@ -52,17 +53,18 @@ class AlertDialog(ChakraComponent): # If true, the siblings of the modal will have `aria-hidden` set to true so that screen readers can only see the modal. This is commonly known as making the other elements **inert** use_inert: Var[bool] - def get_triggers(self) -> Set[str]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers for the component. Returns: The event triggers. """ - return super().get_triggers() | { - "on_close", - "on_close_complete", - "on_esc", - "on_overlay_click", + return { + **super().get_event_triggers(), + "on_close": lambda: [], + "on_close_complete": lambda: [], + "on_esc": lambda: [], + "on_overlay_click": lambda: [], } @classmethod diff --git a/reflex/components/overlay/alertdialog.pyi b/reflex/components/overlay/alertdialog.pyi index 0b951c6e9..975840a8a 100644 --- a/reflex/components/overlay/alertdialog.pyi +++ b/reflex/components/overlay/alertdialog.pyi @@ -3,7 +3,7 @@ # This file was generated by `scripts/pyi_generator.py`! # ------------------------------------------------------ -from typing import Optional, Set, Union, overload +from typing import Dict, Optional, Union, overload from reflex.components.libs.chakra import ChakraComponent from reflex.components.component import Component from reflex.vars import Var, BaseVar, ComputedVar diff --git a/reflex/components/overlay/drawer.py b/reflex/components/overlay/drawer.py index 8e55aa3f0..9278a5159 100644 --- a/reflex/components/overlay/drawer.py +++ b/reflex/components/overlay/drawer.py @@ -1,6 +1,7 @@ """Container to stack elements with spacing.""" +from __future__ import annotations -from typing import Set +from typing import Any, Union from reflex.components.component import Component from reflex.components.libs.chakra import ChakraComponent @@ -58,17 +59,18 @@ class Drawer(ChakraComponent): # Variant of drawer variant: Var[str] - def get_triggers(self) -> Set[str]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers for the component. Returns: The event triggers. """ - return super().get_triggers() | { - "on_close", - "on_close_complete", - "on_esc", - "on_overlay_click", + return { + **super().get_event_triggers(), + "on_close": lambda: [], + "on_close_complete": lambda: [], + "on_esc": lambda: [], + "on_overlay_click": lambda: [], } @classmethod diff --git a/reflex/components/overlay/drawer.pyi b/reflex/components/overlay/drawer.pyi index 2cbd58274..b1bc17420 100644 --- a/reflex/components/overlay/drawer.pyi +++ b/reflex/components/overlay/drawer.pyi @@ -3,7 +3,7 @@ # This file was generated by `scripts/pyi_generator.py`! # ------------------------------------------------------ -from typing import Optional, Set, Union, overload +from typing import Dict, Optional, Union, overload from reflex.components.libs.chakra import ChakraComponent from reflex.components.component import Component from reflex.vars import Var, BaseVar, ComputedVar diff --git a/reflex/components/overlay/menu.py b/reflex/components/overlay/menu.py index 296a2578f..5a571ce7d 100644 --- a/reflex/components/overlay/menu.py +++ b/reflex/components/overlay/menu.py @@ -1,6 +1,7 @@ """Menu components.""" +from __future__ import annotations -from typing import List, Set +from typing import Any, List, Union from reflex.components.component import Component from reflex.components.libs.chakra import ChakraComponent @@ -60,13 +61,17 @@ class Menu(ChakraComponent): # The CSS positioning strategy to use. ("fixed" | "absolute") strategy: Var[str] - def get_triggers(self) -> Set[str]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers for the component. Returns: The event triggers. """ - return super().get_triggers() | {"on_close", "on_open"} + return { + **super().get_event_triggers(), + "on_close": lambda: [], + "on_open": lambda: [], + } @classmethod def create(cls, *children, button=None, items=None, **props) -> Component: diff --git a/reflex/components/overlay/menu.pyi b/reflex/components/overlay/menu.pyi index 79e9ead5e..7c4f0a609 100644 --- a/reflex/components/overlay/menu.pyi +++ b/reflex/components/overlay/menu.pyi @@ -3,7 +3,7 @@ # This file was generated by `scripts/pyi_generator.py`! # ------------------------------------------------------ -from typing import List, Optional, Set, Union, overload +from typing import Dict, List, Optional, Union, overload from reflex.components.libs.chakra import ChakraComponent from reflex.components.component import Component from reflex.vars import Var, BaseVar, ComputedVar diff --git a/reflex/components/overlay/modal.py b/reflex/components/overlay/modal.py index b1f5a3179..e764afbf1 100644 --- a/reflex/components/overlay/modal.py +++ b/reflex/components/overlay/modal.py @@ -1,6 +1,7 @@ """Modal components.""" +from __future__ import annotations -from typing import Optional, Set, Union +from typing import Any, Optional, Union from reflex.components.component import Component from reflex.components.libs.chakra import ChakraComponent @@ -52,17 +53,18 @@ class Modal(ChakraComponent): # A11y: If true, the siblings of the modal will have `aria-hidden` set to true so that screen readers can only see the modal. This is commonly known as making the other elements **inert** use_inert: Var[bool] - def get_triggers(self) -> Set[str]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers for the component. Returns: The event triggers. """ - return super().get_triggers() | { - "on_close", - "on_close_complete", - "on_esc", - "on_overlay_click", + return { + **super().get_event_triggers(), + "on_close": lambda: [], + "on_close_complete": lambda: [], + "on_esc": lambda: [], + "on_overlay_click": lambda: [], } @classmethod @@ -73,7 +75,7 @@ class Modal(ChakraComponent): body: Optional[Union[Component, str]] = None, footer: Optional[Union[Component, str]] = None, close_button: Optional[Component] = None, - **props + **props, ) -> Component: """Create a modal component. diff --git a/reflex/components/overlay/modal.pyi b/reflex/components/overlay/modal.pyi index 57c1b1ad3..297605c2f 100644 --- a/reflex/components/overlay/modal.pyi +++ b/reflex/components/overlay/modal.pyi @@ -3,7 +3,7 @@ # This file was generated by `scripts/pyi_generator.py`! # ------------------------------------------------------ -from typing import Optional, Set, Union, overload +from typing import Dict, Optional, Union, overload from reflex.components.libs.chakra import ChakraComponent from reflex.components.component import Component from reflex.vars import Var, BaseVar, ComputedVar diff --git a/reflex/components/overlay/popover.py b/reflex/components/overlay/popover.py index f1b500065..3ebef5220 100644 --- a/reflex/components/overlay/popover.py +++ b/reflex/components/overlay/popover.py @@ -1,6 +1,7 @@ """Popover components.""" +from __future__ import annotations -from typing import Set +from typing import Any, Union from reflex.components.component import Component from reflex.components.libs.chakra import ChakraComponent @@ -75,13 +76,17 @@ class Popover(ChakraComponent): # The interaction that triggers the popover. hover - means the popover will open when you hover with mouse or focus with keyboard on the popover trigger click - means the popover will open on click or press Enter to Space on keyboard ("click" | "hover") trigger: Var[str] - def get_triggers(self) -> Set[str]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers for the component. Returns: The event triggers. """ - return super().get_triggers() | {"on_close", "on_open"} + return { + **super().get_event_triggers(), + "on_close": lambda: [], + "on_open": lambda: [], + } @classmethod def create( @@ -92,7 +97,7 @@ class Popover(ChakraComponent): body=None, footer=None, use_close_button=False, - **props + **props, ) -> Component: """Create a popover component. diff --git a/reflex/components/overlay/popover.pyi b/reflex/components/overlay/popover.pyi index 95868b144..65e35673d 100644 --- a/reflex/components/overlay/popover.pyi +++ b/reflex/components/overlay/popover.pyi @@ -3,7 +3,7 @@ # This file was generated by `scripts/pyi_generator.py`! # ------------------------------------------------------ -from typing import Optional, Set, Union, overload +from typing import Dict, Optional, Union, overload from reflex.components.libs.chakra import ChakraComponent from reflex.components.component import Component from reflex.vars import Var, BaseVar, ComputedVar diff --git a/reflex/components/overlay/tooltip.py b/reflex/components/overlay/tooltip.py index cc106886d..e5cbd48ab 100644 --- a/reflex/components/overlay/tooltip.py +++ b/reflex/components/overlay/tooltip.py @@ -1,6 +1,7 @@ """Tooltip components.""" +from __future__ import annotations -from typing import Set +from typing import Any, Union from reflex.components.libs.chakra import ChakraComponent from reflex.vars import Var @@ -62,10 +63,14 @@ class Tooltip(ChakraComponent): # If true, the tooltip will wrap its children in a `` with `tabIndex=0` should_wrap_children: Var[bool] - def get_triggers(self) -> Set[str]: + def get_event_triggers(self) -> dict[str, Union[Var, Any]]: """Get the event triggers for the component. Returns: The event triggers. """ - return super().get_triggers() | {"on_close", "on_open"} + return { + **super().get_event_triggers(), + "on_close": lambda: [], + "on_open": lambda: [], + } diff --git a/reflex/components/overlay/tooltip.pyi b/reflex/components/overlay/tooltip.pyi index acf5a05d8..4435649cd 100644 --- a/reflex/components/overlay/tooltip.pyi +++ b/reflex/components/overlay/tooltip.pyi @@ -3,7 +3,7 @@ # This file was generated by `scripts/pyi_generator.py`! # ------------------------------------------------------ -from typing import Optional, Set, Union, overload +from typing import Dict, Optional, Union, overload from reflex.components.libs.chakra import ChakraComponent from reflex.components.component import Component from reflex.vars import Var, BaseVar, ComputedVar diff --git a/reflex/constants.py b/reflex/constants.py index 0e874dcbb..ab4942874 100644 --- a/reflex/constants.py +++ b/reflex/constants.py @@ -407,9 +407,36 @@ ALEMBIC_CONFIG = os.environ.get("ALEMBIC_CONFIG", "alembic.ini") COOKIES = "cookies" LOCAL_STORAGE = "local_storage" -# Names of event handlers on all components mapped to useEffect -ON_MOUNT = "on_mount" -ON_UNMOUNT = "on_unmount" + +class EventTriggers(SimpleNamespace): + """All trigger names used in Reflex.""" + + ON_FOCUS = "on_focus" + ON_BLUR = "on_blur" + ON_CANCEL = "on_cancel" + ON_CLICK = "on_click" + ON_CHANGE = "on_change" + ON_CHANGE_END = "on_change_end" + ON_CHANGE_START = "on_change_start" + ON_COMPLETE = "on_complete" + ON_CONTEXT_MENU = "on_context_menu" + ON_DOUBLE_CLICK = "on_double_click" + ON_DROP = "on_drop" + ON_EDIT = "on_edit" + ON_KEY_DOWN = "on_key_down" + ON_KEY_UP = "on_key_up" + ON_MOUSE_DOWN = "on_mouse_down" + ON_MOUSE_ENTER = "on_mouse_enter" + ON_MOUSE_LEAVE = "on_mouse_leave" + ON_MOUSE_MOVE = "on_mouse_move" + ON_MOUSE_OUT = "on_mouse_out" + ON_MOUSE_OVER = "on_mouse_over" + ON_MOUSE_UP = "on_mouse_up" + ON_SCROLL = "on_scroll" + ON_SUBMIT = "on_submit" + ON_MOUNT = "on_mount" + ON_UNMOUNT = "on_unmount" + # If this env var is set to "yes", App.compile will be a no-op SKIP_COMPILE_ENV_VAR = "__REFLEX_SKIP_COMPILE" diff --git a/reflex/event.py b/reflex/event.py index e7f3e547a..04e37e67c 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -2,11 +2,12 @@ from __future__ import annotations import inspect -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any, Callable, Dict, List, Optional, Tuple, Union from reflex import constants from reflex.base import Base -from reflex.utils import format +from reflex.utils import console, format +from reflex.utils.types import ArgsSpec from reflex.vars import BaseVar, Var @@ -109,6 +110,8 @@ class EventChain(Base): events: List[EventSpec] + args_spec: Optional[ArgsSpec] + class Target(Base): """A Javascript event target.""" @@ -383,7 +386,9 @@ def get_hydrate_event(state) -> str: return get_event(state, constants.HYDRATE) -def call_event_handler(event_handler: EventHandler, arg: Var) -> EventSpec: +def call_event_handler( + event_handler: EventHandler, arg_spec: Union[Var, ArgsSpec] +) -> EventSpec: """Call an event handler to get the event spec. This function will inspect the function signature of the event handler. @@ -392,21 +397,66 @@ def call_event_handler(event_handler: EventHandler, arg: Var) -> EventSpec: Args: event_handler: The event handler. - arg: The argument to pass to the event handler. + arg_spec: The lambda that define the argument(s) to pass to the event handler. + + Raises: + ValueError: if number of arguments expected by event_handler doesn't match the spec. Returns: The event spec from calling the event handler. """ args = inspect.getfullargspec(event_handler.fn).args + + # handle new API using lambda to define triggers + if isinstance(arg_spec, ArgsSpec): + parsed_args = parse_args_spec(arg_spec) + + if len(args) == len(["self", *parsed_args]): + return event_handler(*parsed_args) # type: ignore + else: + source = inspect.getsource(arg_spec) + raise ValueError( + f"number of arguments in {event_handler.fn.__name__} " + f"doesn't match the definition '{source.strip().strip(',')}'" + ) + else: + console.deprecate( + feature_name="EVENT_ARG API for triggers", + reason="Replaced by new API using lambda allow arbitrary number of args", + deprecation_version="0.2.8", + removal_version="0.2.9", + ) if len(args) == 1: return event_handler() assert ( len(args) == 2 ), f"Event handler {event_handler.fn} must have 1 or 2 arguments." - return event_handler(arg) + return event_handler(arg_spec) -def call_event_fn(fn: Callable, arg: Var) -> list[EventSpec]: +def parse_args_spec(arg_spec: ArgsSpec): + """Parse the args provided in the ArgsSpec of an event trigger. + + Args: + arg_spec: The spec of the args. + + Returns: + The parsed args. + """ + spec = inspect.getfullargspec(arg_spec) + return arg_spec( + *[ + BaseVar( + name=f"_{l_arg}", + type_=spec.annotations.get(l_arg, FrontendEvent), + is_local=True, + ) + for l_arg in spec.args + ] + ) + + +def call_event_fn(fn: Callable, arg: Union[Var, ArgsSpec]) -> list[EventSpec]: """Call a function to a list of event specs. The function should return either a single EventSpec or a list of EventSpecs. @@ -429,13 +479,16 @@ def call_event_fn(fn: Callable, arg: Var) -> list[EventSpec]: # Get the args of the lambda. args = inspect.getfullargspec(fn).args - # Call the lambda. - if len(args) == 0: - out = fn() - elif len(args) == 1: - out = fn(arg) + if isinstance(arg, ArgsSpec): + out = fn(*parse_args_spec(arg)) else: - raise ValueError(f"Lambda {fn} must have 0 or 1 arguments.") + # Call the lambda. + if len(args) == 0: + out = fn() + elif len(args) == 1: + out = fn(arg) + else: + raise ValueError(f"Lambda {fn} must have 0 or 1 arguments.") # Convert the output to a list. if not isinstance(out, List): @@ -449,7 +502,7 @@ def call_event_fn(fn: Callable, arg: Var) -> list[EventSpec]: if len(args) == 0: e = e() elif len(args) == 1: - e = e(arg) + e = e(arg) # type: ignore # Make sure the event spec is valid. if not isinstance(e, EventSpec): @@ -462,12 +515,11 @@ def call_event_fn(fn: Callable, arg: Var) -> list[EventSpec]: return events -def get_handler_args(event_spec: EventSpec, arg: Var) -> tuple[tuple[Var, Var], ...]: +def get_handler_args(event_spec: EventSpec) -> tuple[tuple[Var, Var], ...]: """Get the handler args for the given event spec. Args: event_spec: The event spec. - arg: The controlled event argument. Returns: The handler args. @@ -539,21 +591,3 @@ def get_fn_signature(fn: Callable) -> inspect.Signature: "state", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Any ) return signature.replace(parameters=(new_param, *signature.parameters.values())) - - -# A set of common event triggers. -EVENT_TRIGGERS: set[str] = { - "on_focus", - "on_blur", - "on_click", - "on_context_menu", - "on_double_click", - "on_mouse_down", - "on_mouse_enter", - "on_mouse_leave", - "on_mouse_move", - "on_mouse_out", - "on_mouse_over", - "on_mouse_up", - "on_scroll", -} diff --git a/reflex/utils/format.py b/reflex/utils/format.py index e4e79ca66..6e6e0d610 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -2,6 +2,7 @@ from __future__ import annotations +import inspect import json import os import os.path as op @@ -300,9 +301,21 @@ def format_prop( # Handle event props. elif isinstance(prop, EventChain): + if prop.args_spec is None: + arg_def = f"{EVENT_ARG}" + else: + sig = inspect.signature(prop.args_spec) + if sig.parameters: + arg_def = ",".join(f"_{p}" for p in sig.parameters) + arg_def = f"({arg_def})" + else: + # add a default argument for addEvents if none were specified in prop.args_spec + # used to trigger the preventDefault() on the event. + arg_def = "(_e)" + chain = ",".join([format_event(event) for event in prop.events]) - event = f"Event([{chain}], {EVENT_ARG})" - prop = f"{EVENT_ARG} => {event}" + event = f"addEvents([{chain}], {arg_def})" + prop = f"{arg_def} => {event}" # Handle other types. elif isinstance(prop, str): @@ -414,7 +427,7 @@ def format_event(event_spec: EventSpec) -> str: if event_spec.client_handler_name: event_args.append(wrap(event_spec.client_handler_name, '"')) - return f"E({', '.join(event_args)})" + return f"Event({', '.join(event_args)})" def format_event_chain( @@ -450,7 +463,7 @@ def format_event_chain( chain = ",".join([format_event(event) for event in event_chain.events]) return "".join( [ - f"Event([{chain}]", + f"addEvents([{chain}]", f", {format_var(event_arg)}" if event_arg else "", ")", ] diff --git a/reflex/utils/types.py b/reflex/utils/types.py index bfa3e5cd5..a0c9a1f8c 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -4,6 +4,7 @@ from __future__ import annotations import contextlib import typing +from types import LambdaType from typing import Any, Callable, Type, Union, _GenericAlias # type: ignore from reflex.base import Base @@ -17,6 +18,8 @@ PrimitiveType = Union[int, float, bool, str, list, dict, set, tuple] StateVar = Union[PrimitiveType, Base, None] StateIterVar = Union[list, set, tuple] +ArgsSpec = LambdaType + def get_args(alias: _GenericAlias) -> tuple[Type, ...]: """Get the arguments of a type alias. diff --git a/scripts/pyi_generator.py b/scripts/pyi_generator.py index b8f3c9b70..bc6f1b8b7 100644 --- a/scripts/pyi_generator.py +++ b/scripts/pyi_generator.py @@ -123,7 +123,7 @@ class PyiGenerator: continue definition += f"{name}: {_get_type_hint(value)} = None, " - for trigger in sorted(_class().get_triggers()): + for trigger in sorted(_class().get_event_triggers().keys()): definition += f"{trigger}: Optional[Union[EventHandler, EventSpec, List, function, BaseVar]] = None, " definition = definition.rstrip(", ") diff --git a/tests/components/base/test_script.py b/tests/components/base/test_script.py index 99cd985ac..ffee64b6e 100644 --- a/tests/components/base/test_script.py +++ b/tests/components/base/test_script.py @@ -57,13 +57,14 @@ def test_script_event_handler(): ) render_dict = component.render() assert ( - 'onReady={_e => Event([E("ev_state.on_ready", {})], _e)}' + 'onReady={(_e) => addEvents([Event("ev_state.on_ready", {})], (_e))}' in render_dict["props"] ) assert ( - 'onLoad={_e => Event([E("ev_state.on_load", {})], _e)}' in render_dict["props"] - ) - assert ( - 'onError={_e => Event([E("ev_state.on_error", {})], _e)}' + 'onLoad={(_e) => addEvents([Event("ev_state.on_load", {})], (_e))}' + in render_dict["props"] + ) + assert ( + 'onError={(_e) => addEvents([Event("ev_state.on_error", {})], (_e))}' in render_dict["props"] ) diff --git a/tests/components/test_component.py b/tests/components/test_component.py index 33249c3da..f1979fa65 100644 --- a/tests/components/test_component.py +++ b/tests/components/test_component.py @@ -1,12 +1,13 @@ -from typing import Dict, List, Type +from typing import Any, Dict, List, Type import pytest import reflex as rx +from reflex.base import Base from reflex.components.component import Component, CustomComponent, custom_component from reflex.components.layout.box import Box -from reflex.constants import ON_MOUNT, ON_UNMOUNT -from reflex.event import EVENT_ARG, EVENT_TRIGGERS, EventHandler +from reflex.constants import EventTriggers +from reflex.event import EVENT_ARG, EventHandler from reflex.state import State from reflex.style import Style from reflex.utils import imports @@ -371,16 +372,71 @@ def test_get_controlled_triggers(component1, component2): assert set(component2().get_controlled_triggers()) == {"on_open", "on_close"} -def test_get_triggers(component1, component2): +def test_get_event_triggers(component1, component2): """Test that we can get the triggers of a component. Args: component1: A test component. component2: A test component. """ - default_triggers = {ON_MOUNT, ON_UNMOUNT} | EVENT_TRIGGERS - assert component1().get_triggers() == default_triggers - assert component2().get_triggers() == {"on_open", "on_close"} | default_triggers + default_triggers = { + EventTriggers.ON_FOCUS, + EventTriggers.ON_BLUR, + EventTriggers.ON_CLICK, + EventTriggers.ON_CONTEXT_MENU, + EventTriggers.ON_DOUBLE_CLICK, + EventTriggers.ON_MOUSE_DOWN, + EventTriggers.ON_MOUSE_ENTER, + EventTriggers.ON_MOUSE_LEAVE, + EventTriggers.ON_MOUSE_MOVE, + EventTriggers.ON_MOUSE_OUT, + EventTriggers.ON_MOUSE_OVER, + EventTriggers.ON_MOUSE_UP, + EventTriggers.ON_SCROLL, + EventTriggers.ON_MOUNT, + EventTriggers.ON_UNMOUNT, + } + assert set(component1().get_event_triggers().keys()) == default_triggers + assert ( + component2().get_event_triggers().keys() + == {"on_open", "on_close"} | default_triggers + ) + + +class C1State(State): + """State for testing C1 component.""" + + def mock_handler(self, _e, _bravo, _charlie): + """Mock handler.""" + pass + + +def test_component_event_trigger_arbitrary_args(): + """Test that we can define arbitrary types for the args of an event trigger.""" + + class Obj(Base): + custom: int = 0 + + def on_foo_spec(_e, alpha: str, bravo: Dict[str, Any], charlie: Obj): + return [_e.target.value, bravo["nested"], charlie.custom + 42] + + class C1(Component): + library = "/local" + tag = "C1" + + def get_event_triggers(self) -> Dict[str, Any]: + return { + **super().get_event_triggers(), + "on_foo": on_foo_spec, + } + + comp = C1.create(on_foo=C1State.mock_handler) + + assert comp.render()["props"][0] == ( + "onFoo={(__e,_alpha,_bravo,_charlie) => addEvents(" + '[Event("c1_state.mock_handler", {_e:__e.target.value,_bravo:_bravo["nested"],_charlie:(_charlie.custom + 42)})], ' + "(__e,_alpha,_bravo,_charlie))}" + ) def test_create_custom_component(my_component): diff --git a/tests/test_event.py b/tests/test_event.py index debfac54c..839770135 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -48,7 +48,7 @@ def test_call_event_handler(): assert event_spec.handler == handler assert event_spec.args == () - assert format.format_event(event_spec) == 'E("test_fn", {})' + assert format.format_event(event_spec) == 'Event("test_fn", {})' handler = EventHandler(fn=test_fn_with_args) event_spec = handler(make_var("first"), make_var("second")) @@ -61,14 +61,14 @@ def test_call_event_handler(): assert event_spec.args[1][1].equals(Var.create_safe("second")) assert ( format.format_event(event_spec) - == 'E("test_fn_with_args", {arg1:first,arg2:second})' + == 'Event("test_fn_with_args", {arg1:first,arg2:second})' ) # Passing args as strings should format differently. event_spec = handler("first", "second") # type: ignore assert ( format.format_event(event_spec) - == 'E("test_fn_with_args", {arg1:"first",arg2:"second"})' + == 'Event("test_fn_with_args", {arg1:"first",arg2:"second"})' ) first, second = 123, "456" @@ -76,7 +76,7 @@ def test_call_event_handler(): event_spec = handler(first, second) # type: ignore assert ( format.format_event(event_spec) - == 'E("test_fn_with_args", {arg1:123,arg2:"456"})' + == 'Event("test_fn_with_args", {arg1:123,arg2:"456"})' ) assert event_spec.handler == handler @@ -126,9 +126,9 @@ def test_event_redirect(): assert spec.handler.fn.__qualname__ == "_redirect" assert spec.args[0][0].equals(Var.create_safe("path")) assert spec.args[0][1].equals(Var.create_safe("/path")) - assert format.format_event(spec) == 'E("_redirect", {path:"/path"})' + assert format.format_event(spec) == 'Event("_redirect", {path:"/path"})' spec = event.redirect(Var.create_safe("path")) - assert format.format_event(spec) == 'E("_redirect", {path:path})' + assert format.format_event(spec) == 'Event("_redirect", {path:path})' def test_event_console_log(): @@ -138,9 +138,9 @@ def test_event_console_log(): assert spec.handler.fn.__qualname__ == "_console" assert spec.args[0][0].equals(Var.create_safe("message")) assert spec.args[0][1].equals(Var.create_safe("message")) - assert format.format_event(spec) == 'E("_console", {message:"message"})' + assert format.format_event(spec) == 'Event("_console", {message:"message"})' spec = event.console_log(Var.create_safe("message")) - assert format.format_event(spec) == 'E("_console", {message:message})' + assert format.format_event(spec) == 'Event("_console", {message:message})' def test_event_window_alert(): @@ -150,9 +150,9 @@ def test_event_window_alert(): assert spec.handler.fn.__qualname__ == "_alert" assert spec.args[0][0].equals(Var.create_safe("message")) assert spec.args[0][1].equals(Var.create_safe("message")) - assert format.format_event(spec) == 'E("_alert", {message:"message"})' + assert format.format_event(spec) == 'Event("_alert", {message:"message"})' spec = event.window_alert(Var.create_safe("message")) - assert format.format_event(spec) == 'E("_alert", {message:message})' + assert format.format_event(spec) == 'Event("_alert", {message:message})' def test_set_focus(): @@ -162,9 +162,9 @@ def test_set_focus(): assert spec.handler.fn.__qualname__ == "_set_focus" assert spec.args[0][0].equals(Var.create_safe("ref")) assert spec.args[0][1].equals(Var.create_safe("ref_input1")) - assert format.format_event(spec) == 'E("_set_focus", {ref:ref_input1})' + assert format.format_event(spec) == 'Event("_set_focus", {ref:ref_input1})' spec = event.set_focus("input1") - assert format.format_event(spec) == 'E("_set_focus", {ref:ref_input1})' + assert format.format_event(spec) == 'Event("_set_focus", {ref:ref_input1})' def test_set_value(): @@ -176,10 +176,11 @@ def test_set_value(): assert spec.args[0][1].equals(Var.create_safe("ref_input1")) assert spec.args[1][0].equals(Var.create_safe("value")) assert spec.args[1][1].equals(Var.create_safe("")) - assert format.format_event(spec) == 'E("_set_value", {ref:ref_input1,value:""})' + assert format.format_event(spec) == 'Event("_set_value", {ref:ref_input1,value:""})' spec = event.set_value("input1", Var.create_safe("message")) assert ( - format.format_event(spec) == 'E("_set_value", {ref:ref_input1,value:message})' + format.format_event(spec) + == 'Event("_set_value", {ref:ref_input1,value:message})' ) @@ -194,7 +195,7 @@ def test_set_cookie(): assert spec.args[1][1].equals(Var.create_safe("testvalue")) assert ( format.format_event(spec) - == 'E("_set_cookie", {key:"testkey",value:"testvalue"})' + == 'Event("_set_cookie", {key:"testkey",value:"testvalue"})' ) @@ -208,7 +209,8 @@ def test_remove_cookie(): assert spec.args[1][0].equals(Var.create_safe("options")) assert spec.args[1][1].equals(Var.create_safe({})) assert ( - format.format_event(spec) == 'E("_remove_cookie", {key:"testkey",options:{}})' + format.format_event(spec) + == 'Event("_remove_cookie", {key:"testkey",options:{}})' ) @@ -229,7 +231,7 @@ def test_remove_cookie_with_options(): assert spec.args[1][1].equals(Var.create_safe(options)) assert ( format.format_event(spec) - == f'E("_remove_cookie", {{key:"testkey",options:{json.dumps(options)}}})' + == f'Event("_remove_cookie", {{key:"testkey",options:{json.dumps(options)}}})' ) @@ -244,7 +246,7 @@ def test_set_local_storage(): assert spec.args[1][1].equals(Var.create_safe("testvalue")) assert ( format.format_event(spec) - == 'E("_set_local_storage", {key:"testkey",value:"testvalue"})' + == 'Event("_set_local_storage", {key:"testkey",value:"testvalue"})' ) @@ -254,7 +256,7 @@ def test_clear_local_storage(): assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_clear_local_storage" assert not spec.args - assert format.format_event(spec) == 'E("_clear_local_storage", {})' + assert format.format_event(spec) == 'Event("_clear_local_storage", {})' def test_remove_local_storage(): @@ -264,4 +266,6 @@ def test_remove_local_storage(): assert spec.handler.fn.__qualname__ == "_remove_local_storage" assert spec.args[0][0].equals(Var.create_safe("key")) assert spec.args[0][1].equals(Var.create_safe("testkey")) - assert format.format_event(spec) == 'E("_remove_local_storage", {key:"testkey"})' + assert ( + format.format_event(spec) == 'Event("_remove_local_storage", {key:"testkey"})' + ) diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index 09ca3abdf..e392cc09a 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -312,8 +312,10 @@ def test_format_route(route: str, format_case: bool, expected: bool): r'{{"a": "foo \"{ \"bar\" }\" baz", "b": val}}', ), ( - EventChain(events=[EventSpec(handler=EventHandler(fn=mock_event))]), - '{_e => Event([E("mock_event", {})], _e)}', + EventChain( + events=[EventSpec(handler=EventHandler(fn=mock_event))], args_spec=None + ), + '{_e => addEvents([Event("mock_event", {})], _e)}', ), ( EventChain( @@ -322,9 +324,10 @@ def test_format_route(route: str, format_case: bool, expected: bool): handler=EventHandler(fn=mock_event), args=((Var.create_safe("arg"), EVENT_ARG.target.value),), ) - ] + ], + args_spec=None, ), - '{_e => Event([E("mock_event", {arg:_e.target.value})], _e)}', + '{_e => addEvents([Event("mock_event", {arg:_e.target.value})], _e)}', ), ({"a": "red", "b": "blue"}, '{{"a": "red", "b": "blue"}}'), (BaseVar(name="var", type_="int"), "{var}"),