Bläddra i källkod

Add pc.set_value (#835)

Nikhil Rao 2 år sedan
förälder
incheckning
1a254aca8e

+ 5 - 0
pynecone/.templates/web/utils/state.js

@@ -90,6 +90,11 @@ export const applyEvent = async (event, router, socket) => {
     return false;
   }
 
+  if (event.name == "_set_value") {
+    event.payload.ref.current.value = event.payload.value;
+    return false; 
+  }
+
   // Send the event to the server.
   event.token = getToken();
   event.router_data = (({ pathname, query }) => ({ pathname, query }))(router);

+ 1 - 0
pynecone/__init__.py

@@ -16,6 +16,7 @@ from .event import (
     EventChain,
     console_log,
     redirect,
+    set_value,
     window_alert,
 )
 from .event import FileUpload as upload_files

+ 21 - 3
pynecone/components/component.py

@@ -181,7 +181,7 @@ class Component(Base, ABC):
             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.
+            full_control: Whether full controlled or not.
 
         Returns:
             The event chain.
@@ -243,7 +243,7 @@ class Component(Base, ABC):
             events = [
                 EventSpec(
                     handler=e.handler,
-                    local_args=(EVENT_ARG.name,),
+                    local_args=(EVENT_ARG,),
                     args=get_handler_args(e, arg),
                 )
                 for e in events
@@ -461,10 +461,18 @@ class Component(Base, ABC):
         )
 
     def _get_hooks(self) -> Optional[str]:
+        """Get the React hooks for this component.
+
+        Returns:
+            The hooks for just this component.
+        """
+        ref = self.get_ref()
+        if ref is not None:
+            return f"const {ref} = useRef(null);"
         return None
 
     def get_hooks(self) -> Set[str]:
-        """Get javascript code for react hooks.
+        """Get the React hooks for this component and its children.
 
         Returns:
             The code that should appear just before returning the rendered component.
@@ -483,6 +491,16 @@ class Component(Base, ABC):
 
         return code
 
+    def get_ref(self) -> Optional[str]:
+        """Get the name of the ref for the component.
+
+        Returns:
+            The ref name.
+        """
+        if self.id is None:
+            return None
+        return format.format_ref(self.id)
+
     def get_custom_components(
         self, seen: Optional[Set[str]] = None
     ) -> Set[CustomComponent]:

+ 14 - 0
pynecone/components/forms/input.py

@@ -4,6 +4,7 @@ from typing import Dict
 
 from pynecone.components.component import EVENT_ARG
 from pynecone.components.libs.chakra import ChakraComponent
+from pynecone.utils import imports
 from pynecone.var import Var
 
 
@@ -48,6 +49,12 @@ class Input(ChakraComponent):
     # "lg" | "md" | "sm" | "xs"
     size: Var[str]
 
+    def _get_imports(self) -> imports.ImportDict:
+        return imports.merge_imports(
+            super()._get_imports(),
+            {"/utils/state": {"set_val"}},
+        )
+
     @classmethod
     def get_controlled_triggers(cls) -> Dict[str, Var]:
         """Get the event triggers that pass the component's value to the handler.
@@ -63,6 +70,13 @@ class Input(ChakraComponent):
             "on_key_up": EVENT_ARG.key,
         }
 
+    def _render(self):
+        out = super()._render()
+        ref = self.get_ref()
+        if ref is not None:
+            out.add_props(ref=Var.create(ref, is_local=False))
+        return out
+
 
 class InputGroup(ChakraComponent):
     """The InputGroup component is a component that is used to group a set of inputs."""

+ 1 - 1
pynecone/components/tags/tag.py

@@ -73,7 +73,7 @@ class Tag(Base):
 
         # Handle event props.
         elif isinstance(prop, EventChain):
-            local_args = ",".join(prop.events[0].local_args)
+            local_args = ",".join(([str(a) for a in prop.events[0].local_args]))
 
             if len(prop.events) == 1 and prop.events[0].upload:
                 # Special case for upload events.

+ 47 - 37
pynecone/event.py

@@ -23,7 +23,7 @@ class Event(Base):
     router_data: Dict[str, Any] = {}
 
     # The event payload.
-    payload: Dict[str, Any] = {}
+    payload: Dict[Any, Any] = {}
 
 
 class EventHandler(Base):
@@ -54,21 +54,18 @@ class EventHandler(Base):
         """
         # Get the function args.
         fn_args = inspect.getfullargspec(self.fn).args[1:]
+        fn_args = (Var.create_safe(arg) for arg in fn_args)
 
         # Construct the payload.
         values = []
         for arg in args:
-            # If it is a Var, add the full name.
-            if isinstance(arg, Var):
-                values.append(arg.full_name)
-                continue
-
+            # Special case for file uploads.
             if isinstance(arg, FileUpload):
                 return EventSpec(handler=self, upload=True)
 
             # Otherwise, convert to JSON.
             try:
-                values.append(format.json_dumps(arg))
+                values.append(Var.create(arg))
             except TypeError as e:
                 raise TypeError(
                     f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}."
@@ -90,10 +87,10 @@ class EventSpec(Base):
     handler: EventHandler
 
     # The local arguments on the frontend.
-    local_args: Tuple[str, ...] = ()
+    local_args: Tuple[Var, ...] = ()
 
     # The arguments to pass to the function.
-    args: Tuple[Any, ...] = ()
+    args: Tuple[Tuple[Var, Var], ...] = ()
 
     # Whether to upload files.
     upload: bool = False
@@ -142,27 +139,43 @@ class FileUpload(Base):
 
 
 # Special server-side events.
-def redirect(path: str) -> EventSpec:
-    """Redirect to a new path.
+def server_side(name: str, **kwargs) -> EventSpec:
+    """A server-side event.
 
     Args:
-        path: The path to redirect to.
+        name: The name of the event.
+        **kwargs: The arguments to pass to the event.
 
     Returns:
-        An event to redirect to the path.
+        An event spec for a server-side event.
     """
 
     def fn():
         return None
 
-    fn.__qualname__ = "_redirect"
+    fn.__qualname__ = name
     return EventSpec(
         handler=EventHandler(fn=fn),
-        args=(("path", path),),
+        args=tuple(
+            (Var.create_safe(k), Var.create_safe(v, is_string=type(v) is str))
+            for k, v in kwargs.items()
+        ),
     )
 
 
-def console_log(message: str) -> EventSpec:
+def redirect(path: Union[str, Var[str]]) -> EventSpec:
+    """Redirect to a new path.
+
+    Args:
+        path: The path to redirect to.
+
+    Returns:
+        An event to redirect to the path.
+    """
+    return server_side("_redirect", path=path)
+
+
+def console_log(message: Union[str, Var[str]]) -> EventSpec:
     """Do a console.log on the browser.
 
     Args:
@@ -171,18 +184,10 @@ def console_log(message: str) -> EventSpec:
     Returns:
         An event to log the message.
     """
+    return server_side("_console", message=message)
 
-    def fn():
-        return None
 
-    fn.__qualname__ = "_console"
-    return EventSpec(
-        handler=EventHandler(fn=fn),
-        args=(("message", message),),
-    )
-
-
-def window_alert(message: str) -> EventSpec:
+def window_alert(message: Union[str, Var[str]]) -> EventSpec:
     """Create a window alert on the browser.
 
     Args:
@@ -191,14 +196,21 @@ def window_alert(message: str) -> EventSpec:
     Returns:
         An event to alert the message.
     """
+    return server_side("_alert", message=message)
 
-    def fn():
-        return None
 
-    fn.__qualname__ = "_alert"
-    return EventSpec(
-        handler=EventHandler(fn=fn),
-        args=(("message", message),),
+def set_value(ref: str, value: Any) -> EventSpec:
+    """Set the value of a ref.
+
+    Args:
+        ref: The ref.
+        value: The value to set.
+
+    Returns:
+        An event to set the ref.
+    """
+    return server_side(
+        "_set_value", ref=Var.create_safe(format.format_ref(ref)), value=value
     )
 
 
@@ -306,7 +318,7 @@ def call_event_fn(fn: Callable, arg: Var) -> List[EventSpec]:
     return events
 
 
-def get_handler_args(event_spec: EventSpec, arg: Var) -> Tuple[Tuple[str, str], ...]:
+def get_handler_args(event_spec: EventSpec, arg: Var) -> Tuple[Tuple[Var, Var], ...]:
     """Get the handler args for the given event spec.
 
     Args:
@@ -324,7 +336,7 @@ def get_handler_args(event_spec: EventSpec, arg: Var) -> Tuple[Tuple[str, str],
         raise ValueError(
             f"Event handler has an invalid signature, needed a method with a parameter, got {event_spec.handler}."
         )
-    return event_spec.args if len(args) > 2 else ((args[1], arg.name),)
+    return event_spec.args if len(args) > 2 else ((Var.create_safe(args[1]), arg),)
 
 
 def fix_events(
@@ -339,8 +351,6 @@ def fix_events(
     Returns:
         The fixed events.
     """
-    from pynecone.event import Event, EventHandler, EventSpec
-
     # If the event handler returns nothing, return an empty list.
     if events is None:
         return []
@@ -359,7 +369,7 @@ def fix_events(
             e = e()
         assert isinstance(e, EventSpec), f"Unexpected event type, {type(e)}."
         name = format.format_event_handler(e.handler)
-        payload = dict(e.args)
+        payload = {k.name: v.name for k, v in e.args}
 
         # Create an event and append it to the list.
         out.append(

+ 20 - 1
pynecone/utils/format.py

@@ -286,7 +286,12 @@ def format_event(event_spec: EventSpec) -> str:
     Returns:
         The compiled event.
     """
-    args = ",".join([":".join((name, val)) for name, val in event_spec.args])
+    args = ",".join(
+        [
+            ":".join((name.name, json.dumps(val.name) if val.is_string else val.name))
+            for name, val in event_spec.args
+        ]
+    )
     return f"E(\"{format_event_handler(event_spec.handler)}\", {wrap(args, '{')})"
 
 
@@ -398,6 +403,20 @@ def format_state(value: Any) -> Dict:
     )
 
 
+def format_ref(ref: str) -> str:
+    """Format a ref.
+
+    Args:
+        ref: The ref to format.
+
+    Returns:
+        The formatted ref.
+    """
+    # Replace all non-word characters with underscores.
+    clean_ref = re.sub(r"[^\w]+", "_", ref)
+    return f"ref_{clean_ref}"
+
+
 def json_dumps(obj: Any) -> str:
     """Takes an object and returns a jsonified string.
 

+ 18 - 0
pynecone/var.py

@@ -105,6 +105,24 @@ class Var(ABC):
 
         return BaseVar(name=name, type_=type_, is_local=is_local, is_string=is_string)
 
+    @classmethod
+    def create_safe(
+        cls, value: Any, is_local: bool = True, is_string: bool = False
+    ) -> Var:
+        """Create a var from a value, guaranteeing that it is not None.
+
+        Args:
+            value: The value to create the var from.
+            is_local: Whether the var is local.
+            is_string: Whether the var is a string literal.
+
+        Returns:
+            The var.
+        """
+        var = cls.create(value, is_local=is_local, is_string=is_string)
+        assert var is not None
+        return var
+
     @classmethod
     def __class_getitem__(cls, type_: str) -> _GenericAlias:
         """Get a typed var.

+ 1 - 1
pyproject.toml

@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "pynecone"
-version = "0.1.27"
+version = "0.1.28"
 description = "Web apps in pure Python."
 license = "Apache-2.0"
 authors = [

+ 2 - 2
scripts/integration.sh

@@ -15,9 +15,9 @@ while ! nc -z localhost 3000 || ! lsof -i :8000 >/dev/null; do
       echo "Error: Server process with PID $pid exited early"
       break
   fi
-  if ((wait_time >= 200)); then
+  if ((wait_time >= 300)); then
     echo "Error: Timeout waiting for ports 3000 and 8000 to become available"
-    break
+    exit 1
   fi
   sleep 5
   ((wait_time += 5))

+ 4 - 4
tests/components/test_tag.py

@@ -3,7 +3,7 @@ from typing import Any, Dict
 import pytest
 
 from pynecone.components.tags import CondTag, Tag
-from pynecone.event import EventChain, EventHandler, EventSpec
+from pynecone.event import EVENT_ARG, EventChain, EventHandler, EventSpec
 from pynecone.var import BaseVar, Var
 
 
@@ -32,12 +32,12 @@ def mock_event(arg):
                 events=[
                     EventSpec(
                         handler=EventHandler(fn=mock_event),
-                        local_args=("e",),
-                        args=(("arg", "e.target.value"),),
+                        local_args=(EVENT_ARG,),
+                        args=((Var.create_safe("arg"), EVENT_ARG.target.value),),
                     )
                 ]
             ),
-            '{(e) => Event([E("mock_event", {arg:e.target.value})])}',
+            '{(_e) => Event([E("mock_event", {arg:_e.target.value})])}',
         ),
         ({"a": "red", "b": "blue"}, '{{"a": "red", "b": "blue"}}'),
         (BaseVar(name="var", type_="int"), "{var}"),

+ 25 - 0
tests/test_event.py

@@ -73,6 +73,9 @@ def test_event_redirect():
     assert isinstance(spec, EventSpec)
     assert spec.handler.fn.__qualname__ == "_redirect"
     assert spec.args == (("path", "/path"),)
+    assert format.format_event(spec) == 'E("_redirect", {path:"/path"})'
+    spec = event.redirect(Var.create_safe("path"))
+    assert format.format_event(spec) == 'E("_redirect", {path:path})'
 
 
 def test_event_console_log():
@@ -81,6 +84,9 @@ def test_event_console_log():
     assert isinstance(spec, EventSpec)
     assert spec.handler.fn.__qualname__ == "_console"
     assert spec.args == (("message", "message"),)
+    assert format.format_event(spec) == 'E("_console", {message:"message"})'
+    spec = event.console_log(Var.create_safe("message"))
+    assert format.format_event(spec) == 'E("_console", {message:message})'
 
 
 def test_event_window_alert():
@@ -89,3 +95,22 @@ def test_event_window_alert():
     assert isinstance(spec, EventSpec)
     assert spec.handler.fn.__qualname__ == "_alert"
     assert spec.args == (("message", "message"),)
+    assert format.format_event(spec) == 'E("_alert", {message:"message"})'
+    spec = event.window_alert(Var.create_safe("message"))
+    assert format.format_event(spec) == 'E("_alert", {message:message})'
+
+
+def test_set_value():
+    """Test the event window alert function."""
+    spec = event.set_value("input1", "")
+    assert isinstance(spec, EventSpec)
+    assert spec.handler.fn.__qualname__ == "_set_value"
+    assert spec.args == (
+        ("ref", Var.create_safe("ref_input1")),
+        ("value", ""),
+    )
+    assert format.format_event(spec) == 'E("_set_value", {ref:ref_input1,value:""})'
+    spec = event.set_value("input1", Var.create_safe("message"))
+    assert (
+        format.format_event(spec) == 'E("_set_value", {ref:ref_input1,value:message})'
+    )

+ 20 - 0
tests/test_utils.py

@@ -337,3 +337,23 @@ def test_create_config(app_name, expected_config_name, mocker):
     tmpl_mock.format.assert_called_with(
         app_name=app_name, config_name=expected_config_name
     )
+
+
+@pytest.mark.parametrize(
+    "name,expected",
+    [
+        ("input1", "ref_input1"),
+        ("input 1", "ref_input_1"),
+        ("input-1", "ref_input_1"),
+        ("input_1", "ref_input_1"),
+        ("a long test?1! name", "ref_a_long_test_1_name"),
+    ],
+)
+def test_format_ref(name, expected):
+    """Test formatting a ref.
+
+    Args:
+        name: The name to format.
+        expected: The expected formatted name.
+    """
+    assert format.format_ref(name) == expected