Skip to content

Commit a1746f3

Browse files
committed
Remove key from VdomDict
1 parent 8a9737a commit a1746f3

File tree

10 files changed

+30
-39
lines changed

10 files changed

+30
-39
lines changed

docs/source/about/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Unreleased
3939

4040
**Changed**
4141

42+
- :pull:`1284` - The ``key`` attribute is now stored within ``attributes`` in the VDOM spec.
4243
- :pull:`1251` - Substitute client-side usage of ``react`` with ``preact``.
4344
- :pull:`1239` - Script elements no longer support behaving like effects. They now strictly behave like plain HTML scripts.
4445
- :pull:`1255` - The ``reactpy.html`` module has been modified to allow for auto-creation of any HTML nodes. For example, you can create a ``<data-table>`` element by calling ``html.data_table()``.

src/js/packages/@reactpy/client/src/components.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ function StandardElement({ model }: { model: ReactPyVdom }) {
6767
model.tagName === "" ? Fragment : model.tagName,
6868
createAttributes(model, client),
6969
...createChildren(model, (child) => {
70-
return <Element model={child} key={child.key} />;
70+
return <Element model={child} key={child.attributes?.key} />;
7171
}),
7272
);
7373
}
@@ -100,7 +100,7 @@ function UserInputElement({ model }: { model: ReactPyVdom }): JSX.Element {
100100
// overwrite
101101
{ ...props, value },
102102
...createChildren(model, (child) => (
103-
<Element model={child} key={child.key} />
103+
<Element model={child} key={child.attributes?.key} />
104104
)),
105105
);
106106
}
@@ -135,7 +135,7 @@ function ScriptElement({ model }: { model: ReactPyVdom }) {
135135
return () => {
136136
ref.current?.removeChild(scriptElement);
137137
};
138-
}, [model.key]);
138+
}, [model.attributes?.key]);
139139

140140
return <div ref={ref} />;
141141
}

src/js/packages/@reactpy/client/src/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ export type ReactPyComponent = ComponentType<{ model: ReactPyVdom }>;
4848

4949
export type ReactPyVdom = {
5050
tagName: string;
51-
key?: string;
5251
attributes?: { [key: string]: string };
5352
children?: (ReactPyVdom | string)[];
5453
error?: string;

src/reactpy/_html.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from reactpy.core.vdom import Vdom
77
from reactpy.types import (
88
EventHandlerDict,
9-
Key,
109
VdomAttributes,
1110
VdomChild,
1211
VdomChildren,
@@ -100,29 +99,26 @@
10099
def _fragment(
101100
attributes: VdomAttributes,
102101
children: Sequence[VdomChild],
103-
key: Key | None,
104102
event_handlers: EventHandlerDict,
105103
) -> VdomDict:
106104
"""An HTML fragment - this element will not appear in the DOM"""
107-
attributes.pop("key", None)
108-
if attributes or event_handlers:
105+
if any(k != "key" for k in attributes) or event_handlers:
109106
msg = "Fragments cannot have attributes besides 'key'"
110107
raise TypeError(msg)
111108
model = VdomDict(tagName="")
112109

113110
if children:
114111
model["children"] = children
115112

116-
if key is not None:
117-
model["key"] = key
113+
if attributes:
114+
model["attributes"] = attributes
118115

119116
return model
120117

121118

122119
def _script(
123120
attributes: VdomAttributes,
124121
children: Sequence[VdomChild],
125-
key: Key | None,
126122
event_handlers: EventHandlerDict,
127123
) -> VdomDict:
128124
"""Create a new `<script> <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script>`__ element.
@@ -148,6 +144,8 @@ def _script(
148144
msg = "'script' elements do not support event handlers"
149145
raise ValueError(msg)
150146

147+
key = attributes.get("key")
148+
151149
if children:
152150
if len(children) > 1:
153151
msg = "'script' nodes may have, at most, one child."
@@ -165,7 +163,9 @@ def _script(
165163
key = attributes["src"]
166164

167165
if key is not None:
168-
model["key"] = key
166+
if "attributes" not in model:
167+
model["attributes"] = {}
168+
model["attributes"]["key"] = key
169169

170170
return model
171171

src/reactpy/core/layout.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,9 @@ async def _render_model(
303303
except Exception as e: # nocov
304304
msg = f"Expected a VDOM element dict, not {raw_model}"
305305
raise ValueError(msg) from e
306-
if "key" in raw_model:
307-
new_state.key = new_state.model.current["key"] = raw_model["key"]
306+
key = raw_model.get("attributes", {}).get("key")
307+
if key is not None:
308+
new_state.key = key
308309
if "importSource" in raw_model:
309310
new_state.model.current["importSource"] = raw_model["importSource"]
310311
self._render_model_attributes(old_state, new_state, raw_model)
@@ -726,7 +727,7 @@ def _get_children_info(
726727
continue
727728
elif isinstance(child, dict):
728729
child_type = _DICT_TYPE
729-
key = child.get("key")
730+
key = child.get("attributes", {}).get("key")
730731
elif isinstance(child, Component):
731732
child_type = _COMPONENT_TYPE
732733
key = child.key

src/reactpy/core/vdom.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
"type": "object",
4242
"properties": {
4343
"tagName": {"type": "string"},
44-
"key": {"type": ["string", "number", "null"]},
4544
"error": {"type": "string"},
4645
"children": {"$ref": "#/definitions/elementChildren"},
4746
"attributes": {"type": "object"},
@@ -170,7 +169,6 @@ def __call__(
170169
) -> VdomDict:
171170
"""The entry point for the VDOM API, for example reactpy.html(<WE_ARE_HERE>)."""
172171
attributes, children = separate_attributes_and_children(attributes_and_children)
173-
key = attributes.get("key", None)
174172
attributes, event_handlers, inline_javascript = (
175173
separate_attributes_handlers_and_inline_javascript(attributes)
176174
)
@@ -180,7 +178,6 @@ def __call__(
180178
# Run custom constructor, if defined
181179
if self.custom_constructor:
182180
result = self.custom_constructor(
183-
key=key,
184181
children=children,
185182
attributes=attributes,
186183
event_handlers=event_handlers,
@@ -189,7 +186,6 @@ def __call__(
189186
# Otherwise, use the default constructor
190187
else:
191188
result = {
192-
**({"key": key} if key is not None else {}),
193189
**({"children": children} if children else {}),
194190
**({"attributes": attributes} if attributes else {}),
195191
**({"eventHandlers": event_handlers} if event_handlers else {}),
@@ -277,7 +273,9 @@ def _validate_child_key_integrity(value: Any) -> None:
277273
for child in value:
278274
if isinstance(child, Component) and child.key is None:
279275
warn(f"Key not specified for child in list {child}", UserWarning)
280-
elif isinstance(child, Mapping) and "key" not in child:
276+
elif isinstance(child, Mapping) and "key" not in child.get(
277+
"attributes", {}
278+
):
281279
# remove 'children' to reduce log spam
282280
child_copy = {**child, "children": EllipsisRepr()}
283281
warn(f"Key not specified for child in list {child_copy}", UserWarning)

src/reactpy/transforms.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ def infer_key_from_attributes(vdom: VdomDict) -> None:
113113
if key is None and vdom["tagName"] in {"input", "select", "textarea"}:
114114
key = attributes.get("name")
115115

116-
if key:
117-
vdom["key"] = key
116+
if key and "key" not in attributes:
117+
attributes["key"] = key
118118

119119
def intercept_link_clicks(self, vdom: VdomDict) -> None:
120120
"""Intercepts anchor link clicks and prevents the default behavior.

src/reactpy/types.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ class CssStyleTypeDict(TypedDict, total=False):
533533
zIndex: str | int
534534

535535

536-
# TODO: Enable `extra_items` on `CssStyleDict` when PEP 728 is merged, likely in Python 3.14. Ref: https://peps.python.org/pep-0728/
536+
# TODO: Enable `extra_items` on `CssStyleDict` when PEP 728 is merged, likely in Python 3.15. Ref: https://peps.python.org/pep-0728/
537537
CssStyleDict = CssStyleTypeDict | dict[str, Any]
538538

539539
EventFunc = Callable[[dict[str, Any]], Awaitable[None] | None]
@@ -797,7 +797,6 @@ class DangerouslySetInnerHTML(TypedDict):
797797

798798
VdomDictKeys = Literal[
799799
"tagName",
800-
"key",
801800
"children",
802801
"attributes",
803802
"eventHandlers",
@@ -806,7 +805,6 @@ class DangerouslySetInnerHTML(TypedDict):
806805
]
807806
ALLOWED_VDOM_KEYS = {
808807
"tagName",
809-
"key",
810808
"children",
811809
"attributes",
812810
"eventHandlers",
@@ -819,7 +817,6 @@ class VdomTypeDict(TypedDict):
819817
"""TypedDict representation of what the `VdomDict` should look like."""
820818

821819
tagName: str
822-
key: NotRequired[Key | None]
823820
children: NotRequired[Sequence[Component | VdomChild]]
824821
attributes: NotRequired[VdomAttributes]
825822
eventHandlers: NotRequired[EventHandlerDict]
@@ -844,8 +841,6 @@ def __init__(self, **kwargs: Unpack[VdomTypeDict]) -> None:
844841
@overload
845842
def __getitem__(self, key: Literal["tagName"]) -> str: ...
846843
@overload
847-
def __getitem__(self, key: Literal["key"]) -> Key | None: ...
848-
@overload
849844
def __getitem__(
850845
self, key: Literal["children"]
851846
) -> Sequence[Component | VdomChild]: ...
@@ -863,8 +858,6 @@ def __getitem__(self, key: VdomDictKeys) -> Any:
863858
@overload
864859
def __setitem__(self, key: Literal["tagName"], value: str) -> None: ...
865860
@overload
866-
def __setitem__(self, key: Literal["key"], value: Key | None) -> None: ...
867-
@overload
868861
def __setitem__(
869862
self, key: Literal["children"], value: Sequence[Component | VdomChild]
870863
) -> None: ...
@@ -1116,7 +1109,6 @@ def __call__(
11161109
self,
11171110
attributes: VdomAttributes,
11181111
children: Sequence[VdomChildren],
1119-
key: Key | None,
11201112
event_handlers: EventHandlerDict,
11211113
) -> VdomDict: ...
11221114

tests/test_html.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,13 @@ def test_script_has_no_event_handlers():
106106
def test_simple_fragment():
107107
assert html.fragment() == {"tagName": ""}
108108
assert html.fragment(1, 2, 3) == {"tagName": "", "children": [1, 2, 3]}
109-
assert html.fragment({"key": "something"}) == {"tagName": "", "key": "something"}
109+
assert html.fragment({"key": "something"}) == {
110+
"tagName": "",
111+
"attributes": {"key": "something"},
112+
}
110113
assert html.fragment({"key": "something"}, 1, 2, 3) == {
111114
"tagName": "",
112-
"key": "something",
115+
"attributes": {"key": "something"},
113116
"children": [1, 2, 3],
114117
}
115118

tests/test_utils.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,17 +172,15 @@ def test_string_to_reactpy(case):
172172
"source": '<div id="my-key"></div>',
173173
"model": {
174174
"tagName": "div",
175-
"key": "my-key",
176-
"attributes": {"id": "my-key"},
175+
"attributes": {"id": "my-key", "key": "my-key"},
177176
},
178177
},
179178
# 7: Infer ReactJS `key` from the `name` attribute
180179
{
181180
"source": '<input type="text" name="my-input">',
182181
"model": {
183182
"tagName": "input",
184-
"key": "my-input",
185-
"attributes": {"type": "text", "name": "my-input"},
183+
"attributes": {"type": "text", "name": "my-input", "key": "my-input"},
186184
},
187185
},
188186
# 8: Infer ReactJS `key` from the `key` attribute
@@ -191,7 +189,6 @@ def test_string_to_reactpy(case):
191189
"model": {
192190
"tagName": "div",
193191
"attributes": {"key": "my-key"},
194-
"key": "my-key",
195192
},
196193
},
197194
# 9: Includes `inlineJavaScript` attribue
@@ -268,7 +265,7 @@ def test_non_html_tag_behavior():
268265
"tagName": "my-tag",
269266
"attributes": {"data-x": "something"},
270267
"children": [
271-
{"tagName": "my-other-tag", "attributes": {"key": "a-key"}, "key": "a-key"},
268+
{"tagName": "my-other-tag", "attributes": {"key": "a-key"}},
272269
],
273270
}
274271

0 commit comments

Comments
 (0)