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

components as literal vars #4223

Merged
merged 23 commits into from
Oct 30, 2024
Merged
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
12 changes: 4 additions & 8 deletions reflex/.templates/jinja/web/pages/utils.js.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,10 @@
{# component: component dictionary #}
{% macro render_tag(component) %}
<{{component.name}} {{- render_props(component.props) }}>
{%- if component.args is not none -%}
{{- render_arg_content(component) }}
{%- else -%}
{{ component.contents }}
{% for child in component.children %}
{{ render(child) }}
{% endfor %}
{%- endif -%}
{{ component.contents }}
{% for child in component.children %}
{{ render(child) }}
{% endfor %}
</{{component.name}}>
{%- endmacro %}

Expand Down
4 changes: 1 addition & 3 deletions reflex/.templates/web/utils/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
} from "$/utils/context.js";
import debounce from "$/utils/helpers/debounce";
import throttle from "$/utils/helpers/throttle";
import * as Babel from "@babel/standalone";

// Endpoint URLs.
const EVENTURL = env.EVENT;
Expand Down Expand Up @@ -139,8 +138,7 @@ export const evalReactComponent = async (component) => {
if (!window.React && window.__reflex) {
window.React = window.__reflex.react;
}
const output = Babel.transform(component, { presets: ["react"] }).code;
const encodedJs = encodeURIComponent(output);
const encodedJs = encodeURIComponent(component);
const dataUri = "data:text/javascript;charset=utf-8," + encodedJs;
const module = await eval(`import(dataUri)`);
return module.default;
Expand Down
75 changes: 72 additions & 3 deletions reflex/components/base/bare.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

from typing import Any, Iterator

from reflex.components.component import Component
from reflex.components.component import Component, LiteralComponentVar
from reflex.components.tags import Tag
from reflex.components.tags.tagless import Tagless
from reflex.vars import ArrayVar, BooleanVar, ObjectVar, Var
from reflex.utils.imports import ParsedImportDict
from reflex.vars import BooleanVar, ObjectVar, Var


class Bare(Component):
Expand All @@ -31,9 +32,77 @@ def create(cls, contents: Any) -> Component:
contents = str(contents) if contents is not None else ""
return cls(contents=contents) # type: ignore

def _get_all_hooks_internal(self) -> dict[str, None]:
"""Include the hooks for the component.

Returns:
The hooks for the component.
"""
hooks = super()._get_all_hooks_internal()
if isinstance(self.contents, LiteralComponentVar):
hooks |= self.contents._var_value._get_all_hooks_internal()
return hooks

def _get_all_hooks(self) -> dict[str, None]:
"""Include the hooks for the component.

Returns:
The hooks for the component.
"""
hooks = super()._get_all_hooks()
if isinstance(self.contents, LiteralComponentVar):
hooks |= self.contents._var_value._get_all_hooks()
return hooks

def _get_all_imports(self) -> ParsedImportDict:
"""Include the imports for the component.

Returns:
The imports for the component.
"""
imports = super()._get_all_imports()
if isinstance(self.contents, LiteralComponentVar):
var_data = self.contents._get_all_var_data()
if var_data:
imports |= {k: list(v) for k, v in var_data.imports}
return imports

def _get_all_dynamic_imports(self) -> set[str]:
"""Get dynamic imports for the component.

Returns:
The dynamic imports.
"""
dynamic_imports = super()._get_all_dynamic_imports()
if isinstance(self.contents, LiteralComponentVar):
dynamic_imports |= self.contents._var_value._get_all_dynamic_imports()
return dynamic_imports

def _get_all_custom_code(self) -> set[str]:
"""Get custom code for the component.

Returns:
The custom code.
"""
custom_code = super()._get_all_custom_code()
if isinstance(self.contents, LiteralComponentVar):
custom_code |= self.contents._var_value._get_all_custom_code()
return custom_code

def _get_all_refs(self) -> set[str]:
"""Get the refs for the children of the component.

Returns:
The refs for the children.
"""
refs = super()._get_all_refs()
if isinstance(self.contents, LiteralComponentVar):
refs |= self.contents._var_value._get_all_refs()
return refs

def _render(self) -> Tag:
if isinstance(self.contents, Var):
if isinstance(self.contents, (BooleanVar, ObjectVar, ArrayVar)):
if isinstance(self.contents, (BooleanVar, ObjectVar)):
return Tagless(contents=f"{{{str(self.contents.to_string())}}}")
return Tagless(contents=f"{{{str(self.contents)}}}")
return Tagless(contents=str(self.contents))
Expand Down
211 changes: 210 additions & 1 deletion reflex/components/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import copy
import dataclasses
import typing
from abc import ABC, abstractmethod
from functools import lru_cache, wraps
Expand Down Expand Up @@ -59,7 +60,15 @@
parse_imports,
)
from reflex.vars import VarData
from reflex.vars.base import LiteralVar, Var
from reflex.vars.base import (
CachedVarOperation,
LiteralVar,
Var,
cached_property_no_lock,
)
from reflex.vars.function import ArgsFunctionOperation, FunctionStringVar
from reflex.vars.number import ternary_operation
from reflex.vars.object import ObjectVar
from reflex.vars.sequence import LiteralArrayVar


Expand Down Expand Up @@ -2345,3 +2354,203 @@ def create(cls, *children, **props) -> Component:


load_dynamic_serializer()


class ComponentVar(Var[Component], python_types=BaseComponent):
"""A Var that represents a Component."""


def empty_component() -> Component:
"""Create an empty component.

Returns:
An empty component.
"""
from reflex.components.base.bare import Bare

return Bare.create("")


def render_dict_to_var(tag: dict | Component | str, imported_names: set[str]) -> Var:
Copy link
Collaborator

Choose a reason for hiding this comment

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

it would be nice if we could move some of this special specific logic closer to the components that will actually depend on it

Copy link
Member Author

Choose a reason for hiding this comment

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

at the current stage there's no standardization of the dict format, so the checks are very tight to what we have right now, i can restructure the rendering logic in the future if needed

"""Convert a render dict to a Var.

Args:
tag: The render dict.
imported_names: The names of the imported components.

Returns:
The Var.
"""
if not isinstance(tag, dict):
if isinstance(tag, Component):
return render_dict_to_var(tag.render(), imported_names)
return Var.create(tag)

if "iterable" in tag:
function_return = Var.create(
[
render_dict_to_var(child.render(), imported_names)
for child in tag["children"]
]
)

func = ArgsFunctionOperation.create(
(tag["arg_var_name"], tag["index_var_name"]),
function_return,
)

return FunctionStringVar.create("Array.prototype.map.call").call(
tag["iterable"]
if not isinstance(tag["iterable"], ObjectVar)
else tag["iterable"].items(),
func,
)

if tag["name"] == "match":
element = tag["cond"]

conditionals = tag["default"]

for case in tag["match_cases"][::-1]:
condition = case[0].to_string() == element.to_string()
for pattern in case[1:-1]:
condition = condition | (pattern.to_string() == element.to_string())

conditionals = ternary_operation(
condition,
case[-1],
conditionals,
)

return conditionals

if "cond" in tag:
return ternary_operation(
tag["cond"],
render_dict_to_var(tag["true_value"], imported_names),
render_dict_to_var(tag["false_value"], imported_names)
if tag["false_value"] is not None
else Var.create(None),
)

props = {}

special_props = []

for prop_str in tag["props"]:
if "=" not in prop_str:
special_props.append(Var(prop_str).to(ObjectVar))
continue
prop = prop_str.index("=")
key = prop_str[:prop]
value = prop_str[prop + 2 : -1]
props[key] = value

props = Var.create({Var.create(k): Var(v) for k, v in props.items()})

for prop in special_props:
props = props.merge(prop)

contents = tag["contents"][1:-1] if tag["contents"] else None

raw_tag_name = tag.get("name")
tag_name = Var(raw_tag_name or "Fragment")

tag_name = (
Var.create(raw_tag_name)
if raw_tag_name
and raw_tag_name.split(".")[0] not in imported_names
and raw_tag_name.lower() == raw_tag_name
else tag_name
)

return FunctionStringVar.create(
"jsx",
).call(
tag_name,
props,
*([Var(contents)] if contents is not None else []),
*[render_dict_to_var(child, imported_names) for child in tag["children"]],
)


@dataclasses.dataclass(
eq=False,
frozen=True,
)
class LiteralComponentVar(CachedVarOperation, LiteralVar, ComponentVar):
"""A Var that represents a Component."""

_var_value: BaseComponent = dataclasses.field(default_factory=empty_component)

@cached_property_no_lock
def _cached_var_name(self) -> str:
"""Get the name of the var.

Returns:
The name of the var.
"""
var_data = self._get_all_var_data()
if var_data is not None:
# flatten imports
imported_names = {j.alias or j.name for i in var_data.imports for j in i[1]}
else:
imported_names = set()
return str(render_dict_to_var(self._var_value.render(), imported_names))

@cached_property_no_lock
def _cached_get_all_var_data(self) -> VarData | None:
"""Get the VarData for the var.

Returns:
The VarData for the var.
"""
return VarData.merge(
VarData(
imports={
"@emotion/react": [
ImportVar(tag="jsx"),
],
}
),
VarData(
imports=self._var_value._get_all_imports(),
),
VarData(
imports={
"react": [
ImportVar(tag="Fragment"),
],
}
),
)

def __hash__(self) -> int:
"""Get the hash of the var.

Returns:
The hash of the var.
"""
return hash((self.__class__.__name__, self._js_expr))

@classmethod
def create(
cls,
value: Component,
_var_data: VarData | None = None,
):
"""Create a var from a value.

Args:
value: The value of the var.
_var_data: Additional hooks and imports associated with the Var.

Returns:
The var.
"""
return LiteralComponentVar(
_js_expr="",
_var_type=type(value),
_var_data=_var_data,
_var_value=value,
)
Loading
Loading