Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New API to define triggers #1820

Merged
merged 24 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5bc257e
wip lambda triggers
Lendemor Sep 14, 2023
56df3c3
cleaned up new API for triggers
Lendemor Sep 15, 2023
974ca81
Merge branch 'main' into lendemor/improve_triggers_api
Lendemor Sep 15, 2023
93a9905
removed some prints
Lendemor Sep 15, 2023
7ee7b9e
remove copytoclipboard and updated pyi
Lendemor Sep 15, 2023
b97daf5
Merge branch 'main' into lendemor/improve_triggers_api
Lendemor Sep 18, 2023
21335f5
rename Event to addEvents and E to Event+ add default arg for argspec…
Lendemor Sep 18, 2023
a7e0875
add retrocompatibility with get_triggers and deprecation message
Lendemor Sep 18, 2023
de687ba
fix docstring
Lendemor Sep 18, 2023
e25ac36
update pyi
Lendemor Sep 18, 2023
b3bf8ac
remove duplicate constant
Lendemor Sep 18, 2023
c3f09d3
remove print statement
Lendemor Sep 18, 2023
0941940
fix mount/unmount
Lendemor Sep 18, 2023
bbb9374
fix upload
Lendemor Sep 18, 2023
a56bb75
add __future__ annotations where needed
Lendemor Sep 18, 2023
8379a44
fix annotations
Lendemor Sep 18, 2023
cd2a01d
missed some types that need fixing
Lendemor Sep 18, 2023
38a6e4a
fix lambdas, typing pyright and stuff
Lendemor Sep 18, 2023
072f831
fix test_script
Lendemor Sep 18, 2023
020c661
allow arbitrary types for args spec in event triggers
Lendemor Sep 19, 2023
ceef66d
remove commented code
Lendemor Sep 19, 2023
f0fc064
fix tests because of arg renaming
Lendemor Sep 19, 2023
9af5b0f
fix f-string
Lendemor Sep 20, 2023
2450e1a
Merge branch 'main' into lendemor/improve_triggers_api
Lendemor Sep 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions reflex/.templates/jinja/web/pages/index.js.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions reflex/.templates/jinja/web/utils/context.js.jinja2
Original file line number Diff line number Diff line change
@@ -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)),
]
4 changes: 2 additions & 2 deletions reflex/.templates/web/pages/_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ const GlobalStyles = css`
`;

function EventLoopProvider({ children }) {
const [state, Event, connectError] = useEventLoop(
const [state, addEvents, connectError] = useEventLoop(
initialState,
initialEvents,
clientStorage,
)
return (
<EventLoopContext.Provider value={[Event, connectError]}>
<EventLoopContext.Provider value={[addEvents, connectError]}>
<StateContext.Provider value={state}>
{children}
</StateContext.Provider>
Expand Down
12 changes: 6 additions & 6 deletions reflex/.templates/web/utils/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
};

Expand Down Expand Up @@ -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 = (
Expand All @@ -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)
}
Expand All @@ -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])
Expand All @@ -488,7 +488,7 @@ export const useEventLoop = (
}
})()
})
return [state, Event, connectError]
return [state, addEvents, connectError]
}

/***
Expand Down
2 changes: 1 addition & 1 deletion reflex/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"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"),
Expand Down
1 change: 0 additions & 1 deletion reflex/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@
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
Expand Down
11 changes: 9 additions & 2 deletions reflex/components/base/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"""
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
Expand Down Expand Up @@ -57,13 +59,18 @@ def create(cls, *children, **props) -> 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]:
Expand Down
121 changes: 84 additions & 37 deletions reflex/components/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@
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,
Expand All @@ -21,7 +20,7 @@
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, NoRenderImportVar, Var


Expand Down Expand Up @@ -126,7 +125,7 @@ def __init__(self, *args, **kwargs):

# 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.
Expand Down Expand Up @@ -220,36 +219,36 @@ def _create_event_chain(
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):
if value.type_ is not EventChain:
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:
Lendemor marked this conversation as resolved.
Show resolved Hide resolved
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)
Expand All @@ -258,45 +257,94 @@ def _create_event_chain(
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
]
# if is_controlled_event:
Lendemor marked this conversation as resolved.
Show resolved Hide resolved
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="get_controlled_triggers ({self.__class__.__name__})",
Lendemor marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down Expand Up @@ -434,7 +482,6 @@ def render(self) -> Dict:
The dictionary for template of component.
"""
tag = self._render()

rendered_dict = dict(
tag.add_props(
**self.event_triggers,
Expand Down Expand Up @@ -572,8 +619,8 @@ def _get_mount_lifecycle_hook(self) -> str | None:
"""
# 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:
Expand Down
1 change: 0 additions & 1 deletion reflex/components/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
ColorModeSwitch,
color_mode_cond,
)
from .copytoclipboard import CopyToClipboard
from .date_picker import DatePicker
from .date_time_picker import DateTimePicker
from .debounce import DebounceInput
Expand Down
10 changes: 6 additions & 4 deletions reflex/components/forms/checkbox.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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]]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could define a type Callable[Var, List[Var]] for the lambda type and use that instead of Any. Also, when would Var be the dictionary value type, wouldn't it always be a Callable?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put Any because my previous definition using LambdaTypes was causing a lot of pyright errors.

Also the Union[Var, ...] is for backward compatibility until removal of deprecated features.

"""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],
}


Expand Down
Loading
Loading