
This ensures that the value is properly escaped/formatted for direct use in javascript code.
247 lines
7.9 KiB
Python
247 lines
7.9 KiB
Python
"""Handle client side state with `useState`."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import re
|
|
import sys
|
|
from typing import Any, Callable, Union
|
|
|
|
from reflex import constants
|
|
from reflex.event import EventChain, EventHandler, EventSpec, run_script
|
|
from reflex.utils.imports import ImportVar
|
|
from reflex.vars import VarData, get_unique_variable_name
|
|
from reflex.vars.base import LiteralVar, Var
|
|
from reflex.vars.function import FunctionVar
|
|
|
|
NoValue = object()
|
|
|
|
|
|
_refs_import = {
|
|
f"$/{constants.Dirs.STATE_PATH}": [ImportVar(tag="refs")],
|
|
}
|
|
|
|
|
|
def _client_state_ref(var_name: str) -> str:
|
|
"""Get the ref path for a ClientStateVar.
|
|
|
|
Args:
|
|
var_name: The name of the variable.
|
|
|
|
Returns:
|
|
An accessor for ClientStateVar ref as a string.
|
|
"""
|
|
return f"refs['_client_state_{var_name}']"
|
|
|
|
|
|
@dataclasses.dataclass(
|
|
eq=False,
|
|
frozen=True,
|
|
**{"slots": True} if sys.version_info >= (3, 10) else {},
|
|
)
|
|
class ClientStateVar(Var):
|
|
"""A Var that exists on the client via useState."""
|
|
|
|
# Track the names of the getters and setters
|
|
_setter_name: str = dataclasses.field(default="")
|
|
_getter_name: str = dataclasses.field(default="")
|
|
|
|
# Whether to add the var and setter to the global `refs` object for use in any Component.
|
|
_global_ref: bool = dataclasses.field(default=True)
|
|
|
|
def __hash__(self) -> int:
|
|
"""Define a hash function for a var.
|
|
|
|
Returns:
|
|
The hash of the var.
|
|
"""
|
|
return hash(
|
|
(self._js_expr, str(self._var_type), self._getter_name, self._setter_name)
|
|
)
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
var_name: str | None = None,
|
|
default: Any = NoValue,
|
|
global_ref: bool = True,
|
|
) -> "ClientStateVar":
|
|
"""Create a local_state Var that can be accessed and updated on the client.
|
|
|
|
The `ClientStateVar` should be included in the highest parent component
|
|
that contains the components which will access and manipulate the client
|
|
state. It has no visual rendering, including it ensures that the
|
|
`useState` hook is called in the correct scope.
|
|
|
|
To render the var in a component, use the `value` property.
|
|
|
|
To update the var in a component, use the `set` property or `set_value` method.
|
|
|
|
To access the var in an event handler, use the `retrieve` method with
|
|
`callback` set to the event handler which should receive the value.
|
|
|
|
To update the var in an event handler, use the `push` method with the
|
|
value to update.
|
|
|
|
Args:
|
|
var_name: The name of the variable.
|
|
default: The default value of the variable.
|
|
global_ref: Whether the state should be accessible in any Component and on the backend.
|
|
|
|
Raises:
|
|
ValueError: If the var_name is not a string.
|
|
|
|
Returns:
|
|
ClientStateVar
|
|
"""
|
|
if var_name is None:
|
|
var_name = get_unique_variable_name()
|
|
if not isinstance(var_name, str):
|
|
raise ValueError("var_name must be a string.")
|
|
if default is NoValue:
|
|
default_var = Var(_js_expr="")
|
|
elif not isinstance(default, Var):
|
|
default_var = LiteralVar.create(default)
|
|
else:
|
|
default_var = default
|
|
setter_name = f"set{var_name.capitalize()}"
|
|
hooks = {
|
|
f"const [{var_name}, {setter_name}] = useState({default_var!s})": None,
|
|
}
|
|
imports = {
|
|
"react": [ImportVar(tag="useState")],
|
|
}
|
|
if global_ref:
|
|
hooks[f"{_client_state_ref(var_name)} = {var_name}"] = None
|
|
hooks[f"{_client_state_ref(setter_name)} = {setter_name}"] = None
|
|
imports.update(_refs_import)
|
|
return cls(
|
|
_js_expr="",
|
|
_setter_name=setter_name,
|
|
_getter_name=var_name,
|
|
_global_ref=global_ref,
|
|
_var_type=default_var._var_type,
|
|
_var_data=VarData.merge(
|
|
default_var._var_data,
|
|
VarData(
|
|
hooks=hooks,
|
|
imports=imports,
|
|
),
|
|
),
|
|
)
|
|
|
|
@property
|
|
def value(self) -> Var:
|
|
"""Get a placeholder for the Var.
|
|
|
|
This property can only be rendered on the frontend.
|
|
|
|
To access the value in a backend event handler, see `retrieve`.
|
|
|
|
Returns:
|
|
an accessor for the client state variable.
|
|
"""
|
|
return (
|
|
Var(
|
|
_js_expr=(
|
|
_client_state_ref(self._getter_name)
|
|
if self._global_ref
|
|
else self._getter_name
|
|
)
|
|
)
|
|
.to(self._var_type)
|
|
._replace(
|
|
merge_var_data=VarData( # type: ignore
|
|
imports=_refs_import if self._global_ref else {}
|
|
)
|
|
)
|
|
)
|
|
|
|
def set_value(self, value: Any = NoValue) -> Var:
|
|
"""Set the value of the client state variable.
|
|
|
|
This property can only be attached to a frontend event trigger.
|
|
|
|
To set a value from a backend event handler, see `push`.
|
|
|
|
Args:
|
|
value: The value to set.
|
|
|
|
Returns:
|
|
A special EventChain Var which will set the value when triggered.
|
|
"""
|
|
setter = (
|
|
_client_state_ref(self._setter_name)
|
|
if self._global_ref
|
|
else self._setter_name
|
|
)
|
|
_var_data = VarData(imports=_refs_import if self._global_ref else {})
|
|
if value is not NoValue:
|
|
# This is a hack to make it work like an EventSpec taking an arg
|
|
value_var = LiteralVar.create(value)
|
|
_var_data = VarData.merge(_var_data, value_var._get_all_var_data())
|
|
value_str = str(value_var)
|
|
|
|
if value_str.startswith("_"):
|
|
# remove patterns of ["*"] from the value_str using regex
|
|
arg = re.sub(r"\[\".*\"\]", "", value_str)
|
|
setter = f"(({arg}) => {setter}({value_str}))"
|
|
else:
|
|
setter = f"(() => {setter}({value_str}))"
|
|
return Var(
|
|
_js_expr=setter,
|
|
_var_data=_var_data,
|
|
).to(FunctionVar, EventChain)
|
|
|
|
@property
|
|
def set(self) -> Var:
|
|
"""Set the value of the client state variable.
|
|
|
|
This property can only be attached to a frontend event trigger.
|
|
|
|
To set a value from a backend event handler, see `push`.
|
|
|
|
Returns:
|
|
A special EventChain Var which will set the value when triggered.
|
|
"""
|
|
return self.set_value()
|
|
|
|
def retrieve(
|
|
self, callback: Union[EventHandler, Callable, None] = None
|
|
) -> EventSpec:
|
|
"""Pass the value of the client state variable to a backend EventHandler.
|
|
|
|
The event handler must `yield` or `return` the EventSpec to trigger the event.
|
|
|
|
Args:
|
|
callback: The callback to pass the value to.
|
|
|
|
Returns:
|
|
An EventSpec which will retrieve the value when triggered.
|
|
|
|
Raises:
|
|
ValueError: If the ClientStateVar is not global.
|
|
"""
|
|
if not self._global_ref:
|
|
raise ValueError("ClientStateVar must be global to retrieve the value.")
|
|
return run_script(_client_state_ref(self._getter_name), callback=callback)
|
|
|
|
def push(self, value: Any) -> EventSpec:
|
|
"""Push a value to the client state variable from the backend.
|
|
|
|
The event handler must `yield` or `return` the EventSpec to trigger the event.
|
|
|
|
Args:
|
|
value: The value to update.
|
|
|
|
Returns:
|
|
An EventSpec which will push the value when triggered.
|
|
|
|
Raises:
|
|
ValueError: If the ClientStateVar is not global.
|
|
"""
|
|
if not self._global_ref:
|
|
raise ValueError("ClientStateVar must be global to push the value.")
|
|
value = Var.create(value)
|
|
return run_script(f"{_client_state_ref(self._setter_name)}({value})")
|