Add pc.set_value (#835)
This commit is contained in:
parent
a9ee9f6d44
commit
1a254aca8e
@ -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);
|
||||
|
@ -16,6 +16,7 @@ from .event import (
|
||||
EventChain,
|
||||
console_log,
|
||||
redirect,
|
||||
set_value,
|
||||
window_alert,
|
||||
)
|
||||
from .event import FileUpload as upload_files
|
||||
|
@ -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]:
|
||||
|
@ -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."""
|
||||
|
@ -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.
|
||||
|
@ -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(
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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 = [
|
||||
|
@ -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))
|
||||
|
@ -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}"),
|
||||
|
@ -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})'
|
||||
)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user