Skip to content

Commit 5618235

Browse files
committed
add use_linked_inputs
1 parent cf7d037 commit 5618235

File tree

2 files changed

+140
-76
lines changed

2 files changed

+140
-76
lines changed

Diff for: src/idom/widgets.py

+57-23
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
from __future__ import annotations
22

33
from base64 import b64encode
4-
from typing import Any, Callable, Dict, Optional, Set, Tuple, Union
4+
from typing import (
5+
Any,
6+
Callable,
7+
Dict,
8+
List,
9+
Optional,
10+
Sequence,
11+
Set,
12+
Tuple,
13+
TypeVar,
14+
Union,
15+
)
516

617
import idom
718

8-
from . import html
919
from .core import hooks
1020
from .core.component import component
1121
from .core.proto import ComponentConstructor, VdomDict
@@ -35,28 +45,52 @@ def image(
3545
return {"tagName": "img", "attributes": {"src": src, **(attributes or {})}}
3646

3747

38-
@component
39-
def Input(
40-
callback: Callable[[str], None],
41-
type: str,
42-
value: str = "",
43-
attributes: Optional[Dict[str, Any]] = None,
44-
cast: Optional[Callable[[str], Any]] = None,
48+
_CastInput = TypeVar("_CastInput")
49+
50+
51+
def use_linked_inputs(
52+
attributes: Sequence[Dict[str, Any]],
53+
on_change: Callable[[_CastInput], None] = lambda value: None,
54+
cast: Callable[[str], _CastInput] = lambda value: value,
55+
initial_value: str = "",
4556
ignore_empty: bool = True,
46-
) -> VdomDict:
47-
"""Utility for making an ``<input/>`` with a callback"""
48-
attrs = attributes or {}
49-
value, set_value = idom.hooks.use_state(value)
50-
51-
def on_change(event: Dict[str, Any]) -> None:
52-
value = event["target"]["value"]
53-
set_value(value)
54-
if not value and ignore_empty:
55-
return
56-
callback(value if cast is None else cast(value))
57-
58-
attributes = {**attrs, "type": type, "value": value, "onChange": on_change}
59-
return html.input(attributes)
57+
) -> List[VdomDict]:
58+
"""Return a list of linked inputs equal to the number of given attributes.
59+
60+
Parameters:
61+
attributes:
62+
That attributes of each returned input element. If the number of generated
63+
inputs is variable, you may need to assign each one a
64+
:ref:`key <Organizing Items With Keys>` by including a ``"key"`` in each
65+
attribute dictionary.
66+
on_change:
67+
A callback which is triggered when any input is changed. This callback need
68+
not update the 'value' field in the attributes of the inputs since that is
69+
handled automatically.
70+
cast:
71+
Cast the 'value' of changed inputs that is passed to ``on_change``.
72+
initial_value:
73+
Initialize the 'value' field of the inputs.
74+
ignore_empty:
75+
Do not trigger ``on_change`` if the 'value' is an empty string.
76+
"""
77+
value, set_value = idom.hooks.use_state(initial_value)
78+
79+
def sync_inputs(event: Dict[str, Any]) -> None:
80+
new_value = event["value"]
81+
set_value(new_value)
82+
if not new_value and ignore_empty:
83+
return None
84+
on_change(cast(new_value))
85+
86+
keys_and_attrs = [
87+
(attrs.pop("key", None), attrs) for attrs in map(dict.copy, attributes)
88+
]
89+
90+
return [
91+
idom.html.input({**attrs, "onChange": sync_inputs, "value": value}, key=key)
92+
for key, attrs in keys_and_attrs
93+
]
6094

6195

6296
MountFunc = Callable[[ComponentConstructor], None]

Diff for: tests/test_widgets.py

+83-53
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import time
21
from base64 import b64encode
32
from pathlib import Path
43

@@ -82,71 +81,102 @@ def test_image_from_bytes(driver, display):
8281
assert BASE64_IMAGE_SRC in client_img.get_attribute("src")
8382

8483

85-
def test_input_callback(driver, driver_wait, display):
86-
inp_ref = idom.Ref(None)
84+
def test_use_linked_inputs(driver, driver_wait, display):
85+
@idom.component
86+
def SomeComponent():
87+
i_1, i_2 = idom.widgets.use_linked_inputs([{"id": "i_1"}, {"id": "i_2"}])
88+
return idom.html.div(i_1, i_2)
89+
90+
display(SomeComponent)
91+
92+
input_1 = driver.find_element("id", "i_1")
93+
input_2 = driver.find_element("id", "i_2")
94+
95+
send_keys(input_1, "hello")
96+
97+
driver_wait.until(lambda d: input_1.get_attribute("value") == "hello")
98+
driver_wait.until(lambda d: input_2.get_attribute("value") == "hello")
99+
100+
send_keys(input_2, " world")
101+
102+
driver_wait.until(lambda d: input_1.get_attribute("value") == "hello world")
103+
driver_wait.until(lambda d: input_2.get_attribute("value") == "hello world")
104+
105+
106+
def test_use_linked_inputs_on_change(driver, driver_wait, display):
107+
value = idom.Ref(None)
108+
109+
@idom.component
110+
def SomeComponent():
111+
i_1, i_2 = idom.widgets.use_linked_inputs(
112+
[{"id": "i_1"}, {"id": "i_2"}],
113+
on_change=value.set_current,
114+
)
115+
return idom.html.div(i_1, i_2)
116+
117+
display(SomeComponent)
87118

88-
display(
89-
lambda: idom.widgets.Input(
90-
lambda value: setattr(inp_ref, "current", value),
91-
"text",
92-
"initial-value",
93-
{"id": "inp"},
119+
input_1 = driver.find_element("id", "i_1")
120+
input_2 = driver.find_element("id", "i_2")
121+
122+
send_keys(input_1, "hello")
123+
124+
driver_wait.until(lambda d: value.current == "hello")
125+
126+
send_keys(input_2, " world")
127+
128+
driver_wait.until(lambda d: value.current == "hello world")
129+
130+
131+
def test_use_linked_inputs_on_change_with_cast(driver, driver_wait, display):
132+
value = idom.Ref(None)
133+
134+
@idom.component
135+
def SomeComponent():
136+
i_1, i_2 = idom.widgets.use_linked_inputs(
137+
[{"id": "i_1"}, {"id": "i_2"}], on_change=value.set_current, cast=int
94138
)
95-
)
139+
return idom.html.div(i_1, i_2)
140+
141+
display(SomeComponent)
142+
143+
input_1 = driver.find_element("id", "i_1")
144+
input_2 = driver.find_element("id", "i_2")
96145

97-
client_inp = driver.find_element("id", "inp")
98-
assert client_inp.get_attribute("value") == "initial-value"
146+
send_keys(input_1, "1")
99147

100-
client_inp.clear()
101-
send_keys(client_inp, "new-value-1")
102-
driver_wait.until(lambda dvr: inp_ref.current == "new-value-1")
148+
driver_wait.until(lambda d: value.current == 1)
103149

104-
client_inp.clear()
105-
send_keys(client_inp, "new-value-2")
106-
driver_wait.until(lambda dvr: client_inp.get_attribute("value") == "new-value-2")
150+
send_keys(input_2, "2")
107151

152+
driver_wait.until(lambda d: value.current == 12)
108153

109-
def test_input_ignore_empty(driver, driver_wait, display):
110-
# ignore empty since that's an invalid float
111-
inp_ingore_ref = idom.Ref("1")
112-
inp_not_ignore_ref = idom.Ref("1")
154+
155+
def test_use_linked_inputs_ignore_empty(driver, driver_wait, display):
156+
value = idom.Ref(None)
113157

114158
@idom.component
115-
def InputWrapper():
116-
return idom.html.div(
117-
idom.widgets.Input(
118-
lambda value: setattr(inp_ingore_ref, "current", value),
119-
"number",
120-
inp_ingore_ref.current,
121-
{"id": "inp-ignore"},
122-
ignore_empty=True,
123-
),
124-
idom.widgets.Input(
125-
lambda value: setattr(inp_not_ignore_ref, "current", value),
126-
"number",
127-
inp_not_ignore_ref.current,
128-
{"id": "inp-not-ignore"},
129-
ignore_empty=False,
130-
),
159+
def SomeComponent():
160+
i_1, i_2 = idom.widgets.use_linked_inputs(
161+
[{"id": "i_1"}, {"id": "i_2"}],
162+
on_change=value.set_current,
163+
ignore_empty=True,
131164
)
165+
return idom.html.div(i_1, i_2)
166+
167+
display(SomeComponent)
168+
169+
input_1 = driver.find_element("id", "i_1")
170+
input_2 = driver.find_element("id", "i_2")
132171

133-
display(InputWrapper)
172+
send_keys(input_1, "1")
134173

135-
client_inp_ignore = driver.find_element("id", "inp-ignore")
136-
client_inp_not_ignore = driver.find_element("id", "inp-not-ignore")
174+
driver_wait.until(lambda d: value.current == "1")
137175

138-
send_keys(client_inp_ignore, Keys.BACKSPACE)
139-
time.sleep(0.1) # waiting and deleting again seems to decrease flakiness
140-
send_keys(client_inp_ignore, Keys.BACKSPACE)
176+
send_keys(input_2, Keys.BACKSPACE)
141177

142-
send_keys(client_inp_not_ignore, Keys.BACKSPACE)
143-
time.sleep(0.1) # waiting and deleting again seems to decrease flakiness
144-
send_keys(client_inp_not_ignore, Keys.BACKSPACE)
178+
assert value.current == "1"
145179

146-
driver_wait.until(lambda drv: client_inp_ignore.get_attribute("value") == "")
147-
driver_wait.until(lambda drv: client_inp_not_ignore.get_attribute("value") == "")
180+
send_keys(input_1, "2")
148181

149-
# ignored empty value on change
150-
assert inp_ingore_ref.current == "1"
151-
# did not ignore empty value on change
152-
assert inp_not_ignore_ref.current == ""
182+
driver_wait.until(lambda d: value.current == "2")

0 commit comments

Comments
 (0)