diff --git a/pynecone/.templates/web/utils/state.js b/pynecone/.templates/web/utils/state.js index fb1010664..946b6ee1b 100644 --- a/pynecone/.templates/web/utils/state.js +++ b/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; } diff --git a/pynecone/components/__init__.py b/pynecone/components/__init__.py index f056d65d1..37cd49dd9 100644 --- a/pynecone/components/__init__.py +++ b/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 diff --git a/pynecone/components/component.py b/pynecone/components/component.py index 1ee94c876..53c6ce635 100644 --- a/pynecone/components/component.py +++ b/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() diff --git a/pynecone/components/forms/__init__.py b/pynecone/components/forms/__init__.py index 1d424c742..4fd13b933 100644 --- a/pynecone/components/forms/__init__.py +++ b/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 ( diff --git a/pynecone/components/forms/button.py b/pynecone/components/forms/button.py index 40cc6c19a..b2b37dbca 100644 --- a/pynecone/components/forms/button.py +++ b/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.""" diff --git a/pynecone/components/forms/formcontrol.py b/pynecone/components/forms/formcontrol.py index ecb81a654..cbb3d6388 100644 --- a/pynecone/components/forms/formcontrol.py +++ b/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.""" diff --git a/pynecone/components/tags/tag.py b/pynecone/components/tags/tag.py index 44ce65906..20b9add54 100644 --- a/pynecone/components/tags/tag.py +++ b/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): diff --git a/pynecone/event.py b/pynecone/event.py index 5178cf940..910f7f5ea 100644 --- a/pynecone/event.py +++ b/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], ...] = () diff --git a/pynecone/pc.py b/pynecone/pc.py index cd55d5e73..824aa56c4 100644 --- a/pynecone/pc.py +++ b/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 diff --git a/pynecone/utils/build.py b/pynecone/utils/build.py index 7ebc35ee0..33dc66f58 100644 --- a/pynecone/utils/build.py +++ b/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( diff --git a/pynecone/utils/format.py b/pynecone/utils/format.py index 774c17361..ce233b38a 100644 --- a/pynecone/utils/format.py +++ b/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: diff --git a/pyproject.toml b/pyproject.toml index eb2b31951..8c0bf8dc9 100644 --- a/pyproject.toml +++ b/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 = [ diff --git a/tests/components/test_tag.py b/tests/components/test_tag.py index 42c059258..2e53d0517 100644 --- a/tests/components/test_tag.py +++ b/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}"), diff --git a/tests/test_event.py b/tests/test_event.py index f39068373..691d02006 100644 --- a/tests/test_event.py +++ b/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})' ) diff --git a/tests/test_utils.py b/tests/test_utils.py index f3e57e983..c22451ca0 100644 --- a/tests/test_utils.py +++ b/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()