reflex/reflex/components/core/cond.py
Masen Furer 958c4fa7f2
Abstract color_mode related Var creation (#3533)
* StatefulComponent event handler useCallback dependency array

Ensure the useCallback dependency array for event handlers also handled
destructuring objects and the `...` rest parameter.

Avoid hooks that are not variable declarations.

* Abstract color_mode related Var creation

* Allow `set_color_mode` to take a parameter at compile time
* Update type hinting of `Var._replace` to indicate that it returns BaseVar

* color_mode_button with allow_system=True uses new set_color_mode API

`set_color_mode` is now a CallableVar and uses very similar logic internally,
so this bit of code can be replaced.

* Fix for pydantic v1.10.17
2024-06-20 16:46:48 -07:00

212 lines
6.2 KiB
Python

"""Create a list of components from an iterable."""
from __future__ import annotations
from typing import Any, Dict, Optional, Union, overload
from reflex.components.base.fragment import Fragment
from reflex.components.component import BaseComponent, Component, MemoizationLeaf
from reflex.components.tags import CondTag, Tag
from reflex.constants import Dirs
from reflex.constants.colors import Color
from reflex.style import LIGHT_COLOR_MODE, resolved_color_mode
from reflex.utils import format
from reflex.utils.imports import ImportDict, ImportVar
from reflex.vars import BaseVar, Var, VarData
_IS_TRUE_IMPORT: ImportDict = {
f"/{Dirs.STATE_PATH}": [ImportVar(tag="isTrue")],
}
class Cond(MemoizationLeaf):
"""Render one of two components based on a condition."""
# The cond to determine which component to render.
cond: Var[Any]
# The component to render if the cond is true.
comp1: BaseComponent = None # type: ignore
# The component to render if the cond is false.
comp2: BaseComponent = None # type: ignore
@classmethod
def create(
cls,
cond: Var,
comp1: BaseComponent,
comp2: Optional[BaseComponent] = None,
) -> Component:
"""Create a conditional component.
Args:
cond: The cond to determine which component to render.
comp1: The component to render if the cond is true.
comp2: The component to render if the cond is false.
Returns:
The conditional component.
"""
# Wrap everything in fragments.
if comp1.__class__.__name__ != "Fragment":
comp1 = Fragment.create(comp1)
if comp2 is None or comp2.__class__.__name__ != "Fragment":
comp2 = Fragment.create(comp2) if comp2 else Fragment.create()
return Fragment.create(
cls(
cond=cond,
comp1=comp1,
comp2=comp2,
children=[comp1, comp2],
)
)
def _get_props_imports(self):
"""Get the imports needed for component's props.
Returns:
The imports for the component's props of the component.
"""
return []
def _render(self) -> Tag:
return CondTag(
cond=self.cond,
true_value=self.comp1.render(),
false_value=self.comp2.render(),
)
def render(self) -> Dict:
"""Render the component.
Returns:
The dictionary for template of component.
"""
tag = self._render()
return dict(
tag.add_props(
**self.event_triggers,
key=self.key,
sx=self.style,
id=self.id,
class_name=self.class_name,
).set(
props=tag.format_props(),
),
cond_state=f"isTrue({self.cond._var_full_name})",
)
def add_imports(self) -> ImportDict:
"""Add imports for the Cond component.
Returns:
The import dict for the component.
"""
cond_imports: dict[str, str | ImportVar | list[str | ImportVar]] = getattr(
self.cond._var_data, "imports", {}
)
return {**cond_imports, **_IS_TRUE_IMPORT}
@overload
def cond(condition: Any, c1: Component, c2: Any) -> Component: ...
@overload
def cond(condition: Any, c1: Component) -> Component: ...
@overload
def cond(condition: Any, c1: Any, c2: Any) -> BaseVar: ...
def cond(condition: Any, c1: Any, c2: Any = None):
"""Create a conditional component or Prop.
Args:
condition: The cond to determine which component to render.
c1: The component or prop to render if the cond_var is true.
c2: The component or prop to render if the cond_var is false.
Returns:
The conditional component.
Raises:
ValueError: If the arguments are invalid.
"""
var_datas: list[VarData | None] = [
VarData( # type: ignore
imports=_IS_TRUE_IMPORT,
),
]
# Convert the condition to a Var.
cond_var = Var.create(condition)
assert cond_var is not None, "The condition must be set."
# If the first component is a component, create a Cond component.
if isinstance(c1, BaseComponent):
assert c2 is None or isinstance(
c2, BaseComponent
), "Both arguments must be components."
return Cond.create(cond_var, c1, c2)
if isinstance(c1, Var):
var_datas.append(c1._var_data)
# Otherwise, create a conditional Var.
# Check that the second argument is valid.
if isinstance(c2, BaseComponent):
raise ValueError("Both arguments must be props.")
if c2 is None:
raise ValueError("For conditional vars, the second argument must be set.")
if isinstance(c2, Var):
var_datas.append(c2._var_data)
def create_var(cond_part):
return Var.create_safe(
cond_part,
_var_is_string=isinstance(cond_part, (str, Color)),
)
# convert the truth and false cond parts into vars so the _var_data can be obtained.
c1 = create_var(c1)
c2 = create_var(c2)
var_datas.extend([c1._var_data, c2._var_data])
c1_type = c1._var_type if isinstance(c1, Var) else type(c1)
c2_type = c2._var_type if isinstance(c2, Var) else type(c2)
var_type = c1_type if c1_type == c2_type else Union[c1_type, c2_type]
# Create the conditional var.
return cond_var._replace(
_var_name=format.format_cond(
cond=cond_var._var_full_name,
true_value=c1,
false_value=c2,
is_prop=True,
),
_var_type=var_type,
_var_is_local=False,
_var_full_name_needs_state_prefix=False,
merge_var_data=VarData.merge(*var_datas),
)
def color_mode_cond(light: Any, dark: Any = None) -> Var | Component:
"""Create a component or Prop based on color_mode.
Args:
light: The component or prop to render if color_mode is default
dark: The component or prop to render if color_mode is non-default
Returns:
The conditional component or prop.
"""
return cond(
resolved_color_mode == Var.create(LIGHT_COLOR_MODE, _var_is_string=True),
light,
dark,
)