Add pc.set_value (#835)

This commit is contained in:
Nikhil Rao 2023-04-29 19:01:37 -07:00 committed by GitHub
parent a9ee9f6d44
commit 1a254aca8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 183 additions and 53 deletions

View File

@ -90,6 +90,11 @@ export const applyEvent = async (event, router, socket) => {
return false;
}
if (event.name == "_set_value") {
event.payload.ref.current.value = event.payload.value;
return false;
}
// Send the event to the server.
event.token = getToken();
event.router_data = (({ pathname, query }) => ({ pathname, query }))(router);

View File

@ -16,6 +16,7 @@ from .event import (
EventChain,
console_log,
redirect,
set_value,
window_alert,
)
from .event import FileUpload as upload_files

View File

@ -181,7 +181,7 @@ class Component(Base, ABC):
event_trigger: The event trigger to bind the chain to.
value: The value to create the event chain from.
state_name: The state to be fully controlled.
full_control: Whether full contorolled or not.
full_control: Whether full controlled or not.
Returns:
The event chain.
@ -243,7 +243,7 @@ class Component(Base, ABC):
events = [
EventSpec(
handler=e.handler,
local_args=(EVENT_ARG.name,),
local_args=(EVENT_ARG,),
args=get_handler_args(e, arg),
)
for e in events
@ -461,10 +461,18 @@ class Component(Base, ABC):
)
def _get_hooks(self) -> Optional[str]:
"""Get the React hooks for this component.
Returns:
The hooks for just this component.
"""
ref = self.get_ref()
if ref is not None:
return f"const {ref} = useRef(null);"
return None
def get_hooks(self) -> Set[str]:
"""Get javascript code for react hooks.
"""Get the React hooks for this component and its children.
Returns:
The code that should appear just before returning the rendered component.
@ -483,6 +491,16 @@ class Component(Base, ABC):
return code
def get_ref(self) -> Optional[str]:
"""Get the name of the ref for the component.
Returns:
The ref name.
"""
if self.id is None:
return None
return format.format_ref(self.id)
def get_custom_components(
self, seen: Optional[Set[str]] = None
) -> Set[CustomComponent]:

View File

@ -4,6 +4,7 @@ from typing import Dict
from pynecone.components.component import EVENT_ARG
from pynecone.components.libs.chakra import ChakraComponent
from pynecone.utils import imports
from pynecone.var import Var
@ -48,6 +49,12 @@ class Input(ChakraComponent):
# "lg" | "md" | "sm" | "xs"
size: Var[str]
def _get_imports(self) -> imports.ImportDict:
return imports.merge_imports(
super()._get_imports(),
{"/utils/state": {"set_val"}},
)
@classmethod
def get_controlled_triggers(cls) -> Dict[str, Var]:
"""Get the event triggers that pass the component's value to the handler.
@ -63,6 +70,13 @@ class Input(ChakraComponent):
"on_key_up": EVENT_ARG.key,
}
def _render(self):
out = super()._render()
ref = self.get_ref()
if ref is not None:
out.add_props(ref=Var.create(ref, is_local=False))
return out
class InputGroup(ChakraComponent):
"""The InputGroup component is a component that is used to group a set of inputs."""

View File

@ -73,7 +73,7 @@ class Tag(Base):
# Handle event props.
elif isinstance(prop, EventChain):
local_args = ",".join(prop.events[0].local_args)
local_args = ",".join(([str(a) for a in prop.events[0].local_args]))
if len(prop.events) == 1 and prop.events[0].upload:
# Special case for upload events.

View File

@ -23,7 +23,7 @@ class Event(Base):
router_data: Dict[str, Any] = {}
# The event payload.
payload: Dict[str, Any] = {}
payload: Dict[Any, Any] = {}
class EventHandler(Base):
@ -54,21 +54,18 @@ class EventHandler(Base):
"""
# Get the function args.
fn_args = inspect.getfullargspec(self.fn).args[1:]
fn_args = (Var.create_safe(arg) for arg in fn_args)
# Construct the payload.
values = []
for arg in args:
# If it is a Var, add the full name.
if isinstance(arg, Var):
values.append(arg.full_name)
continue
# Special case for file uploads.
if isinstance(arg, FileUpload):
return EventSpec(handler=self, upload=True)
# Otherwise, convert to JSON.
try:
values.append(format.json_dumps(arg))
values.append(Var.create(arg))
except TypeError as e:
raise TypeError(
f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}."
@ -90,10 +87,10 @@ class EventSpec(Base):
handler: EventHandler
# The local arguments on the frontend.
local_args: Tuple[str, ...] = ()
local_args: Tuple[Var, ...] = ()
# The arguments to pass to the function.
args: Tuple[Any, ...] = ()
args: Tuple[Tuple[Var, Var], ...] = ()
# Whether to upload files.
upload: bool = False
@ -142,7 +139,31 @@ class FileUpload(Base):
# Special server-side events.
def redirect(path: str) -> EventSpec:
def server_side(name: str, **kwargs) -> EventSpec:
"""A server-side event.
Args:
name: The name of the event.
**kwargs: The arguments to pass to the event.
Returns:
An event spec for a server-side event.
"""
def fn():
return None
fn.__qualname__ = name
return EventSpec(
handler=EventHandler(fn=fn),
args=tuple(
(Var.create_safe(k), Var.create_safe(v, is_string=type(v) is str))
for k, v in kwargs.items()
),
)
def redirect(path: Union[str, Var[str]]) -> EventSpec:
"""Redirect to a new path.
Args:
@ -151,18 +172,10 @@ def redirect(path: str) -> EventSpec:
Returns:
An event to redirect to the path.
"""
def fn():
return None
fn.__qualname__ = "_redirect"
return EventSpec(
handler=EventHandler(fn=fn),
args=(("path", path),),
)
return server_side("_redirect", path=path)
def console_log(message: str) -> EventSpec:
def console_log(message: Union[str, Var[str]]) -> EventSpec:
"""Do a console.log on the browser.
Args:
@ -171,18 +184,10 @@ def console_log(message: str) -> EventSpec:
Returns:
An event to log the message.
"""
def fn():
return None
fn.__qualname__ = "_console"
return EventSpec(
handler=EventHandler(fn=fn),
args=(("message", message),),
)
return server_side("_console", message=message)
def window_alert(message: str) -> EventSpec:
def window_alert(message: Union[str, Var[str]]) -> EventSpec:
"""Create a window alert on the browser.
Args:
@ -191,14 +196,21 @@ def window_alert(message: str) -> EventSpec:
Returns:
An event to alert the message.
"""
return server_side("_alert", message=message)
def fn():
return None
fn.__qualname__ = "_alert"
return EventSpec(
handler=EventHandler(fn=fn),
args=(("message", message),),
def set_value(ref: str, value: Any) -> EventSpec:
"""Set the value of a ref.
Args:
ref: The ref.
value: The value to set.
Returns:
An event to set the ref.
"""
return server_side(
"_set_value", ref=Var.create_safe(format.format_ref(ref)), value=value
)
@ -306,7 +318,7 @@ def call_event_fn(fn: Callable, arg: Var) -> List[EventSpec]:
return events
def get_handler_args(event_spec: EventSpec, arg: Var) -> Tuple[Tuple[str, str], ...]:
def get_handler_args(event_spec: EventSpec, arg: Var) -> Tuple[Tuple[Var, Var], ...]:
"""Get the handler args for the given event spec.
Args:
@ -324,7 +336,7 @@ def get_handler_args(event_spec: EventSpec, arg: Var) -> Tuple[Tuple[str, str],
raise ValueError(
f"Event handler has an invalid signature, needed a method with a parameter, got {event_spec.handler}."
)
return event_spec.args if len(args) > 2 else ((args[1], arg.name),)
return event_spec.args if len(args) > 2 else ((Var.create_safe(args[1]), arg),)
def fix_events(
@ -339,8 +351,6 @@ def fix_events(
Returns:
The fixed events.
"""
from pynecone.event import Event, EventHandler, EventSpec
# If the event handler returns nothing, return an empty list.
if events is None:
return []
@ -359,7 +369,7 @@ def fix_events(
e = e()
assert isinstance(e, EventSpec), f"Unexpected event type, {type(e)}."
name = format.format_event_handler(e.handler)
payload = dict(e.args)
payload = {k.name: v.name for k, v in e.args}
# Create an event and append it to the list.
out.append(

View File

@ -286,7 +286,12 @@ def format_event(event_spec: EventSpec) -> str:
Returns:
The compiled event.
"""
args = ",".join([":".join((name, val)) for name, val in event_spec.args])
args = ",".join(
[
":".join((name.name, json.dumps(val.name) if val.is_string else val.name))
for name, val in event_spec.args
]
)
return f"E(\"{format_event_handler(event_spec.handler)}\", {wrap(args, '{')})"
@ -398,6 +403,20 @@ def format_state(value: Any) -> Dict:
)
def format_ref(ref: str) -> str:
"""Format a ref.
Args:
ref: The ref to format.
Returns:
The formatted ref.
"""
# Replace all non-word characters with underscores.
clean_ref = re.sub(r"[^\w]+", "_", ref)
return f"ref_{clean_ref}"
def json_dumps(obj: Any) -> str:
"""Takes an object and returns a jsonified string.

View File

@ -105,6 +105,24 @@ class Var(ABC):
return BaseVar(name=name, type_=type_, is_local=is_local, is_string=is_string)
@classmethod
def create_safe(
cls, value: Any, is_local: bool = True, is_string: bool = False
) -> Var:
"""Create a var from a value, guaranteeing that it is not None.
Args:
value: The value to create the var from.
is_local: Whether the var is local.
is_string: Whether the var is a string literal.
Returns:
The var.
"""
var = cls.create(value, is_local=is_local, is_string=is_string)
assert var is not None
return var
@classmethod
def __class_getitem__(cls, type_: str) -> _GenericAlias:
"""Get a typed var.

View File

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

View File

@ -15,9 +15,9 @@ while ! nc -z localhost 3000 || ! lsof -i :8000 >/dev/null; do
echo "Error: Server process with PID $pid exited early"
break
fi
if ((wait_time >= 200)); then
if ((wait_time >= 300)); then
echo "Error: Timeout waiting for ports 3000 and 8000 to become available"
break
exit 1
fi
sleep 5
((wait_time += 5))

View File

@ -3,7 +3,7 @@ from typing import Any, Dict
import pytest
from pynecone.components.tags import CondTag, Tag
from pynecone.event import EventChain, EventHandler, EventSpec
from pynecone.event import EVENT_ARG, EventChain, EventHandler, EventSpec
from pynecone.var import BaseVar, Var
@ -32,12 +32,12 @@ def mock_event(arg):
events=[
EventSpec(
handler=EventHandler(fn=mock_event),
local_args=("e",),
args=(("arg", "e.target.value"),),
local_args=(EVENT_ARG,),
args=((Var.create_safe("arg"), EVENT_ARG.target.value),),
)
]
),
'{(e) => Event([E("mock_event", {arg:e.target.value})])}',
'{(_e) => Event([E("mock_event", {arg:_e.target.value})])}',
),
({"a": "red", "b": "blue"}, '{{"a": "red", "b": "blue"}}'),
(BaseVar(name="var", type_="int"), "{var}"),

View File

@ -73,6 +73,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"})'
spec = event.redirect(Var.create_safe("path"))
assert format.format_event(spec) == 'E("_redirect", {path:path})'
def test_event_console_log():
@ -81,6 +84,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"})'
spec = event.console_log(Var.create_safe("message"))
assert format.format_event(spec) == 'E("_console", {message:message})'
def test_event_window_alert():
@ -89,3 +95,22 @@ 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"})'
spec = event.window_alert(Var.create_safe("message"))
assert format.format_event(spec) == 'E("_alert", {message:message})'
def test_set_value():
"""Test the event window alert function."""
spec = event.set_value("input1", "")
assert isinstance(spec, EventSpec)
assert spec.handler.fn.__qualname__ == "_set_value"
assert spec.args == (
("ref", Var.create_safe("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})'
)

View File

@ -337,3 +337,23 @@ def test_create_config(app_name, expected_config_name, mocker):
tmpl_mock.format.assert_called_with(
app_name=app_name, config_name=expected_config_name
)
@pytest.mark.parametrize(
"name,expected",
[
("input1", "ref_input1"),
("input 1", "ref_input_1"),
("input-1", "ref_input_1"),
("input_1", "ref_input_1"),
("a long test?1! name", "ref_a_long_test_1_name"),
],
)
def test_format_ref(name, expected):
"""Test formatting a ref.
Args:
name: The name to format.
expected: The expected formatted name.
"""
assert format.format_ref(name) == expected