diff --git a/pynecone/compiler/templates.py b/pynecone/compiler/templates.py index c8731f963..39fb89c9b 100644 --- a/pynecone/compiler/templates.py +++ b/pynecone/compiler/templates.py @@ -154,7 +154,15 @@ UPLOAD_FN = path_ops.join( "}})", ] ).format - +FULL_CONTROL = path_ops.join( + [ + "{{setState(prev => ({{", + "...prev,{state_name}: {arg}", + "}}), ", + "()=>Event([{chain}])", + ")}}", + ] +).format # Effects. ROUTER = constants.ROUTER diff --git a/pynecone/components/component.py b/pynecone/components/component.py index 3605419b7..09eda8db2 100644 --- a/pynecone/components/component.py +++ b/pynecone/components/component.py @@ -134,7 +134,11 @@ class Component(Base, ABC): # Check if the key is an event trigger. if key in triggers: - kwargs["event_triggers"][key] = self._create_event_chain(key, value) + state_name = kwargs["value"].name if kwargs.get("value", False) else "" + full_control = self.is_full_control(kwargs) + kwargs["event_triggers"][key] = self._create_event_chain( + key, value, state_name, full_control + ) # Remove any keys that were added as events. for key in kwargs["event_triggers"]: @@ -167,12 +171,16 @@ class Component(Base, ABC): value: Union[ Var, EventHandler, EventSpec, List[Union[EventHandler, EventSpec]], Callable ], + state_name: str = "", + full_control: bool = False, ) -> Union[EventChain, Var]: """Create an event chain from a variety of input types. Args: event_trigger: The event trigger to bind the chain to. value: The value to create the event chain from. + state_name: The state to be fully controlled. + full_control: Whether full contorolled or not. Returns: The event chain. @@ -240,8 +248,13 @@ class Component(Base, ABC): for e in events ] + # set state name when fully controlled input + state_name = state_name if full_control else "" + # Return the event chain. - return EventChain(events=events) + return EventChain( + events=events, state_name=state_name, full_control=full_control + ) @classmethod def get_triggers(cls) -> Set[str]: @@ -466,6 +479,27 @@ class Component(Base, ABC): custom_components |= child.get_custom_components(seen=seen) return custom_components + def is_full_control(self, kwargs: dict) -> bool: + """Return if the component is fully controlled input. + + Args: + kwargs: The component kwargs. + + Returns: + Whether fully controlled. + """ + value = kwargs.get("value") + if value is None or type(value) != BaseVar: + return False + + on_change = kwargs.get("on_change") + if on_change is None or type(on_change) != EventHandler: + return False + + value = value.full_name + on_change = on_change.fn.__qualname__ + return value == on_change.replace(constants.SETTER_PREFIX, "") + # Map from component to styling. ComponentStyle = Dict[Union[str, Type[Component]], Any] diff --git a/pynecone/components/tags/tag.py b/pynecone/components/tags/tag.py index f170a83f4..cdd8f9434 100644 --- a/pynecone/components/tags/tag.py +++ b/pynecone/components/tags/tag.py @@ -78,6 +78,9 @@ class Tag(Base): if len(prop.events) == 1 and prop.events[0].upload: # Special case for upload events. event = format.format_upload_event(prop.events[0]) + elif prop.full_control: + # Full control component events. + event = format.format_full_control_event(prop) else: # All other events. chain = ",".join([format.format_event(event) for event in prop.events]) diff --git a/pynecone/event.py b/pynecone/event.py index 38b2ad634..cce531961 100644 --- a/pynecone/event.py +++ b/pynecone/event.py @@ -110,6 +110,12 @@ class EventChain(Base): events: List[EventSpec] + # Whether events are in fully controlled input. + full_control: bool = False + + # State name when fully controlled. + state_name: str = "" + class Target(Base): """A Javascript event target.""" diff --git a/pynecone/utils/format.py b/pynecone/utils/format.py index f971ab603..aed3d25eb 100644 --- a/pynecone/utils/format.py +++ b/pynecone/utils/format.py @@ -15,7 +15,7 @@ from pynecone import constants from pynecone.utils import types if TYPE_CHECKING: - from pynecone.event import EventHandler, EventSpec + from pynecone.event import EventChain, EventHandler, EventSpec WRAP_MAP = { "{": "}", @@ -303,6 +303,25 @@ def format_upload_event(event_spec: EventSpec) -> str: return f'uploadFiles({parent_state}, {templates.RESULT}, {templates.SET_RESULT}, {parent_state}.files, "{state}.{name}",UPLOAD)' +def format_full_control_event(event_chain: EventChain) -> str: + """Format a fully controlled input prop. + + Args: + event_chain: The event chain for full controlled input. + + Returns: + The compiled event. + """ + from pynecone.compiler import templates + + event_spec = event_chain.events[0] + arg = event_spec.args[0][1] + state_name = event_chain.state_name + chain = ",".join([format_event(event) for event in event_chain.events]) + event = templates.FULL_CONTROL(state_name=state_name, arg=arg, chain=chain) + return event + + def format_query_params(router_data: Dict[str, Any]) -> Dict[str, str]: """Convert back query params name to python-friendly case.