Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 48 additions & 5 deletions reflex/experimental/client_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import dataclasses
import re
import unicodedata
from collections.abc import Callable
from typing import Any

Expand All @@ -22,6 +22,50 @@
}


def _event_arg_name(value_str: str) -> tuple[str, ...]:
"""Return the event argument name for an event-derived expression."""
if not value_str:
return ()

arg_name = value_str[0]
if not _is_js_identifier_start(arg_name):
return ()

for char in value_str[1:]:
if not _is_js_identifier_part(char):
break
arg_name += char

return (arg_name,)


def _is_js_identifier_start(char: str) -> bool:
"""Return whether a character can start a JavaScript identifier."""
return char in {"$", "_"} or unicodedata.category(char) in {
"Lu",
"Ll",
"Lt",
"Lm",
"Lo",
"Nl",
}


def _is_js_identifier_part(char: str) -> bool:
"""Return whether a character can continue a JavaScript identifier."""
return (
_is_js_identifier_start(char)
or unicodedata.category(char)
in {
"Mn",
"Mc",
"Nd",
"Pc",
}
or char in {"\u200c", "\u200d"}
)


def _client_state_ref(var_name: str) -> Var:
"""Get the ref accessor Var for a ClientStateVar.

Expand Down Expand Up @@ -194,7 +238,7 @@ def create(

@property
def value(self) -> Var:
"""Get a placeholder for the Var.
"""A placeholder for the Var.

This property can only be rendered on the frontend.

Expand Down Expand Up @@ -235,8 +279,7 @@ def set_value(self, value: Any = NoValue) -> Var:
value_str = str(value_var)

setter = ArgsFunctionOperationBuilder.create(
# remove patterns of ["*"] from the value_str using regex
args_names=(re.sub(r"(\?\.)?\[\".*\"\]", "", value_str),)
args_names=_event_arg_name(value_str)
if value_str.startswith("_")
else (),
return_expr=setter.call(value_var),
Expand Down Expand Up @@ -294,4 +337,4 @@ def push(self, value: Any) -> EventSpec:
msg = "ClientStateVar must be global to push the value."
raise ValueError(msg)
value = Var.create(value)
return run_script(f"{_client_state_ref(self._setter_name)}({value})")
return run_script(f"{_client_state_ref(self._setter_name)}({value})")
30 changes: 29 additions & 1 deletion tests/units/test_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,34 @@ def test_event_window_alert():
)


def test_call_function_client_state_setter_accepts_optional_chain_var():
"""Client state setters should use the event object as the function arg."""
from reflex.experimental.client_state import ClientStateVar

last_x = ClientStateVar.create("last_x", default=0)
spec = rx.call_function(last_x.set_value(Var("_event?.clientX")))

assert (
format.format_event(spec)
== 'ReflexEvent("_call_function", {function:((_event) => '
"(refs['_client_state_setLast_x'](_event?.clientX))),callback:null})"
)


def test_call_function_client_state_setter_accepts_unicode_event_var():
"""Client state setters should preserve unicode event argument names."""
from reflex.experimental.client_state import ClientStateVar

last_x = ClientStateVar.create("last_x", default=0)
spec = rx.call_function(last_x.set_value(Var("_événement?.clientX")))

assert (
format.format_event(spec)
== 'ReflexEvent("_call_function", {function:((_événement) => '
"(refs['_client_state_setLast_x'](_événement?.clientX))),callback:null})"
)


@pytest.mark.parametrize(
("func", "qualname"), [("set_focus", "_set_focus"), ("blur_focus", "_blur_focus")]
)
Expand Down Expand Up @@ -1237,4 +1265,4 @@ def f(s: GlobalState, arg: str):
def test_decentralized_event_global_state():
"""Test the decentralized event with a global state."""
_ = rx.input(on_change=f("foo"))
_ = rx.input(on_change=f)
_ = rx.input(on_change=f)