diff --git a/pynecone/components/component.py b/pynecone/components/component.py index 86342df94..1540b02ca 100644 --- a/pynecone/components/component.py +++ b/pynecone/components/component.py @@ -94,6 +94,13 @@ class Component(Base, ABC): Raises: TypeError: If an invalid prop is passed. """ + # Set the id and children initially. + initial_kwargs = { + "id": kwargs.get("id"), + "children": kwargs.get("children", []), + } + super().__init__(**initial_kwargs) + # Get the component fields, triggers, and props. fields = self.get_fields() triggers = self.get_triggers() @@ -264,17 +271,15 @@ class Component(Base, ABC): events=events, state_name=state_name, full_control=full_control ) - @classmethod - def get_triggers(cls) -> Set[str]: + def get_triggers(self) -> Set[str]: """Get the event triggers for the component. Returns: The event triggers. """ - return EVENT_TRIGGERS | set(cls.get_controlled_triggers()) + return EVENT_TRIGGERS | set(self.get_controlled_triggers()) - @classmethod - def get_controlled_triggers(cls) -> Dict[str, Var]: + def get_controlled_triggers(self) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. Returns: @@ -488,7 +493,7 @@ class Component(Base, ABC): # Add the hook code for the children. for child in self.children: - code.update(child.get_hooks()) + code |= child.get_hooks() return code @@ -502,6 +507,20 @@ class Component(Base, ABC): return None return format.format_ref(self.id) + def get_refs(self) -> Set[str]: + """Get the refs for the children of the component. + + Returns: + The refs for the children. + """ + refs = set() + ref = self.get_ref() + if ref is not None: + refs.add(ref) + for child in self.children: + refs |= child.get_refs() + return refs + def get_custom_components( self, seen: Optional[Set[str]] = None ) -> Set[CustomComponent]: @@ -565,7 +584,7 @@ class CustomComponent(Component): library = f"/{constants.COMPONENTS_PATH}" # The function that creates the component. - component_fn: Callable[..., Component] + component_fn: Callable[..., Component] = Component.create # The props of the component. props: Dict[str, Any] = {} diff --git a/pynecone/components/forms/checkbox.py b/pynecone/components/forms/checkbox.py index 495a8275d..1eb354701 100644 --- a/pynecone/components/forms/checkbox.py +++ b/pynecone/components/forms/checkbox.py @@ -48,8 +48,7 @@ class Checkbox(ChakraComponent): # The spacing between the checkbox and its label text (0.5rem) spacing: Var[str] - @classmethod - def get_controlled_triggers(cls) -> Dict[str, Var]: + def get_controlled_triggers(self) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. Returns: diff --git a/pynecone/components/forms/copytoclipboard.py b/pynecone/components/forms/copytoclipboard.py index 28334a156..a737b4ca9 100644 --- a/pynecone/components/forms/copytoclipboard.py +++ b/pynecone/components/forms/copytoclipboard.py @@ -16,8 +16,7 @@ class CopyToClipboard(Component): # The text to copy when clicked. text: Var[str] - @classmethod - def get_controlled_triggers(cls) -> Set[str]: + def get_controlled_triggers(self) -> Set[str]: """Get the event triggers that pass the component's value to the handler. Returns: diff --git a/pynecone/components/forms/editable.py b/pynecone/components/forms/editable.py index 58b2febec..3dcc0fdc7 100644 --- a/pynecone/components/forms/editable.py +++ b/pynecone/components/forms/editable.py @@ -36,8 +36,7 @@ class Editable(ChakraComponent): # The initial value of the Editable in both edit and preview mode. default_value: Var[str] - @classmethod - def get_controlled_triggers(cls) -> Dict[str, Var]: + def get_controlled_triggers(self) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. Returns: diff --git a/pynecone/components/forms/formcontrol.py b/pynecone/components/forms/formcontrol.py index cbb3d6388..7410a95f7 100644 --- a/pynecone/components/forms/formcontrol.py +++ b/pynecone/components/forms/formcontrol.py @@ -1,6 +1,6 @@ """Form components.""" -from typing import Set +from typing import Dict from pynecone.components.component import Component from pynecone.components.libs.chakra import ChakraComponent @@ -14,14 +14,19 @@ class Form(ChakraComponent): as_: Var[str] = "form" # type: ignore - @classmethod - def get_triggers(cls) -> Set[str]: - """Get the event triggers for the component. + def get_controlled_triggers(self) -> Dict[str, Dict]: + """Get the event triggers that pass the component's value to the handler. Returns: - The event triggers. + A dict mapping the event trigger to the var that is passed to the handler. """ - return super().get_triggers() | {"on_submit"} + # Send all the input refs to the handler. + return { + "on_submit": { + ref[4:]: Var.create(f"{ref}.current.value", is_local=False) + for ref in self.get_refs() + } + } class FormControl(ChakraComponent): @@ -52,7 +57,7 @@ class FormControl(ChakraComponent): input=None, help_text=None, error_message=None, - **props + **props, ) -> Component: """Create a form control component. diff --git a/pynecone/components/forms/input.py b/pynecone/components/forms/input.py index 3fd1efb10..b9c05a20b 100644 --- a/pynecone/components/forms/input.py +++ b/pynecone/components/forms/input.py @@ -55,8 +55,7 @@ class Input(ChakraComponent): {"/utils/state": {ImportVar(tag="set_val")}}, ) - @classmethod - def get_controlled_triggers(cls) -> Dict[str, Var]: + def get_controlled_triggers(self) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. Returns: diff --git a/pynecone/components/forms/numberinput.py b/pynecone/components/forms/numberinput.py index 00c64e62d..87395be5e 100644 --- a/pynecone/components/forms/numberinput.py +++ b/pynecone/components/forms/numberinput.py @@ -64,8 +64,7 @@ class NumberInput(ChakraComponent): # "outline" | "filled" | "flushed" | "unstyled" variant: Var[str] - @classmethod - def get_controlled_triggers(cls) -> Dict[str, Var]: + def get_controlled_triggers(self) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. Returns: diff --git a/pynecone/components/forms/pininput.py b/pynecone/components/forms/pininput.py index 1a2e0f51b..01f32bd55 100644 --- a/pynecone/components/forms/pininput.py +++ b/pynecone/components/forms/pininput.py @@ -55,8 +55,7 @@ class PinInput(ChakraComponent): # "outline" | "flushed" | "filled" | "unstyled" variant: Var[str] - @classmethod - def get_controlled_triggers(cls) -> Dict[str, Var]: + def get_controlled_triggers(self) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. Returns: diff --git a/pynecone/components/forms/radio.py b/pynecone/components/forms/radio.py index 946311649..a938d5585 100644 --- a/pynecone/components/forms/radio.py +++ b/pynecone/components/forms/radio.py @@ -23,8 +23,7 @@ class RadioGroup(ChakraComponent): # The default value. default_value: Var[Any] - @classmethod - def get_controlled_triggers(cls) -> Dict[str, Var]: + def get_controlled_triggers(self) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. Returns: diff --git a/pynecone/components/forms/rangeslider.py b/pynecone/components/forms/rangeslider.py index 0bda15d41..534e2d7fd 100644 --- a/pynecone/components/forms/rangeslider.py +++ b/pynecone/components/forms/rangeslider.py @@ -43,8 +43,7 @@ class RangeSlider(ChakraComponent): # The minimum distance between slider thumbs. Useful for preventing the thumbs from being too close together. min_steps_between_thumbs: Var[int] - @classmethod - def get_controlled_triggers(cls) -> Dict[str, Var]: + def get_controlled_triggers(self) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. Returns: diff --git a/pynecone/components/forms/select.py b/pynecone/components/forms/select.py index fca96825c..c444192ed 100644 --- a/pynecone/components/forms/select.py +++ b/pynecone/components/forms/select.py @@ -48,8 +48,7 @@ class Select(ChakraComponent): # The size of the select. size: Var[str] - @classmethod - def get_controlled_triggers(cls) -> Dict[str, Var]: + def get_controlled_triggers(self) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. Returns: diff --git a/pynecone/components/forms/slider.py b/pynecone/components/forms/slider.py index 636cbcfa3..aabaa48d6 100644 --- a/pynecone/components/forms/slider.py +++ b/pynecone/components/forms/slider.py @@ -61,8 +61,7 @@ class Slider(ChakraComponent): # Maximum width of the slider. max_w: Var[str] - @classmethod - def get_controlled_triggers(cls) -> Dict[str, Var]: + def get_controlled_triggers(self) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. Returns: diff --git a/pynecone/components/forms/switch.py b/pynecone/components/forms/switch.py index d5192cf79..a99103fa1 100644 --- a/pynecone/components/forms/switch.py +++ b/pynecone/components/forms/switch.py @@ -41,8 +41,7 @@ class Switch(ChakraComponent): # The color scheme of the switch (e.g. "blue", "green", "red", etc.) color_scheme: Var[str] - @classmethod - def get_controlled_triggers(cls) -> Dict[str, Var]: + def get_controlled_triggers(self) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. Returns: diff --git a/pynecone/components/forms/textarea.py b/pynecone/components/forms/textarea.py index be5644522..8860470cb 100644 --- a/pynecone/components/forms/textarea.py +++ b/pynecone/components/forms/textarea.py @@ -42,8 +42,7 @@ class TextArea(ChakraComponent): # "outline" | "filled" | "flushed" | "unstyled" variant: Var[str] - @classmethod - def get_controlled_triggers(cls) -> Dict[str, Var]: + def get_controlled_triggers(self) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. Returns: diff --git a/pynecone/components/forms/upload.py b/pynecone/components/forms/upload.py index 424fbf9ca..f4eb3f2c9 100644 --- a/pynecone/components/forms/upload.py +++ b/pynecone/components/forms/upload.py @@ -80,8 +80,7 @@ class Upload(Component): # Create the component. return super().create(zone, on_drop=upload_file, **upload_props) - @classmethod - def get_controlled_triggers(cls) -> Dict[str, Var]: + def get_controlled_triggers(self) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. Returns: diff --git a/pynecone/components/layout/cond.py b/pynecone/components/layout/cond.py index e67be3448..4dc4ab097 100644 --- a/pynecone/components/layout/cond.py +++ b/pynecone/components/layout/cond.py @@ -17,10 +17,10 @@ class Cond(Component): cond: Var[Any] # The component to render if the cond is true. - comp1: Component + comp1: Component = Fragment.create() # The component to render if the cond is false. - comp2: Component + comp2: Component = Fragment.create() @classmethod def create( diff --git a/pynecone/components/layout/foreach.py b/pynecone/components/layout/foreach.py index 098449747..9865e34dc 100644 --- a/pynecone/components/layout/foreach.py +++ b/pynecone/components/layout/foreach.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any, Callable, List from pynecone.components.component import Component +from pynecone.components.layout.fragment import Fragment from pynecone.components.tags import IterTag from pynecone.vars import BaseVar, Var, get_unique_variable_name @@ -15,7 +16,7 @@ class Foreach(Component): iterable: Var[List] # A function from the render args to the component. - render_fn: Callable + render_fn: Callable = Fragment.create @classmethod def create(cls, iterable: Var[List], render_fn: Callable, **props) -> Foreach: diff --git a/pynecone/components/media/avatar.py b/pynecone/components/media/avatar.py index f562601b7..2d3555854 100644 --- a/pynecone/components/media/avatar.py +++ b/pynecone/components/media/avatar.py @@ -35,8 +35,7 @@ class Avatar(ChakraComponent): # "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "full" size: Var[str] - @classmethod - def get_triggers(cls) -> Set[str]: + def get_triggers(self) -> Set[str]: """Get the event triggers for the component. Returns: diff --git a/pynecone/components/media/image.py b/pynecone/components/media/image.py index 83199c2b5..455701429 100644 --- a/pynecone/components/media/image.py +++ b/pynecone/components/media/image.py @@ -43,8 +43,7 @@ class Image(ChakraComponent): # The image srcset attribute. src_set: Var[str] - @classmethod - def get_triggers(cls) -> Set[str]: + def get_triggers(self) -> Set[str]: """Get the event triggers for the component. Returns: diff --git a/pynecone/components/overlay/alertdialog.py b/pynecone/components/overlay/alertdialog.py index 46b095f6d..1dd2bd8ca 100644 --- a/pynecone/components/overlay/alertdialog.py +++ b/pynecone/components/overlay/alertdialog.py @@ -52,8 +52,7 @@ class AlertDialog(ChakraComponent): # If true, the siblings of the modal will have `aria-hidden` set to true so that screen readers can only see the modal. This is commonly known as making the other elements **inert** use_inert: Var[bool] - @classmethod - def get_triggers(cls) -> Set[str]: + def get_triggers(self) -> Set[str]: """Get the event triggers for the component. Returns: diff --git a/pynecone/components/overlay/drawer.py b/pynecone/components/overlay/drawer.py index a832ba596..782fdcae7 100644 --- a/pynecone/components/overlay/drawer.py +++ b/pynecone/components/overlay/drawer.py @@ -58,8 +58,7 @@ class Drawer(ChakraComponent): # Variant of drawer variant: Var[str] - @classmethod - def get_triggers(cls) -> Set[str]: + def get_triggers(self) -> Set[str]: """Get the event triggers for the component. Returns: diff --git a/pynecone/components/overlay/menu.py b/pynecone/components/overlay/menu.py index 1b6619434..5a162baa7 100644 --- a/pynecone/components/overlay/menu.py +++ b/pynecone/components/overlay/menu.py @@ -60,8 +60,7 @@ class Menu(ChakraComponent): # The CSS positioning strategy to use. ("fixed" | "absolute") strategy: Var[str] - @classmethod - def get_triggers(cls) -> Set[str]: + def get_triggers(self) -> Set[str]: """Get the event triggers for the component. Returns: diff --git a/pynecone/components/overlay/modal.py b/pynecone/components/overlay/modal.py index 85d4f8264..18981d38a 100644 --- a/pynecone/components/overlay/modal.py +++ b/pynecone/components/overlay/modal.py @@ -52,8 +52,7 @@ class Modal(ChakraComponent): # A11y: If true, the siblings of the modal will have `aria-hidden` set to true so that screen readers can only see the modal. This is commonly known as making the other elements **inert** use_inert: Var[bool] - @classmethod - def get_triggers(cls) -> Set[str]: + def get_triggers(self) -> Set[str]: """Get the event triggers for the component. Returns: diff --git a/pynecone/components/overlay/popover.py b/pynecone/components/overlay/popover.py index acd8a83eb..b42d80cd6 100644 --- a/pynecone/components/overlay/popover.py +++ b/pynecone/components/overlay/popover.py @@ -75,8 +75,7 @@ class Popover(ChakraComponent): # The interaction that triggers the popover. hover - means the popover will open when you hover with mouse or focus with keyboard on the popover trigger click - means the popover will open on click or press Enter to Space on keyboard ("click" | "hover") trigger: Var[str] - @classmethod - def get_triggers(cls) -> Set[str]: + def get_triggers(self) -> Set[str]: """Get the event triggers for the component. Returns: diff --git a/pynecone/components/overlay/tooltip.py b/pynecone/components/overlay/tooltip.py index af0f719dc..3e5dd8355 100644 --- a/pynecone/components/overlay/tooltip.py +++ b/pynecone/components/overlay/tooltip.py @@ -62,8 +62,7 @@ class Tooltip(ChakraComponent): # If true, the tooltip will wrap its children in a `` with `tabIndex=0` should_wrap_children: Var[bool] - @classmethod - def get_triggers(cls) -> Set[str]: + def get_triggers(self) -> Set[str]: """Get the event triggers for the component. Returns: diff --git a/pynecone/components/tags/tag.py b/pynecone/components/tags/tag.py index 5d4cbd1ec..397b6e8f4 100644 --- a/pynecone/components/tags/tag.py +++ b/pynecone/components/tags/tag.py @@ -3,7 +3,6 @@ from __future__ import annotations import json -import re from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union from plotly.graph_objects import Figure @@ -97,14 +96,10 @@ class Tag(Base): prop = json.loads(to_json(prop))["data"] # type: ignore # For dictionaries, convert any properties to strings. - else: - if isinstance(prop, dict): - # Convert any var keys to strings. - prop = { - key: str(val) if isinstance(val, Var) else val - for key, val in prop.items() - } + elif isinstance(prop, dict): + prop = format.format_dict(prop) + else: # Dump the prop as JSON. try: prop = format.json_dumps(prop) @@ -113,11 +108,6 @@ class Tag(Base): f"Could not format prop: {prop} of type {type(prop)}" ) from e - # This substitution is necessary to unwrap var values. - prop = re.sub('"{', "", prop) - prop = re.sub('}"', "", prop) - prop = re.sub('\\\\"', '"', prop) - # Wrap the variable in braces. assert isinstance(prop, str), "The prop must be a string." return format.wrap(prop, "{", check_first=False) diff --git a/pynecone/utils/format.py b/pynecone/utils/format.py index ce233b38a..2fc6d1de2 100644 --- a/pynecone/utils/format.py +++ b/pynecone/utils/format.py @@ -15,6 +15,7 @@ from pynecone import constants from pynecone.utils import types if TYPE_CHECKING: + from pynecone.components.component import ComponentStyle from pynecone.event import EventChain, EventHandler, EventSpec WRAP_MAP = { @@ -411,6 +412,33 @@ def format_ref(ref: str) -> str: return f"ref_{clean_ref}" +def format_dict(prop: ComponentStyle) -> str: + """Format a dict with vars potentially as values. + + Args: + prop: The dict to format. + + Returns: + The formatted dict. + """ + # Import here to avoid circular imports. + from pynecone.vars import Var + + # Convert any var keys to strings. + prop = {key: str(val) if isinstance(val, Var) else val for key, val in prop.items()} + + # Dump the dict to a string. + fprop = json_dumps(prop) + + # This substitution is necessary to unwrap var values. + fprop = re.sub('"{', "", fprop) + fprop = re.sub('}"', "", fprop) + fprop = re.sub('\\\\"', '"', fprop) + + # Return the formatted dict. + return fprop + + def json_dumps(obj: Any) -> str: """Takes an object and returns a jsonified string. diff --git a/pynecone/vars.py b/pynecone/vars.py index 87062e4e6..b617c3831 100644 --- a/pynecone/vars.py +++ b/pynecone/vars.py @@ -101,6 +101,9 @@ class Var(ABC): value = json.loads(to_json(value))["data"] # type: ignore type_ = Figure + if isinstance(value, dict): + value = format.format_dict(value) + try: name = value if isinstance(value, str) else json.dumps(value) except TypeError as e: diff --git a/tests/components/test_component.py b/tests/components/test_component.py index 756c46450..b5340e0c7 100644 --- a/tests/components/test_component.py +++ b/tests/components/test_component.py @@ -62,8 +62,7 @@ def component2() -> Type[Component]: # A test list prop. arr: Var[List[str]] - @classmethod - def get_controlled_triggers(cls) -> Dict[str, Var]: + def get_controlled_triggers(self) -> Dict[str, Var]: """Test controlled triggers. Returns: @@ -307,8 +306,8 @@ def test_get_controlled_triggers(component1, component2): component1: A test component. component2: A test component. """ - assert component1.get_controlled_triggers() == dict() - assert set(component2.get_controlled_triggers()) == {"on_open", "on_close"} + assert component1().get_controlled_triggers() == dict() + assert set(component2().get_controlled_triggers()) == {"on_open", "on_close"} def test_get_triggers(component1, component2): @@ -318,8 +317,8 @@ def test_get_triggers(component1, component2): component1: A test component. component2: A test component. """ - assert component1.get_triggers() == EVENT_TRIGGERS - assert component2.get_triggers() == {"on_open", "on_close"} | EVENT_TRIGGERS + assert component1().get_triggers() == EVENT_TRIGGERS + assert component2().get_triggers() == {"on_open", "on_close"} | EVENT_TRIGGERS def test_create_custom_component(my_component):