Add pc.form component (#929)

This commit is contained in:
Nikhil Rao 2023-05-14 22:34:10 -07:00 committed by GitHub
parent d0e383d23c
commit 948ec90fc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 60 additions and 41 deletions

View File

@ -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;
}

View File

@ -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

View File

@ -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()

View File

@ -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 (

View File

@ -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."""

View File

@ -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."""

View File

@ -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):

View File

@ -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], ...] = ()

View File

@ -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

View File

@ -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(

View File

@ -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:

View File

@ -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 = [

View File

@ -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}"),

View File

@ -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})'
)

View File

@ -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()