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") { 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;
} }

View File

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

View File

@ -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()} props = {
attr[:-1] if attr.endswith("_") else attr: getattr(self, attr)
# Special case for props named `type_`. for attr in self.get_props()
if hasattr(self, "type_"): }
props["type"] = self.type_ # type: ignore
if hasattr(self, "as_"):
props["as"] = self.as_ # type: ignore
# 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()

View File

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

View File

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

View File

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

View File

@ -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}])" event = f"{{{EVENT_ARG}.preventDefault(); Event([{chain}])}}"
prop = f"({local_args}) => {event}" prop = f"({EVENT_ARG}) => {event}"
# Handle other types. # Handle other types.
elif isinstance(prop, str): elif isinstance(prop, str):

View File

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

View File

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

View File

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

View File

@ -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 = '"' event_args = [
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 ''})" 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:

View File

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

View File

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

View File

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

View File

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