ソースを参照

Add pc.form component (#929)

Nikhil Rao 2 年 前
コミット
948ec90fc4

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

@@ -93,6 +93,7 @@ export const applyEvent = async (event, router, socket) => {
   }
 
   if (event.name == "_set_value") {
+    event.payload.ref.current.blur();
     event.payload.ref.current.value = event.payload.value;
     return false;
   }

+ 1 - 0
pynecone/components/__init__.py

@@ -81,6 +81,7 @@ editable = Editable.create
 editable_input = EditableInput.create
 editable_preview = EditablePreview.create
 editable_textarea = EditableTextarea.create
+form = Form.create
 form_control = FormControl.create
 form_error_message = FormErrorMessage.create
 form_helper_text = FormHelperText.create

+ 4 - 8
pynecone/components/component.py

@@ -251,7 +251,6 @@ class Component(Base, ABC):
             events = [
                 EventSpec(
                     handler=e.handler,
-                    local_args=(EVENT_ARG,),
                     args=get_handler_args(e, arg),
                 )
                 for e in events
@@ -312,13 +311,10 @@ class Component(Base, ABC):
         )
 
         # Add component props to the tag.
-        props = {attr: getattr(self, attr) for attr in self.get_props()}
-
-        # Special case for props named `type_`.
-        if hasattr(self, "type_"):
-            props["type"] = self.type_  # type: ignore
-        if hasattr(self, "as_"):
-            props["as"] = self.as_  # type: ignore
+        props = {
+            attr[:-1] if attr.endswith("_") else attr: getattr(self, attr)
+            for attr in self.get_props()
+        }
 
         # Add ref to element if `id` is not None.
         ref = self.get_ref()

+ 1 - 1
pynecone/components/forms/__init__.py

@@ -4,7 +4,7 @@ from .button import Button, ButtonGroup
 from .checkbox import Checkbox, CheckboxGroup
 from .copytoclipboard import CopyToClipboard
 from .editable import Editable, EditableInput, EditablePreview, EditableTextarea
-from .formcontrol import FormControl, FormErrorMessage, FormHelperText, FormLabel
+from .formcontrol import Form, FormControl, FormErrorMessage, FormHelperText, FormLabel
 from .iconbutton import IconButton
 from .input import Input, InputGroup, InputLeftAddon, InputRightAddon
 from .numberinput import (

+ 3 - 0
pynecone/components/forms/button.py

@@ -39,6 +39,9 @@ class Button(ChakraComponent):
     # | "purple" | "pink" | "linkedin" | "facebook" | "messenger" | "whatsapp" | "twitter" | "telegram"
     color_scheme: Var[str]
 
+    # The type of button.
+    type_: Var[str]
+
 
 class ButtonGroup(ChakraComponent):
     """A group of buttons."""

+ 19 - 0
pynecone/components/forms/formcontrol.py

@@ -1,10 +1,29 @@
 """Form components."""
 
+from typing import Set
+
 from pynecone.components.component import Component
 from pynecone.components.libs.chakra import ChakraComponent
 from pynecone.vars import Var
 
 
+class Form(ChakraComponent):
+    """A form component."""
+
+    tag = "Box"
+
+    as_: Var[str] = "form"  # type: ignore
+
+    @classmethod
+    def get_triggers(cls) -> Set[str]:
+        """Get the event triggers for the component.
+
+        Returns:
+            The event triggers.
+        """
+        return super().get_triggers() | {"on_submit"}
+
+
 class FormControl(ChakraComponent):
     """Provide context to form components."""
 

+ 3 - 5
pynecone/components/tags/tag.py

@@ -10,7 +10,7 @@ from plotly.graph_objects import Figure
 from plotly.io import to_json
 
 from pynecone.base import Base
-from pynecone.event import EventChain
+from pynecone.event import EVENT_ARG, EventChain
 from pynecone.utils import format, types
 from pynecone.vars import Var
 
@@ -75,16 +75,14 @@ class Tag(Base):
 
         # Handle event props.
         elif isinstance(prop, EventChain):
-            local_args = ",".join(([str(a) for a in prop.events[0].local_args]))
-
             if 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])
-                event = f"Event([{chain}])"
-            prop = f"({local_args}) => {event}"
+                event = f"{{{EVENT_ARG}.preventDefault(); Event([{chain}])}}"
+            prop = f"({EVENT_ARG}) => {event}"
 
         # Handle other types.
         elif isinstance(prop, str):

+ 0 - 3
pynecone/event.py

@@ -92,9 +92,6 @@ class EventSpec(Base):
     # The handler on the client to process event.
     client_handler_name: str = ""
 
-    # The local arguments on the frontend.
-    local_args: Tuple[Var, ...] = ()
-
     # The arguments to pass to the function.
     args: Tuple[Tuple[Var, Var], ...] = ()
 

+ 0 - 3
pynecone/pc.py

@@ -83,9 +83,6 @@ def run(
     frontend_port = get_config().port if port is None else port
     backend_port = get_config().backend_port if backend_port is None else backend_port
 
-    # set the upload url in pynecone.json file
-    build.set_pynecone_upload_endpoint()
-
     # If --no-frontend-only and no --backend-only, then turn on frontend and backend both
     if not frontend and not backend:
         frontend = True

+ 3 - 0
pynecone/utils/build.py

@@ -160,6 +160,9 @@ def setup_frontend(root: Path, disable_telemetry: bool = True):
         dest=str(root / constants.WEB_ASSETS_DIR),
     )
 
+    # set the upload url in pynecone.json file
+    set_pynecone_upload_endpoint()
+
     # Disable the Next telemetry.
     if disable_telemetry:
         subprocess.Popen(

+ 9 - 2
pynecone/utils/format.py

@@ -294,8 +294,15 @@ def format_event(event_spec: EventSpec) -> str:
             for name, val in event_spec.args
         ]
     )
-    quote = '"'
-    return f"E(\"{format_event_handler(event_spec.handler)}\", {wrap(args, '{')}, {wrap(event_spec.client_handler_name, quote) if event_spec.client_handler_name else ''})"
+    event_args = [
+        wrap(format_event_handler(event_spec.handler), '"'),
+    ]
+    if len(args) > 0:
+        event_args.append(wrap(args, "{"))
+
+    if event_spec.client_handler_name:
+        event_args.append(wrap(event_spec.client_handler_name, '"'))
+    return f"E({', '.join(event_args)})"
 
 
 def format_full_control_event(event_chain: EventChain) -> str:

+ 1 - 1
pyproject.toml

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

+ 2 - 3
tests/components/test_tag.py

@@ -25,19 +25,18 @@ def mock_event(arg):
         ({"a": 1, "b": 2, "c": 3}, '{{"a": 1, "b": 2, "c": 3}}'),
         (
             EventChain(events=[EventSpec(handler=EventHandler(fn=mock_event))]),
-            '{() => Event([E("mock_event", {}, )])}',
+            '{(_e) => {_e.preventDefault(); Event([E("mock_event")])}}',
         ),
         (
             EventChain(
                 events=[
                     EventSpec(
                         handler=EventHandler(fn=mock_event),
-                        local_args=(EVENT_ARG,),
                         args=((Var.create_safe("arg"), EVENT_ARG.target.value),),
                     )
                 ]
             ),
-            '{(_e) => Event([E("mock_event", {arg:_e.target.value}, )])}',
+            '{(_e) => {_e.preventDefault(); Event([E("mock_event", {arg:_e.target.value})])}}',
         ),
         ({"a": "red", "b": "blue"}, '{{"a": "red", "b": "blue"}}'),
         (BaseVar(name="var", type_="int"), "{var}"),

+ 12 - 15
tests/test_event.py

@@ -45,27 +45,25 @@ def test_call_event_handler():
     event_spec = handler()
 
     assert event_spec.handler == handler
-    assert event_spec.local_args == ()
     assert event_spec.args == ()
-    assert format.format_event(event_spec) == 'E("test_fn", {}, )'
+    assert format.format_event(event_spec) == 'E("test_fn")'
 
     handler = EventHandler(fn=test_fn_with_args)
     event_spec = handler(make_var("first"), make_var("second"))
 
     # Test passing vars as args.
     assert event_spec.handler == handler
-    assert event_spec.local_args == ()
     assert event_spec.args == (("arg1", "first"), ("arg2", "second"))
     assert (
         format.format_event(event_spec)
-        == 'E("test_fn_with_args", {arg1:first,arg2:second}, )'
+        == 'E("test_fn_with_args", {arg1:first,arg2:second})'
     )
 
     # Passing args as strings should format differently.
     event_spec = handler("first", "second")  # type: ignore
     assert (
         format.format_event(event_spec)
-        == 'E("test_fn_with_args", {arg1:"first",arg2:"second"}, )'
+        == 'E("test_fn_with_args", {arg1:"first",arg2:"second"})'
     )
 
     first, second = 123, "456"
@@ -73,11 +71,10 @@ def test_call_event_handler():
     event_spec = handler(first, second)  # type: ignore
     assert (
         format.format_event(event_spec)
-        == 'E("test_fn_with_args", {arg1:123,arg2:"456"}, )'
+        == 'E("test_fn_with_args", {arg1:123,arg2:"456"})'
     )
 
     assert event_spec.handler == handler
-    assert event_spec.local_args == ()
     assert event_spec.args == (
         ("arg1", format.json_dumps(first)),
         ("arg2", format.json_dumps(second)),
@@ -94,9 +91,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"}, )'
+    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}, )'
+    assert format.format_event(spec) == 'E("_redirect", {path:path})'
 
 
 def test_event_console_log():
@@ -105,9 +102,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"}, )'
+    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}, )'
+    assert format.format_event(spec) == 'E("_console", {message:message})'
 
 
 def test_event_window_alert():
@@ -116,9 +113,9 @@ 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"}, )'
+    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}, )'
+    assert format.format_event(spec) == 'E("_alert", {message:message})'
 
 
 def test_set_value():
@@ -130,8 +127,8 @@ def test_set_value():
         ("ref", Var.create_safe("ref_input1")),
         ("value", ""),
     )
-    assert format.format_event(spec) == 'E("_set_value", {ref: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}, )'
+        format.format_event(spec) == 'E("_set_value", {ref:ref_input1,value:message})'
     )

+ 1 - 0
tests/test_utils.py

@@ -330,6 +330,7 @@ def test_setup_frontend(tmp_path, mocker):
     assert str(web_folder) == prerequisites.create_web_directory(tmp_path)
 
     mocker.patch("pynecone.utils.prerequisites.install_frontend_packages")
+    mocker.patch("pynecone.utils.build.set_pynecone_upload_endpoint")
 
     build.setup_frontend(tmp_path, disable_telemetry=False)
     assert web_folder.exists()