Skip to content

Commit ad279f5

Browse files
committed
initial work to unify vdom constructor interface
1 parent cc9518d commit ad279f5

File tree

9 files changed

+67
-100
lines changed

9 files changed

+67
-100
lines changed

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
session-name: test_python_suite
2424
session-arguments: --maxfail=3 --no-cov
2525
runs-on-array: '["ubuntu-latest", "macos-latest", "windows-latest"]'
26-
python-version-array: '["3.7", "3.8", "3.9", "3.10"]'
26+
python-version-array: '["3.8", "3.9", "3.10", "3.11"]'
2727
docs:
2828
uses: ./.github/workflows/.nox-session.yml
2929
with:

src/idom/core/events.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def event(
4141
4242
.. code-block:: python
4343
44-
element = idom.html.button({"onClick": my_callback})
44+
element = idom.html.button(on_click=my_callback)
4545
4646
You may want the ability to prevent the default action associated with the event
4747
from taking place, or stoping the event from propagating up the DOM. This decorator
@@ -53,7 +53,7 @@ def event(
5353
def my_callback(*data):
5454
...
5555
56-
element = idom.html.button({"onClick": my_callback})
56+
element = idom.html.button(on_click=my_callback)
5757
5858
Parameters:
5959
function:

src/idom/core/types.py

-7
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,6 @@ async def __aexit__(
104104
VdomChildren = Sequence[VdomChild]
105105
"""Describes a series of :class:`VdomChild` elements"""
106106

107-
VdomAttributesAndChildren = Union[
108-
Mapping[str, Any], # this describes both VdomDict and VdomAttributes
109-
Iterable[VdomChild],
110-
VdomChild,
111-
]
112-
"""Useful for the ``*attributes_and_children`` parameter in :func:`idom.core.vdom.vdom`"""
113-
114107

115108
class _VdomDictOptional(TypedDict, total=False):
116109
key: Key | None

src/idom/core/vdom.py

+47-61
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
EventHandlerMapping,
1818
EventHandlerType,
1919
ImportSourceDict,
20-
VdomAttributesAndChildren,
20+
Key,
21+
VdomChild,
2122
VdomDict,
2223
VdomJson,
2324
)
@@ -129,43 +130,47 @@ def is_vdom(value: Any) -> bool:
129130

130131
def vdom(
131132
tag: str,
132-
*attributes_and_children: VdomAttributesAndChildren,
133-
key: str | int | None = None,
134-
event_handlers: Optional[EventHandlerMapping] = None,
135-
import_source: Optional[ImportSourceDict] = None,
133+
/,
134+
*children: VdomChild,
135+
key: Key | None = None,
136+
event_handlers: EventHandlerMapping | None = None,
137+
import_source: ImportSourceDict | None = None,
138+
**attributes: Any,
136139
) -> VdomDict:
137140
"""A helper function for creating VDOM dictionaries.
138141
139142
Parameters:
140143
tag:
141144
The type of element (e.g. 'div', 'h1', 'img')
142-
attributes_and_children:
143-
An optional attribute mapping followed by any number of children or
144-
iterables of children. The attribute mapping **must** precede the children,
145-
or children which will be merged into their respective parts of the model.
145+
children:
146+
String, compoennts, or other VDOM elements that are this element's children.
146147
key:
147-
A string idicating the identity of a particular element. This is significant
148-
to preserve event handlers across updates - without a key, a re-render would
149-
cause these handlers to be deleted, but with a key, they would be redirected
150-
to any newly defined handlers.
148+
A string or integer idicating the identity of a particular element. This is
149+
significant to preserve event handlers across updates - without a key, a
150+
re-render would cause these handlers to be deleted, but with a key, they
151+
would be redirected to any newly defined handlers.
151152
event_handlers:
152-
Maps event types to coroutines that are responsible for handling those events.
153+
Maps event types to coroutines that are responsible for handling those
154+
events.
153155
import_source:
154156
(subject to change) specifies javascript that, when evaluated returns a
155157
React component.
158+
attributes:
159+
Remaining attributes of this element.
156160
"""
157161
model: VdomDict = {"tagName": tag}
158162

159-
attributes, children = coalesce_attributes_and_children(attributes_and_children)
160163
attributes, event_handlers = separate_attributes_and_event_handlers(
161-
attributes, event_handlers or {}
164+
attributes, event_handlers
162165
)
163166

164167
if attributes:
168+
if "cls" in attributes:
169+
attributes["class"] = attributes.pop("cls")
165170
model["attributes"] = attributes
166171

167172
if children:
168-
model["children"] = children
173+
model["children"] = flatten_children(children)
169174

170175
if event_handlers:
171176
model["eventHandlers"] = event_handlers
@@ -179,17 +184,6 @@ def vdom(
179184
return model
180185

181186

182-
class _VdomDictConstructor(Protocol):
183-
def __call__(
184-
self,
185-
*attributes_and_children: VdomAttributesAndChildren,
186-
key: str | int | None = ...,
187-
event_handlers: Optional[EventHandlerMapping] = ...,
188-
import_source: Optional[ImportSourceDict] = ...,
189-
) -> VdomDict:
190-
...
191-
192-
193187
def make_vdom_constructor(
194188
tag: str, allow_children: bool = True
195189
) -> _VdomDictConstructor:
@@ -199,19 +193,8 @@ def make_vdom_constructor(
199193
first ``tag`` argument.
200194
"""
201195

202-
def constructor(
203-
*attributes_and_children: VdomAttributesAndChildren,
204-
key: str | int | None = None,
205-
event_handlers: Optional[EventHandlerMapping] = None,
206-
import_source: Optional[ImportSourceDict] = None,
207-
) -> VdomDict:
208-
model = vdom(
209-
tag,
210-
*attributes_and_children,
211-
key=key,
212-
event_handlers=event_handlers,
213-
import_source=import_source,
214-
)
196+
def constructor(*args: Any, **kwargs: Any) -> VdomDict:
197+
model = vdom(*args, **kwargs)
215198
if not allow_children and "children" in model:
216199
raise TypeError(f"{tag!r} nodes cannot have children.")
217200
return model
@@ -232,35 +215,24 @@ def constructor(
232215
return constructor
233216

234217

235-
def coalesce_attributes_and_children(
236-
values: Sequence[Any],
237-
) -> Tuple[Mapping[str, Any], List[Any]]:
238-
if not values:
239-
return {}, []
240-
241-
children_or_iterables: Sequence[Any]
242-
attributes, *children_or_iterables = values
243-
if not _is_attributes(attributes):
244-
attributes = {}
245-
children_or_iterables = values
246-
247-
children: List[Any] = []
248-
for child in children_or_iterables:
218+
def flatten_children(children: Sequence[VdomChild]) -> Sequence[VdomChild]:
219+
child_list: list[VdomChild] = []
220+
for child in children:
249221
if _is_single_child(child):
250-
children.append(child)
222+
child_list.append(child)
251223
else:
252-
children.extend(child)
253-
254-
return attributes, children
224+
child_list.extend(child)
225+
return child_list
255226

256227

257228
def separate_attributes_and_event_handlers(
258-
attributes: Mapping[str, Any], event_handlers: EventHandlerMapping
229+
attributes: Mapping[str, Any],
230+
event_handlers: EventHandlerMapping | None = None,
259231
) -> Tuple[Dict[str, Any], EventHandlerDict]:
260232
separated_attributes = {}
261233
separated_event_handlers: Dict[str, List[EventHandlerType]] = {}
262234

263-
for k, v in event_handlers.items():
235+
for k, v in (event_handlers or {}).items():
264236
separated_event_handlers[k] = [v]
265237

266238
for k, v in attributes.items():
@@ -339,3 +311,17 @@ def _is_single_child(value: Any) -> bool:
339311
class _EllipsisRepr:
340312
def __repr__(self) -> str:
341313
return "..."
314+
315+
316+
class _VdomDictConstructor(Protocol):
317+
def __call__(
318+
self,
319+
tag: str,
320+
/,
321+
*children: VdomChild,
322+
key: str | int | None = None,
323+
event_handlers: EventHandlerMapping | None = None,
324+
import_source: ImportSourceDict | None = None,
325+
**attributes: Any,
326+
) -> VdomDict:
327+
...

src/idom/html.py

+5-12
Original file line numberDiff line numberDiff line change
@@ -161,18 +161,15 @@
161161
from typing import Any, Mapping
162162

163163
from .core.types import Key, VdomDict
164-
from .core.vdom import coalesce_attributes_and_children, make_vdom_constructor
164+
from .core.vdom import flatten_children, make_vdom_constructor
165165

166166

167167
def _(*children: Any, key: Key | None = None) -> VdomDict:
168168
"""An HTML fragment - this element will not appear in the DOM"""
169-
attributes, coalesced_children = coalesce_attributes_and_children(children)
170-
if attributes:
171-
raise TypeError("Fragments cannot have attributes")
172169
model: VdomDict = {"tagName": ""}
173170

174-
if coalesced_children:
175-
model["children"] = coalesced_children
171+
if children:
172+
model["children"] = flatten_children(children)
176173

177174
if key is not None:
178175
model["key"] = key
@@ -276,10 +273,7 @@ def _(*children: Any, key: Key | None = None) -> VdomDict:
276273
noscript = make_vdom_constructor("noscript")
277274

278275

279-
def script(
280-
*attributes_and_children: Mapping[str, Any] | str,
281-
key: str | int | None = None,
282-
) -> VdomDict:
276+
def script(*children: str, key: Key | None = None, **attributes: Any) -> VdomDict:
283277
"""Create a new `<{script}> <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script>`__ element.
284278
285279
This behaves slightly differently than a normal script element in that it may be run
@@ -295,9 +289,8 @@ def script(
295289
"""
296290
model: VdomDict = {"tagName": "script"}
297291

298-
attributes, children = coalesce_attributes_and_children(attributes_and_children)
299-
300292
if children:
293+
children = flatten_children(children)
301294
if len(children) > 1:
302295
raise ValueError("'script' nodes may have, at most, one child.")
303296
elif not isinstance(children[0], str):

src/idom/testing/common.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ class StaticEventHandler:
154154
def MyComponent():
155155
state, set_state = idom.hooks.use_state(0)
156156
handler = static_handler.use(lambda event: set_state(state + 1))
157-
return idom.html.button({"onClick": handler}, "Click me!")
157+
return idom.html.button("Click me!", on_click=handler)
158158
159159
# gives the target ID for onClick where from the last render of MyComponent
160160
static_handlers.target
@@ -177,7 +177,7 @@ def Parent():
177177
def Child(key):
178178
state, set_state = idom.hooks.use_state(0)
179179
handler = static_handlers_by_key[key].use(lambda event: set_state(state + 1))
180-
return idom.html.button({"onClick": handler}, "Click me!")
180+
return idom.html.button("Click me!", on_click=handler)
181181
182182
# grab the individual targets for each instance above
183183
first_target = static_handlers_by_key["first"].target

tests/test_client.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ async def test_automatic_reconnect(browser: Browser):
2222

2323
@idom.component
2424
def OldComponent():
25-
return idom.html.p({"id": "old-component"}, "old")
25+
return idom.html.p("old", id="old-component")
2626

2727
async with AsyncExitStack() as exit_stack:
2828
server = await exit_stack.enter_async_context(BackendFixture(port=port))
@@ -43,7 +43,7 @@ def OldComponent():
4343
@idom.component
4444
def NewComponent():
4545
state, set_state.current = idom.hooks.use_state(0)
46-
return idom.html.p({"id": f"new-component-{state}"}, f"new-{state}")
46+
return idom.html.p(f"new-{state}", id=f"new-component-{state}")
4747

4848
async with AsyncExitStack() as exit_stack:
4949
server = await exit_stack.enter_async_context(BackendFixture(port=port))
@@ -111,13 +111,13 @@ async def test_slow_server_response_on_input_change(display: DisplayFixture):
111111

112112
@idom.component
113113
def SomeComponent():
114-
value, set_value = idom.hooks.use_state("")
114+
_, set_value = idom.hooks.use_state("")
115115

116116
async def handle_change(event):
117117
await asyncio.sleep(delay)
118118
set_value(event["target"]["value"])
119119

120-
return idom.html.input({"onChange": handle_change, "id": "test-input"})
120+
return idom.html.input(on_change=handle_change, id="test-input")
121121

122122
await display.show(SomeComponent)
123123

tests/test_html.py

+4-9
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ async def test_script_mount_unmount(display: DisplayFixture):
1313
def Root():
1414
is_mounted, toggle_is_mounted.current = use_toggle(True)
1515
return html.div(
16-
html.div({"id": "mount-state", "data-value": False}),
16+
html.div(id="mount-state", data_value=False),
1717
HasScript() if is_mounted else html.div(),
1818
)
1919

@@ -53,8 +53,8 @@ async def test_script_re_run_on_content_change(display: DisplayFixture):
5353
def HasScript():
5454
count, incr_count.current = use_counter(1)
5555
return html.div(
56-
html.div({"id": "mount-count", "data-value": 0}),
57-
html.div({"id": "unmount-count", "data-value": 0}),
56+
html.div(id="mount-count", data_value=0),
57+
html.div(id="unmount-count", data_value=0),
5858
html.script(
5959
f"""() => {{
6060
const mountCountEl = document.getElementById("mount-count");
@@ -101,7 +101,7 @@ def HasScript():
101101
return html.div()
102102
else:
103103
return html.div(
104-
html.div({"id": "run-count", "data-value": 0}),
104+
html.div(id="run-count", data_value=0),
105105
html.script(
106106
{
107107
"src": f"/_idom/modules/{file_name_template.format(src_id=src_id)}"
@@ -148,8 +148,3 @@ def test_simple_fragment():
148148
"key": "something",
149149
"children": [1, 2, 3],
150150
}
151-
152-
153-
def test_fragment_can_have_no_attributes():
154-
with pytest.raises(TypeError, match="Fragments cannot have attributes"):
155-
html._({"some-attribute": 1})

tests/test_widgets.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def make_next_count_constructor(count):
2424

2525
def constructor():
2626
count.current += 1
27-
return idom.html.div({"id": f"hotswap-{count.current}"}, count.current)
27+
return idom.html.div(count.current, id=f"hotswap-{count.current}")
2828

2929
return constructor
3030

@@ -35,7 +35,7 @@ def ButtonSwapsDivs():
3535
async def on_click(event):
3636
mount(make_next_count_constructor(count))
3737

38-
incr = idom.html.button({"onClick": on_click, "id": "incr-button"}, "incr")
38+
incr = idom.html.button("incr", on_click=on_click, id="incr-button")
3939

4040
mount, make_hostswap = idom.widgets.hotswap(update_on_change=True)
4141
mount(make_next_count_constructor(count))

0 commit comments

Comments
 (0)