Skip to content

Commit 61d7457

Browse files
committed
add use_linked_inputs
1 parent cf7d037 commit 61d7457

File tree

2 files changed

+153
-75
lines changed

2 files changed

+153
-75
lines changed

src/idom/widgets.py

+70-22
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
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+
)
16+
17+
from typing_extensions import Protocol
518

619
import idom
720

@@ -35,28 +48,63 @@ def image(
3548
return {"tagName": "img", "attributes": {"src": src, **(attributes or {})}}
3649

3750

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,
51+
_Value = TypeVar("_Value")
52+
53+
54+
def use_linked_inputs(
55+
attributes: Sequence[Dict[str, Any]],
56+
on_change: Callable[[_Value], None] = lambda value: None,
57+
cast: _CastFunc[_Value] = lambda value: value,
58+
initial_value: str = "",
4559
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)
60+
) -> List[VdomDict]:
61+
"""Return a list of linked inputs equal to the number of given attributes.
62+
63+
Parameters:
64+
attributes:
65+
That attributes of each returned input element. If the number of generated
66+
inputs is variable, you may need to assign each one a
67+
:ref:`key <Organizing Items With Keys>` by including a ``"key"`` in each
68+
attribute dictionary.
69+
on_change:
70+
A callback which is triggered when any input is changed. This callback need
71+
not update the 'value' field in the attributes of the inputs since that is
72+
handled automatically.
73+
cast:
74+
Cast the 'value' of changed inputs that is passed to ``on_change``.
75+
initial_value:
76+
Initialize the 'value' field of the inputs.
77+
ignore_empty:
78+
Do not trigger ``on_change`` if the 'value' is an empty string.
79+
"""
80+
value, set_value = idom.hooks.use_state(initial_value)
81+
82+
def sync_inputs(event: Dict[str, Any]) -> None:
83+
new_value = event["value"]
84+
set_value(new_value)
85+
if not new_value and ignore_empty:
86+
return None
87+
on_change(cast(new_value))
88+
89+
inputs: list[VdomDict] = []
90+
for attrs in attributes:
91+
# we're going to mutate this so copy it
92+
attrs = attrs.copy()
93+
94+
key = attrs.pop("key", None)
95+
attrs.update({"onChange": sync_inputs, "value": value})
96+
97+
inputs.append(html.input(attrs, key=key))
98+
99+
return inputs
100+
101+
102+
_CastTo = TypeVar("_CastTo", covariant=True)
103+
104+
105+
class _CastFunc(Protocol[_CastTo]):
106+
def __call__(self, value: str) -> _CastTo:
107+
...
60108

61109

62110
MountFunc = Callable[[ComponentConstructor], None]

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)