瀏覽代碼

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") {
   if (event.name == "_set_value") {
+    event.payload.ref.current.blur();
     event.payload.ref.current.value = event.payload.value;
     event.payload.ref.current.value = event.payload.value;
     return false;
     return false;
   }
   }

+ 1 - 0
pynecone/components/__init__.py

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

+ 4 - 8
pynecone/components/component.py

@@ -251,7 +251,6 @@ class Component(Base, ABC):
             events = [
             events = [
                 EventSpec(
                 EventSpec(
                     handler=e.handler,
                     handler=e.handler,
-                    local_args=(EVENT_ARG,),
                     args=get_handler_args(e, arg),
                     args=get_handler_args(e, arg),
                 )
                 )
                 for e in events
                 for e in events
@@ -312,13 +311,10 @@ class Component(Base, ABC):
         )
         )
 
 
         # Add component props to the tag.
         # 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.
         # Add ref to element if `id` is not None.
         ref = self.get_ref()
         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 .checkbox import Checkbox, CheckboxGroup
 from .copytoclipboard import CopyToClipboard
 from .copytoclipboard import CopyToClipboard
 from .editable import Editable, EditableInput, EditablePreview, EditableTextarea
 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 .iconbutton import IconButton
 from .input import Input, InputGroup, InputLeftAddon, InputRightAddon
 from .input import Input, InputGroup, InputLeftAddon, InputRightAddon
 from .numberinput import (
 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"
     # | "purple" | "pink" | "linkedin" | "facebook" | "messenger" | "whatsapp" | "twitter" | "telegram"
     color_scheme: Var[str]
     color_scheme: Var[str]
 
 
+    # The type of button.
+    type_: Var[str]
+
 
 
 class ButtonGroup(ChakraComponent):
 class ButtonGroup(ChakraComponent):
     """A group of buttons."""
     """A group of buttons."""

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

@@ -1,10 +1,29 @@
 """Form components."""
 """Form components."""
 
 
+from typing import Set
+
 from pynecone.components.component import Component
 from pynecone.components.component import Component
 from pynecone.components.libs.chakra import ChakraComponent
 from pynecone.components.libs.chakra import ChakraComponent
 from pynecone.vars import Var
 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):
 class FormControl(ChakraComponent):
     """Provide context to form components."""
     """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 plotly.io import to_json
 
 
 from pynecone.base import Base
 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.utils import format, types
 from pynecone.vars import Var
 from pynecone.vars import Var
 
 
@@ -75,16 +75,14 @@ class Tag(Base):
 
 
         # Handle event props.
         # Handle event props.
         elif isinstance(prop, EventChain):
         elif isinstance(prop, EventChain):
-            local_args = ",".join(([str(a) for a in prop.events[0].local_args]))
-
             if prop.full_control:
             if prop.full_control:
                 # Full control component events.
                 # Full control component events.
                 event = format.format_full_control_event(prop)
                 event = format.format_full_control_event(prop)
             else:
             else:
                 # All other events.
                 # All other events.
                 chain = ",".join([format.format_event(event) for event in prop.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.
         # Handle other types.
         elif isinstance(prop, str):
         elif isinstance(prop, str):

+ 0 - 3
pynecone/event.py

@@ -92,9 +92,6 @@ class EventSpec(Base):
     # The handler on the client to process event.
     # The handler on the client to process event.
     client_handler_name: str = ""
     client_handler_name: str = ""
 
 
-    # The local arguments on the frontend.
-    local_args: Tuple[Var, ...] = ()
-
     # The arguments to pass to the function.
     # The arguments to pass to the function.
     args: Tuple[Tuple[Var, Var], ...] = ()
     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
     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
     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 --no-frontend-only and no --backend-only, then turn on frontend and backend both
     if not frontend and not backend:
     if not frontend and not backend:
         frontend = True
         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),
         dest=str(root / constants.WEB_ASSETS_DIR),
     )
     )
 
 
+    # set the upload url in pynecone.json file
+    set_pynecone_upload_endpoint()
+
     # Disable the Next telemetry.
     # Disable the Next telemetry.
     if disable_telemetry:
     if disable_telemetry:
         subprocess.Popen(
         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
             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:
 def format_full_control_event(event_chain: EventChain) -> str:

+ 1 - 1
pyproject.toml

@@ -1,6 +1,6 @@
 [tool.poetry]
 [tool.poetry]
 name = "pynecone"
 name = "pynecone"
-version = "0.1.29"
+version = "0.1.30"
 description = "Web apps in pure Python."
 description = "Web apps in pure Python."
 license = "Apache-2.0"
 license = "Apache-2.0"
 authors = [
 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}}'),
         ({"a": 1, "b": 2, "c": 3}, '{{"a": 1, "b": 2, "c": 3}}'),
         (
         (
             EventChain(events=[EventSpec(handler=EventHandler(fn=mock_event))]),
             EventChain(events=[EventSpec(handler=EventHandler(fn=mock_event))]),
-            '{() => Event([E("mock_event", {}, )])}',
+            '{(_e) => {_e.preventDefault(); Event([E("mock_event")])}}',
         ),
         ),
         (
         (
             EventChain(
             EventChain(
                 events=[
                 events=[
                     EventSpec(
                     EventSpec(
                         handler=EventHandler(fn=mock_event),
                         handler=EventHandler(fn=mock_event),
-                        local_args=(EVENT_ARG,),
                         args=((Var.create_safe("arg"), EVENT_ARG.target.value),),
                         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"}}'),
         ({"a": "red", "b": "blue"}, '{{"a": "red", "b": "blue"}}'),
         (BaseVar(name="var", type_="int"), "{var}"),
         (BaseVar(name="var", type_="int"), "{var}"),

+ 12 - 15
tests/test_event.py

@@ -45,27 +45,25 @@ def test_call_event_handler():
     event_spec = handler()
     event_spec = handler()
 
 
     assert event_spec.handler == handler
     assert event_spec.handler == handler
-    assert event_spec.local_args == ()
     assert event_spec.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)
     handler = EventHandler(fn=test_fn_with_args)
     event_spec = handler(make_var("first"), make_var("second"))
     event_spec = handler(make_var("first"), make_var("second"))
 
 
     # Test passing vars as args.
     # Test passing vars as args.
     assert event_spec.handler == handler
     assert event_spec.handler == handler
-    assert event_spec.local_args == ()
     assert event_spec.args == (("arg1", "first"), ("arg2", "second"))
     assert event_spec.args == (("arg1", "first"), ("arg2", "second"))
     assert (
     assert (
         format.format_event(event_spec)
         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.
     # Passing args as strings should format differently.
     event_spec = handler("first", "second")  # type: ignore
     event_spec = handler("first", "second")  # type: ignore
     assert (
     assert (
         format.format_event(event_spec)
         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"
     first, second = 123, "456"
@@ -73,11 +71,10 @@ def test_call_event_handler():
     event_spec = handler(first, second)  # type: ignore
     event_spec = handler(first, second)  # type: ignore
     assert (
     assert (
         format.format_event(event_spec)
         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.handler == handler
-    assert event_spec.local_args == ()
     assert event_spec.args == (
     assert event_spec.args == (
         ("arg1", format.json_dumps(first)),
         ("arg1", format.json_dumps(first)),
         ("arg2", format.json_dumps(second)),
         ("arg2", format.json_dumps(second)),
@@ -94,9 +91,9 @@ def test_event_redirect():
     assert isinstance(spec, EventSpec)
     assert isinstance(spec, EventSpec)
     assert spec.handler.fn.__qualname__ == "_redirect"
     assert spec.handler.fn.__qualname__ == "_redirect"
     assert spec.args == (("path", "/path"),)
     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"))
     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():
 def test_event_console_log():
@@ -105,9 +102,9 @@ def test_event_console_log():
     assert isinstance(spec, EventSpec)
     assert isinstance(spec, EventSpec)
     assert spec.handler.fn.__qualname__ == "_console"
     assert spec.handler.fn.__qualname__ == "_console"
     assert spec.args == (("message", "message"),)
     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"))
     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():
 def test_event_window_alert():
@@ -116,9 +113,9 @@ def test_event_window_alert():
     assert isinstance(spec, EventSpec)
     assert isinstance(spec, EventSpec)
     assert spec.handler.fn.__qualname__ == "_alert"
     assert spec.handler.fn.__qualname__ == "_alert"
     assert spec.args == (("message", "message"),)
     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"))
     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():
 def test_set_value():
@@ -130,8 +127,8 @@ def test_set_value():
         ("ref", Var.create_safe("ref_input1")),
         ("ref", Var.create_safe("ref_input1")),
         ("value", ""),
         ("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"))
     spec = event.set_value("input1", Var.create_safe("message"))
     assert (
     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)
     assert str(web_folder) == prerequisites.create_web_directory(tmp_path)
 
 
     mocker.patch("pynecone.utils.prerequisites.install_frontend_packages")
     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)
     build.setup_frontend(tmp_path, disable_telemetry=False)
     assert web_folder.exists()
     assert web_folder.exists()