Vardata for rx.Match (#2439)

This commit is contained in:
Elijah Ahianyo 2024-01-24 22:55:49 +00:00 committed by GitHub
parent 3de36be5f4
commit 5f1751acc2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 305 additions and 197 deletions

View File

@ -723,7 +723,7 @@ class Component(BaseComponent, ABC):
vars.append(prop_var)
# Style keeps track of its own VarData instance, so embed in a temp Var that is yielded.
if isinstance(self.style, dict) and self.style or isinstance(self.style, Var):
if self.style:
vars.append(
BaseVar(
_var_name="style",

View File

@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
from reflex.components.base import Fragment
from reflex.components.component import BaseComponent, Component, MemoizationLeaf
from reflex.components.tags import MatchTag, Tag
from reflex.style import Style
from reflex.utils import format, imports, types
from reflex.utils.exceptions import MatchTypeError
from reflex.vars import BaseVar, Var, VarData
@ -94,13 +95,33 @@ class Match(MemoizationLeaf):
if not isinstance(cases[-1], tuple):
default = cases.pop()
default = (
Var.create(default, _var_is_string=type(default) is str)
cls._create_case_var_with_var_data(default)
if not isinstance(default, BaseComponent)
else default
)
return cases, default # type: ignore
@classmethod
def _create_case_var_with_var_data(cls, case_element):
"""Convert a case element into a Var.If the case
is a Style type, we extract the var data and merge it with the
newly created Var.
Args:
case_element: The case element.
Returns:
The case element Var.
"""
_var_data = case_element._var_data if isinstance(case_element, Style) else None # type: ignore
case_element = Var.create(
case_element, _var_is_string=type(case_element) is str
)
if _var_data is not None:
case_element._var_data = VarData.merge(case_element._var_data, _var_data) # type: ignore
return case_element
@classmethod
def _process_match_cases(cls, cases: List) -> List[List[BaseVar]]:
"""Process the individual match cases.
@ -130,7 +151,7 @@ class Match(MemoizationLeaf):
for element in case:
# convert all non component element to vars.
el = (
Var.create(element, _var_is_string=type(element) is str)
cls._create_case_var_with_var_data(element)
if not isinstance(element, BaseComponent)
else element
)
@ -199,6 +220,7 @@ class Match(MemoizationLeaf):
cond=match_cond_var,
match_cases=match_cases,
default=default,
children=[case[-1] for case in match_cases] + [default], # type: ignore
)
)
@ -243,19 +265,8 @@ class Match(MemoizationLeaf):
tag.name = "match"
return dict(tag)
def _get_imports(self):
merged_imports = super()._get_imports()
# Obtain the imports of all components the in match case.
for case in self.match_cases:
if isinstance(case[-1], BaseComponent):
merged_imports = imports.merge_imports(
merged_imports,
case[-1].get_imports(),
)
# Get the import of the default case component.
if isinstance(self.default, BaseComponent):
merged_imports = imports.merge_imports(
merged_imports,
self.default.get_imports(),
)
return merged_imports
def _get_imports(self) -> imports.ImportDict:
return imports.merge_imports(
super()._get_imports(),
getattr(self.cond._var_data, "imports", {}),
)

View File

@ -4,7 +4,6 @@ from __future__ import annotations
from typing import Any, Dict, Literal
from reflex.components.base.fragment import Fragment
from reflex.components.component import Component
from reflex.components.core import cond, match
from reflex.components.radix.primitives.base import RadixPrimitiveComponent
@ -15,7 +14,7 @@ from reflex.style import (
format_as_emotion,
)
from reflex.utils import imports
from reflex.vars import BaseVar, Var
from reflex.vars import BaseVar, Var, VarData
LiteralAccordionType = Literal["single", "multiple"]
LiteralAccordionDir = Literal["ltr", "rtl"]
@ -414,6 +413,9 @@ class AccordionRoot(AccordionComponent):
# dynamic themes of the accordion generated at compile time.
_dynamic_themes: Var[dict]
# The var_data associated with the component.
_var_data: VarData = VarData() # type: ignore
@classmethod
def create(cls, *children, **props) -> Component:
"""Create the Accordion root component.
@ -435,8 +437,7 @@ class AccordionRoot(AccordionComponent):
# mark the vars of variant string literals as strings so they are formatted properly in the match condition.
comp.variant._var_is_string = True # type: ignore
# remove Fragment and cond wrap workaround when https://github.com/reflex-dev/reflex/issues/2393 is resolved.
return Fragment.create(comp, cond(True, Fragment.create()))
return comp
def _get_style(self) -> dict:
"""Get the style for the component.
@ -447,25 +448,39 @@ class AccordionRoot(AccordionComponent):
return {"css": self._dynamic_themes._merge(format_as_emotion(self.style))} # type: ignore
def _apply_theme(self, theme: Component):
accordion_theme_root = get_theme_accordion_root(
variant=self.variant, color_scheme=self.color_scheme
)
accordion_theme_content = get_theme_accordion_content(
variant=self.variant, color_scheme=self.color_scheme
)
accordion_theme_trigger = get_theme_accordion_trigger(
variant=self.variant, color_scheme=self.color_scheme
)
# extract var_data from dynamic themes.
self._var_data = self._var_data.merge( # type: ignore
accordion_theme_trigger._var_data,
accordion_theme_content._var_data,
accordion_theme_root._var_data,
)
self._dynamic_themes = Var.create( # type: ignore
convert_dict_to_style_and_format_emotion(
{
"& .AccordionItem": get_theme_accordion_item(),
"& .AccordionHeader": get_theme_accordion_header(),
"& .AccordionTrigger": get_theme_accordion_trigger(
variant=self.variant, color_scheme=self.color_scheme
),
"& .AccordionContent": get_theme_accordion_content(
variant=self.variant, color_scheme=self.color_scheme
),
"& .AccordionTrigger": accordion_theme_trigger,
"& .AccordionContent": accordion_theme_content,
}
)
)._merge( # type: ignore
get_theme_accordion_root(
variant=self.variant, color_scheme=self.color_scheme
)
accordion_theme_root
)
def _get_imports(self):
return imports.merge_imports(super()._get_imports(), self._var_data.imports)
def get_event_triggers(self) -> Dict[str, Any]:
"""Get the events triggers signatures for the component.

View File

@ -8,7 +8,6 @@ from reflex.vars import Var, BaseVar, ComputedVar
from reflex.event import EventChain, EventHandler, EventSpec
from reflex.style import Style
from typing import Any, Dict, Literal
from reflex.components.base.fragment import Fragment
from reflex.components.component import Component
from reflex.components.core import cond, match
from reflex.components.radix.primitives.base import RadixPrimitiveComponent
@ -19,7 +18,7 @@ from reflex.style import (
format_as_emotion,
)
from reflex.utils import imports
from reflex.vars import BaseVar, Var
from reflex.vars import BaseVar, Var, VarData
LiteralAccordionType = Literal["single", "multiple"]
LiteralAccordionDir = Literal["ltr", "rtl"]
@ -149,6 +148,7 @@ class AccordionRoot(AccordionComponent):
Union[Var[Literal["primary", "accent"]], Literal["primary", "accent"]]
] = None,
_dynamic_themes: Optional[Union[Var[dict], dict]] = None,
_var_data: Optional[VarData] = None,
as_child: Optional[Union[Var[bool], bool]] = None,
style: Optional[Style] = None,
key: Optional[Any] = None,
@ -220,6 +220,7 @@ class AccordionRoot(AccordionComponent):
variant: The variant of the accordion.
color_scheme: The color scheme of the accordion.
_dynamic_themes: dynamic themes of the accordion generated at compile time.
_var_data: The var_data associated with the component.
as_child: Change the default rendered element for the one passed as a child.
style: The style of the component.
key: A unique key for the component.

View File

@ -188,16 +188,19 @@ def _format_emotion_style_pseudo_selector(key: str) -> str:
return key
def format_as_emotion(style_dict: dict[str, Any]) -> dict[str, Any] | None:
def format_as_emotion(style_dict: dict[str, Any]) -> Style | None:
"""Convert the style to an emotion-compatible CSS-in-JS dict.
Args:
style_dict: The style dict to convert.
Returns:
The emotion dict.
The emotion style dict.
"""
emotion_style = {}
_var_data = style_dict._var_data if isinstance(style_dict, Style) else None
emotion_style = Style()
for orig_key, value in style_dict.items():
key = _format_emotion_style_pseudo_selector(orig_key)
if isinstance(value, list):
@ -219,6 +222,8 @@ def format_as_emotion(style_dict: dict[str, Any]) -> dict[str, Any] | None:
else:
emotion_style[key] = value
if emotion_style:
if _var_data is not None:
emotion_style._var_data = VarData.merge(emotion_style._var_data, _var_data)
return emotion_style

View File

@ -234,6 +234,8 @@ def _extract_var_data(value: Iterable) -> list[VarData | None]:
Returns:
The extracted VarDatas.
"""
from reflex.style import Style
var_datas = []
with contextlib.suppress(TypeError):
for sub in value:
@ -245,10 +247,15 @@ def _extract_var_data(value: Iterable) -> list[VarData | None]:
var_datas.extend(_extract_var_data(sub.values()))
# Recurse into iterable values (or dict keys).
var_datas.extend(_extract_var_data(sub))
# Recurse when value is a dict itself.
values = getattr(value, "values", None)
if callable(values):
var_datas.extend(_extract_var_data(values()))
# Style objects should already have _var_data.
if isinstance(value, Style):
var_datas.append(value._var_data)
else:
# Recurse when value is a dict itself.
values = getattr(value, "values", None)
if callable(values):
var_datas.extend(_extract_var_data(values()))
return var_datas
@ -1574,6 +1581,8 @@ class Var:
Returns:
The str var without the wrapped curly braces
"""
from reflex.style import Style
type_ = (
get_origin(self._var_type)
if types.is_generic_alias(self._var_type)
@ -1583,7 +1592,9 @@ class Var:
wrapped_var = str(self)
return (
wrapped_var
if not self._var_state and issubclass(type_, dict)
if not self._var_state
and issubclass(type_, dict)
or issubclass(type_, Style)
else wrapped_var.strip("{}")
)

View File

@ -6,6 +6,7 @@ import pytest
import reflex as rx
from reflex import style
from reflex.style import Style
from reflex.vars import Var
test_style = [
@ -73,50 +74,56 @@ def compare_dict_of_var(d1: dict[str, Any], d2: dict[str, Any]):
("kwargs", "style_dict", "expected_get_style"),
[
({}, {}, {"css": None}),
({"color": "hotpink"}, {}, {"css": Var.create({"color": "hotpink"})}),
({}, {"color": "red"}, {"css": Var.create({"color": "red"})}),
({"color": "hotpink"}, {}, {"css": Var.create(Style({"color": "hotpink"}))}),
({}, {"color": "red"}, {"css": Var.create(Style({"color": "red"}))}),
(
{"color": "hotpink"},
{"color": "red"},
{"css": Var.create({"color": "hotpink"})},
{"css": Var.create(Style({"color": "hotpink"}))},
),
(
{"_hover": {"color": "hotpink"}},
{},
{"css": Var.create({"&:hover": {"color": "hotpink"}})},
{"css": Var.create(Style({"&:hover": {"color": "hotpink"}}))},
),
(
{},
{"_hover": {"color": "red"}},
{"css": Var.create({"&:hover": {"color": "red"}})},
{"css": Var.create(Style({"&:hover": {"color": "red"}}))},
),
(
{},
{":hover": {"color": "red"}},
{"css": Var.create({"&:hover": {"color": "red"}})},
{"css": Var.create(Style({"&:hover": {"color": "red"}}))},
),
(
{},
{"::-webkit-scrollbar": {"display": "none"}},
{"css": Var.create({"&::-webkit-scrollbar": {"display": "none"}})},
{"css": Var.create(Style({"&::-webkit-scrollbar": {"display": "none"}}))},
),
(
{},
{"::-moz-progress-bar": {"background_color": "red"}},
{"css": Var.create({"&::-moz-progress-bar": {"backgroundColor": "red"}})},
{
"css": Var.create(
Style({"&::-moz-progress-bar": {"backgroundColor": "red"}})
)
},
),
(
{"color": ["#111", "#222", "#333", "#444", "#555"]},
{},
{
"css": Var.create(
{
"@media screen and (min-width: 0)": {"color": "#111"},
"@media screen and (min-width: 30em)": {"color": "#222"},
"@media screen and (min-width: 48em)": {"color": "#333"},
"@media screen and (min-width: 62em)": {"color": "#444"},
"@media screen and (min-width: 80em)": {"color": "#555"},
}
Style(
{
"@media screen and (min-width: 0)": {"color": "#111"},
"@media screen and (min-width: 30em)": {"color": "#222"},
"@media screen and (min-width: 48em)": {"color": "#333"},
"@media screen and (min-width: 62em)": {"color": "#444"},
"@media screen and (min-width: 80em)": {"color": "#555"},
}
)
)
},
),
@ -128,14 +135,16 @@ def compare_dict_of_var(d1: dict[str, Any], d2: dict[str, Any]):
{},
{
"css": Var.create(
{
"@media screen and (min-width: 0)": {"color": "#111"},
"@media screen and (min-width: 30em)": {"color": "#222"},
"@media screen and (min-width: 48em)": {"color": "#333"},
"@media screen and (min-width: 62em)": {"color": "#444"},
"@media screen and (min-width: 80em)": {"color": "#555"},
"backgroundColor": "#FFF",
}
Style(
{
"@media screen and (min-width: 0)": {"color": "#111"},
"@media screen and (min-width: 30em)": {"color": "#222"},
"@media screen and (min-width: 48em)": {"color": "#333"},
"@media screen and (min-width: 62em)": {"color": "#444"},
"@media screen and (min-width: 80em)": {"color": "#555"},
"backgroundColor": "#FFF",
}
)
)
},
),
@ -147,85 +156,8 @@ def compare_dict_of_var(d1: dict[str, Any], d2: dict[str, Any]):
{},
{
"css": Var.create(
{
"@media screen and (min-width: 0)": {
"color": "#111",
"backgroundColor": "#FFF",
},
"@media screen and (min-width: 30em)": {
"color": "#222",
"backgroundColor": "#EEE",
},
"@media screen and (min-width: 48em)": {
"color": "#333",
"backgroundColor": "#DDD",
},
"@media screen and (min-width: 62em)": {
"color": "#444",
"backgroundColor": "#CCC",
},
"@media screen and (min-width: 80em)": {
"color": "#555",
"backgroundColor": "#BBB",
},
}
)
},
),
(
{
"_hover": [
{"color": "#111"},
{"color": "#222"},
{"color": "#333"},
{"color": "#444"},
{"color": "#555"},
]
},
{},
{
"css": Var.create(
{
"&:hover": {
"@media screen and (min-width: 0)": {"color": "#111"},
"@media screen and (min-width: 30em)": {"color": "#222"},
"@media screen and (min-width: 48em)": {"color": "#333"},
"@media screen and (min-width: 62em)": {"color": "#444"},
"@media screen and (min-width: 80em)": {"color": "#555"},
}
}
)
},
),
(
{"_hover": {"color": ["#111", "#222", "#333", "#444", "#555"]}},
{},
{
"css": Var.create(
{
"&:hover": {
"@media screen and (min-width: 0)": {"color": "#111"},
"@media screen and (min-width: 30em)": {"color": "#222"},
"@media screen and (min-width: 48em)": {"color": "#333"},
"@media screen and (min-width: 62em)": {"color": "#444"},
"@media screen and (min-width: 80em)": {"color": "#555"},
}
}
)
},
),
(
{
"_hover": {
"color": ["#111", "#222", "#333", "#444", "#555"],
"background_color": ["#FFF", "#EEE", "#DDD", "#CCC", "#BBB"],
}
},
{},
{
"css": Var.create(
{
"&:hover": {
Style(
{
"@media screen and (min-width: 0)": {
"color": "#111",
"backgroundColor": "#FFF",
@ -247,7 +179,108 @@ def compare_dict_of_var(d1: dict[str, Any], d2: dict[str, Any]):
"backgroundColor": "#BBB",
},
}
}
)
)
},
),
(
{
"_hover": [
{"color": "#111"},
{"color": "#222"},
{"color": "#333"},
{"color": "#444"},
{"color": "#555"},
]
},
{},
{
"css": Var.create(
Style(
{
"&:hover": {
"@media screen and (min-width: 0)": {"color": "#111"},
"@media screen and (min-width: 30em)": {
"color": "#222"
},
"@media screen and (min-width: 48em)": {
"color": "#333"
},
"@media screen and (min-width: 62em)": {
"color": "#444"
},
"@media screen and (min-width: 80em)": {
"color": "#555"
},
}
}
)
)
},
),
(
{"_hover": {"color": ["#111", "#222", "#333", "#444", "#555"]}},
{},
{
"css": Var.create(
Style(
{
"&:hover": {
"@media screen and (min-width: 0)": {"color": "#111"},
"@media screen and (min-width: 30em)": {
"color": "#222"
},
"@media screen and (min-width: 48em)": {
"color": "#333"
},
"@media screen and (min-width: 62em)": {
"color": "#444"
},
"@media screen and (min-width: 80em)": {
"color": "#555"
},
}
}
)
)
},
),
(
{
"_hover": {
"color": ["#111", "#222", "#333", "#444", "#555"],
"background_color": ["#FFF", "#EEE", "#DDD", "#CCC", "#BBB"],
}
},
{},
{
"css": Var.create(
Style(
{
"&:hover": {
"@media screen and (min-width: 0)": {
"color": "#111",
"backgroundColor": "#FFF",
},
"@media screen and (min-width: 30em)": {
"color": "#222",
"backgroundColor": "#EEE",
},
"@media screen and (min-width: 48em)": {
"color": "#333",
"backgroundColor": "#DDD",
},
"@media screen and (min-width: 62em)": {
"color": "#444",
"backgroundColor": "#CCC",
},
"@media screen and (min-width: 80em)": {
"color": "#555",
"backgroundColor": "#BBB",
},
}
}
)
)
},
),
@ -261,16 +294,26 @@ def compare_dict_of_var(d1: dict[str, Any], d2: dict[str, Any]):
{},
{
"css": Var.create(
{
"&:hover": {
"@media screen and (min-width: 0)": {"color": "#111"},
"@media screen and (min-width: 30em)": {"color": "#222"},
"@media screen and (min-width: 48em)": {"color": "#333"},
"@media screen and (min-width: 62em)": {"color": "#444"},
"@media screen and (min-width: 80em)": {"color": "#555"},
"backgroundColor": "#FFF",
Style(
{
"&:hover": {
"@media screen and (min-width: 0)": {"color": "#111"},
"@media screen and (min-width: 30em)": {
"color": "#222"
},
"@media screen and (min-width: 48em)": {
"color": "#333"
},
"@media screen and (min-width: 62em)": {
"color": "#444"
},
"@media screen and (min-width: 80em)": {
"color": "#555"
},
"backgroundColor": "#FFF",
}
}
}
)
)
},
),
@ -304,20 +347,26 @@ class StyleState(rx.State):
[
(
{"color": StyleState.color},
{"css": Var.create({"color": StyleState.color})},
{"css": Var.create(Style({"color": StyleState.color}))},
),
(
{"color": f"dark{StyleState.color}"},
{"css": Var.create_safe(f'{{"color": `dark{StyleState.color}`}}').to(dict)},
{
"css": Var.create_safe(f'{{"color": `dark{StyleState.color}`}}').to(
Style
)
},
),
(
{"color": StyleState.color, "_hover": {"color": StyleState.color2}},
{
"css": Var.create(
{
"color": StyleState.color,
"&:hover": {"color": StyleState.color2},
}
Style(
{
"color": StyleState.color,
"&:hover": {"color": StyleState.color2},
}
)
)
},
),
@ -325,15 +374,19 @@ class StyleState(rx.State):
{"color": [StyleState.color, "gray", StyleState.color2, "yellow", "blue"]},
{
"css": Var.create(
{
"@media screen and (min-width: 0)": {"color": StyleState.color},
"@media screen and (min-width: 30em)": {"color": "gray"},
"@media screen and (min-width: 48em)": {
"color": StyleState.color2
},
"@media screen and (min-width: 62em)": {"color": "yellow"},
"@media screen and (min-width: 80em)": {"color": "blue"},
}
Style(
{
"@media screen and (min-width: 0)": {
"color": StyleState.color
},
"@media screen and (min-width: 30em)": {"color": "gray"},
"@media screen and (min-width: 48em)": {
"color": StyleState.color2
},
"@media screen and (min-width: 62em)": {"color": "yellow"},
"@media screen and (min-width: 80em)": {"color": "blue"},
}
)
)
},
),
@ -349,19 +402,27 @@ class StyleState(rx.State):
},
{
"css": Var.create(
{
"&:hover": {
"@media screen and (min-width: 0)": {
"color": StyleState.color
},
"@media screen and (min-width: 30em)": {
"color": StyleState.color2
},
"@media screen and (min-width: 48em)": {"color": "#333"},
"@media screen and (min-width: 62em)": {"color": "#444"},
"@media screen and (min-width: 80em)": {"color": "#555"},
Style(
{
"&:hover": {
"@media screen and (min-width: 0)": {
"color": StyleState.color
},
"@media screen and (min-width: 30em)": {
"color": StyleState.color2
},
"@media screen and (min-width: 48em)": {
"color": "#333"
},
"@media screen and (min-width: 62em)": {
"color": "#444"
},
"@media screen and (min-width: 80em)": {
"color": "#555"
},
}
}
}
)
)
},
),
@ -379,19 +440,27 @@ class StyleState(rx.State):
},
{
"css": Var.create(
{
"&:hover": {
"@media screen and (min-width: 0)": {
"color": StyleState.color
},
"@media screen and (min-width: 30em)": {
"color": StyleState.color2
},
"@media screen and (min-width: 48em)": {"color": "#333"},
"@media screen and (min-width: 62em)": {"color": "#444"},
"@media screen and (min-width: 80em)": {"color": "#555"},
Style(
{
"&:hover": {
"@media screen and (min-width: 0)": {
"color": StyleState.color
},
"@media screen and (min-width: 30em)": {
"color": StyleState.color2
},
"@media screen and (min-width: 48em)": {
"color": "#333"
},
"@media screen and (min-width: 62em)": {
"color": "#444"
},
"@media screen and (min-width: 80em)": {
"color": "#555"
},
}
}
}
)
)
},
),
@ -410,9 +479,5 @@ def test_style_via_component_with_state(
comp = rx.el.div(**kwargs)
assert comp.style._var_data == expected_get_style["css"]._var_data
# Remove the _var_data from the expected style, since the emotion-formatted
# style dict won't actually have it.
expected_get_style["css"]._var_data = None
# Assert that style values are equal.
compare_dict_of_var(comp._get_style(), expected_get_style)