From 35c7e8dae5af44a22458733134fd5d6663004a86 Mon Sep 17 00:00:00 2001 From: ShawnCrawley-NOAA <shawn.crawley@noaa.gov> Date: Mon, 10 Feb 2025 08:25:01 -0700 Subject: [PATCH 01/14] Ensures key is properly passed to importedElements The "key" prop was being lost in the trickle-down of importedElements. --- src/js/packages/@reactpy/client/src/reactpy-vdom.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/js/packages/@reactpy/client/src/reactpy-vdom.tsx b/src/js/packages/@reactpy/client/src/reactpy-vdom.tsx index 22fa3e61d..7d7784585 100644 --- a/src/js/packages/@reactpy/client/src/reactpy-vdom.tsx +++ b/src/js/packages/@reactpy/client/src/reactpy-vdom.tsx @@ -133,6 +133,14 @@ export function createAttributes( model: ReactPyVdom, client: ReactPyClient, ): { [key: string]: any } { + // Add key to attributes + if (model.key) { + if (model.attributes) { + model.attributes['key'] = model.key + } else { + model.attributes = {'key': model.key} + } + } return Object.fromEntries( Object.entries({ // Normal HTML attributes From fc6c001e88aead1a5611d22b8d0684bb3b38cd9b Mon Sep 17 00:00:00 2001 From: ShawnCrawley-NOAA <shawn.crawley@noaa.gov> Date: Mon, 10 Feb 2025 08:45:10 -0700 Subject: [PATCH 02/14] Add semicolons --- src/js/packages/@reactpy/client/src/reactpy-vdom.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/packages/@reactpy/client/src/reactpy-vdom.tsx b/src/js/packages/@reactpy/client/src/reactpy-vdom.tsx index 7d7784585..fa27adf43 100644 --- a/src/js/packages/@reactpy/client/src/reactpy-vdom.tsx +++ b/src/js/packages/@reactpy/client/src/reactpy-vdom.tsx @@ -136,9 +136,9 @@ export function createAttributes( // Add key to attributes if (model.key) { if (model.attributes) { - model.attributes['key'] = model.key + model.attributes['key'] = model.key; } else { - model.attributes = {'key': model.key} + model.attributes = {'key': model.key}; } } return Object.fromEntries( From 22a1de898633a124cba2e35fc3d7b4f014be208e Mon Sep 17 00:00:00 2001 From: ShawnCrawley-NOAA <shawn.crawley@noaa.gov> Date: Mon, 10 Feb 2025 09:52:25 -0700 Subject: [PATCH 03/14] Update reactpy-vdom.tsx --- src/js/packages/@reactpy/client/src/reactpy-vdom.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/packages/@reactpy/client/src/reactpy-vdom.tsx b/src/js/packages/@reactpy/client/src/reactpy-vdom.tsx index fa27adf43..9aff47b13 100644 --- a/src/js/packages/@reactpy/client/src/reactpy-vdom.tsx +++ b/src/js/packages/@reactpy/client/src/reactpy-vdom.tsx @@ -136,9 +136,9 @@ export function createAttributes( // Add key to attributes if (model.key) { if (model.attributes) { - model.attributes['key'] = model.key; + model.attributes.key = model.key; } else { - model.attributes = {'key': model.key}; + model.attributes = {key: model.key}; } } return Object.fromEntries( From a41adbee11cf385ffa4d4cf69470279b5fdce004 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:21:48 -0800 Subject: [PATCH 04/14] Format via `hatch run javascript:fix` --- src/js/packages/@reactpy/client/src/vdom.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/packages/@reactpy/client/src/vdom.tsx b/src/js/packages/@reactpy/client/src/vdom.tsx index 5c7f8e306..a8a324b4a 100644 --- a/src/js/packages/@reactpy/client/src/vdom.tsx +++ b/src/js/packages/@reactpy/client/src/vdom.tsx @@ -147,7 +147,7 @@ export function createAttributes( if (model.attributes) { model.attributes.key = model.key; } else { - model.attributes = {key: model.key}; + model.attributes = { key: model.key }; } } return Object.fromEntries( From b1e01cc24dffec8d6b1bac11028d0c22f6344741 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:31:15 -0800 Subject: [PATCH 05/14] Add changelog item --- docs/source/about/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst index c90a8dcff..c361113da 100644 --- a/docs/source/about/changelog.rst +++ b/docs/source/about/changelog.rst @@ -16,6 +16,7 @@ Unreleased ---------- **Added** + - :pull:`1113` - Added ``reactpy.executors.asgi.ReactPy`` that can be used to run ReactPy in standalone mode via ASGI. - :pull:`1269` - Added ``reactpy.executors.asgi.ReactPyPyodide`` that can be used to run ReactPy in standalone mode via ASGI, but rendered entirely client-sided. - :pull:`1113` - Added ``reactpy.executors.asgi.ReactPyMiddleware`` that can be used to utilize ReactPy within any ASGI compatible framework. @@ -56,6 +57,7 @@ Unreleased **Fixed** - :pull:`1239` - Fixed a bug where script elements would not render to the DOM as plain text. +- :pull:`1271` - Fixed bug where JavaScript components were unable to obtain the ``key`` attribute provided within ReactPy. v1.1.0 ------ From e1ff1ce85eb3cf27a37bb878fefb5ddbc1aa72bb Mon Sep 17 00:00:00 2001 From: Shawn Crawley <shawncrawley@gmail.com> Date: Mon, 10 Mar 2025 10:13:08 -0600 Subject: [PATCH 06/14] Test written --- .../js_fixtures/keys-properly-propagated.js | 15 ++++++ tests/test_web/test_module.py | 54 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 tests/test_web/js_fixtures/keys-properly-propagated.js diff --git a/tests/test_web/js_fixtures/keys-properly-propagated.js b/tests/test_web/js_fixtures/keys-properly-propagated.js new file mode 100644 index 000000000..d425a74fa --- /dev/null +++ b/tests/test_web/js_fixtures/keys-properly-propagated.js @@ -0,0 +1,15 @@ +import React from "https://esm.sh/react@19.0" +import ReactDOM from "https://esm.sh/react-dom@19.0/client" +import GridLayout from "https://esm.sh/react-grid-layout@1.5.0"; +export {GridLayout}; + +export function bind(node, config) { + debugger; + const root = ReactDOM.createRoot(node); + return { + create: (type, props, children) => + React.createElement(type, props, children), + render: (element) => root.render(element, node), + unmount: () => root.unmount() + }; +} diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index 8cd487c0c..22246cedc 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -208,6 +208,60 @@ async def test_imported_components_can_render_children(display: DisplayFixture): assert (await child.get_attribute("id")) == f"child-{index + 1}" +async def test_keys_properly_propagated(display: DisplayFixture): + module = reactpy.web.module_from_file( + "keys-properly-propagated", JS_FIXTURES_DIR / "keys-properly-propagated.js" + ) + GridLayout = reactpy.web.export(module, "GridLayout") + + await display.show( + lambda: GridLayout({ + "layout": [ + { + "i": "a", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "static": True, + }, + { + "i": "b", + "x": 1, + "y": 0, + "w": 3, + "h": 2, + "minW": 2, + "maxW": 4, + }, + { + "i": "c", + "x": 4, + "y": 0, + "w": 1, + "h": 2, + } + ], + "cols": 12, + "rowHeight": 30, + "width": 1200, + }, + reactpy.html.div({"key": "a"}, "a"), + reactpy.html.div({"key": "b"}, "b"), + reactpy.html.div({"key": "c"}, "c"), + ) + ) + + parent = await display.page.wait_for_selector(".react-grid-layout", state="attached", timeout=0) + children = await parent.query_selector_all("div") + + # The children simply will not render unless they receive the key prop + assert len(children) == 3 + + # for index, child in enumerate(children): + # assert (await child.get_attribute("key")) == child.get_text() + + def test_module_from_string(): reactpy.web.module_from_string("temp", "old") with assert_reactpy_did_log(r"Existing web module .* will be replaced with"): From 1cc35027f95aba30122090309cf72b84ab31ff17 Mon Sep 17 00:00:00 2001 From: ShawnCrawley-NOAA <shawn.crawley@noaa.gov> Date: Mon, 10 Mar 2025 10:24:58 -0600 Subject: [PATCH 07/14] Delete unnecessary, commented out lines --- tests/test_web/test_module.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index 22246cedc..d3474bad4 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -258,9 +258,6 @@ async def test_keys_properly_propagated(display: DisplayFixture): # The children simply will not render unless they receive the key prop assert len(children) == 3 - # for index, child in enumerate(children): - # assert (await child.get_attribute("key")) == child.get_text() - def test_module_from_string(): reactpy.web.module_from_string("temp", "old") From 7a7dca266f419b40e37af7b6151af88ae7bba42e Mon Sep 17 00:00:00 2001 From: ShawnCrawley-NOAA <shawn.crawley@noaa.gov> Date: Mon, 10 Mar 2025 10:53:29 -0600 Subject: [PATCH 08/14] Update keys-properly-propagated.js Removes debugger statement. --- tests/test_web/js_fixtures/keys-properly-propagated.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_web/js_fixtures/keys-properly-propagated.js b/tests/test_web/js_fixtures/keys-properly-propagated.js index d425a74fa..8d700397e 100644 --- a/tests/test_web/js_fixtures/keys-properly-propagated.js +++ b/tests/test_web/js_fixtures/keys-properly-propagated.js @@ -4,7 +4,6 @@ import GridLayout from "https://esm.sh/react-grid-layout@1.5.0"; export {GridLayout}; export function bind(node, config) { - debugger; const root = ReactDOM.createRoot(node); return { create: (type, props, children) => From 60345a5565a4a79298f0be1b2114f965217c38c8 Mon Sep 17 00:00:00 2001 From: ShawnCrawley-NOAA <shawn.crawley@noaa.gov> Date: Mon, 10 Mar 2025 10:55:24 -0600 Subject: [PATCH 09/14] Update test_module.py Removes `timeout=0` that was there for testing --- tests/test_web/test_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index d3474bad4..76ff620ce 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -252,7 +252,7 @@ async def test_keys_properly_propagated(display: DisplayFixture): ) ) - parent = await display.page.wait_for_selector(".react-grid-layout", state="attached", timeout=0) + parent = await display.page.wait_for_selector(".react-grid-layout", state="attached") children = await parent.query_selector_all("div") # The children simply will not render unless they receive the key prop From b9f50da501adcc49a0c8dc11653ab85c1136c4ec Mon Sep 17 00:00:00 2001 From: Shawn Crawley <shawncrawley@gmail.com> Date: Mon, 10 Mar 2025 21:10:22 -0600 Subject: [PATCH 10/14] Adds docstring and tweaks changelog Per feedback by @Archmonger --- docs/source/about/changelog.rst | 2 +- tests/test_web/test_module.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst index d4aeda42b..0db168ba2 100644 --- a/docs/source/about/changelog.rst +++ b/docs/source/about/changelog.rst @@ -70,7 +70,7 @@ Unreleased **Fixed** - :pull:`1239` - Fixed a bug where script elements would not render to the DOM as plain text. -- :pull:`1271` - Fixed bug where JavaScript components were unable to obtain the ``key`` attribute provided within ReactPy. +- :pull:`1271` - Fixed a bug where the ``key`` property provided via server-side ReactPy code was failing to propagate to the front-end JavaScript component. - :pull:`1254` - Fixed a bug where ``RuntimeError("Hook stack is in an invalid state")`` errors would be provided when using a webserver that reuses threads. v1.1.0 diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index 76ff620ce..fc65250ba 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -209,6 +209,13 @@ async def test_imported_components_can_render_children(display: DisplayFixture): async def test_keys_properly_propagated(display: DisplayFixture): + """ Addresses issue #1271 + The "key" property was being lost in its propagation from + the server-side ReactPy definition to the front-end JavaScript. + This is a critical issue for the proper functionality of + certain components, such as the GridLayout from + react-grid-layout. + """ module = reactpy.web.module_from_file( "keys-properly-propagated", JS_FIXTURES_DIR / "keys-properly-propagated.js" ) From 4ab15d4769067845fdcfe1b5f22a5194a69c6f52 Mon Sep 17 00:00:00 2001 From: Mark Bakhit <archiethemonger@gmail.com> Date: Mon, 10 Mar 2025 20:43:26 -0700 Subject: [PATCH 11/14] Update docstring in test_module.py --- tests/test_web/test_module.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index fc65250ba..4b5f980c4 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -209,11 +209,13 @@ async def test_imported_components_can_render_children(display: DisplayFixture): async def test_keys_properly_propagated(display: DisplayFixture): - """ Addresses issue #1271 - The "key" property was being lost in its propagation from - the server-side ReactPy definition to the front-end JavaScript. - This is a critical issue for the proper functionality of - certain components, such as the GridLayout from + """ + Fix https://github.com/reactive-python/reactpy/issues/1275 + + The `key` property was being lost in its propagation from the server-side ReactPy + definition to the front-end JavaScript. + + This property is required for certain JS components, such as the GridLayout from react-grid-layout. """ module = reactpy.web.module_from_file( From 35f9e7d3587949ee7cfb0ff906b968c5484c4a49 Mon Sep 17 00:00:00 2001 From: Shawn Crawley <shawncrawley@gmail.com> Date: Tue, 11 Mar 2025 06:42:17 -0600 Subject: [PATCH 12/14] Handles fix on server-side rather than client-side --- src/js/packages/@reactpy/client/src/vdom.tsx | 8 -------- src/reactpy/core/vdom.py | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/js/packages/@reactpy/client/src/vdom.tsx b/src/js/packages/@reactpy/client/src/vdom.tsx index a8a324b4a..25eb9f3e7 100644 --- a/src/js/packages/@reactpy/client/src/vdom.tsx +++ b/src/js/packages/@reactpy/client/src/vdom.tsx @@ -142,14 +142,6 @@ export function createAttributes( model: ReactPyVdom, client: ReactPyClientInterface, ): { [key: string]: any } { - // Add key to attributes - if (model.key) { - if (model.attributes) { - model.attributes.key = model.key; - } else { - model.attributes = { key: model.key }; - } - } return Object.fromEntries( Object.entries({ // Normal HTML attributes diff --git a/src/reactpy/core/vdom.py b/src/reactpy/core/vdom.py index 4186ab5a6..6bc28dfd4 100644 --- a/src/reactpy/core/vdom.py +++ b/src/reactpy/core/vdom.py @@ -148,7 +148,7 @@ def __call__( ) -> VdomDict: """The entry point for the VDOM API, for example reactpy.html(<WE_ARE_HERE>).""" attributes, children = separate_attributes_and_children(attributes_and_children) - key = attributes.pop("key", None) + key = attributes.get("key", None) attributes, event_handlers = separate_attributes_and_event_handlers(attributes) if REACTPY_CHECK_JSON_ATTRS.current: json.dumps(attributes) From 7591d1fe7f39499442375f75cebd5b71342e1285 Mon Sep 17 00:00:00 2001 From: Shawn Crawley <shawncrawley@gmail.com> Date: Tue, 11 Mar 2025 08:40:13 -0600 Subject: [PATCH 13/14] Update tests per maintaing "key" in vdom attrs --- src/reactpy/_html.py | 2 +- src/reactpy/transforms.py | 2 +- tests/test_utils.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/reactpy/_html.py b/src/reactpy/_html.py index 9f160b403..6e9e331f9 100644 --- a/src/reactpy/_html.py +++ b/src/reactpy/_html.py @@ -104,7 +104,7 @@ def _fragment( event_handlers: EventHandlerDict, ) -> VdomDict: """An HTML fragment - this element will not appear in the DOM""" - if attributes or event_handlers: + if (attributes and any(a != "key" for a in attributes)) or event_handlers: msg = "Fragments cannot have attributes besides 'key'" raise TypeError(msg) model = VdomDict(tagName="") diff --git a/src/reactpy/transforms.py b/src/reactpy/transforms.py index 027fb3e3b..cdac48c7e 100644 --- a/src/reactpy/transforms.py +++ b/src/reactpy/transforms.py @@ -103,7 +103,7 @@ def infer_key_from_attributes(vdom: VdomDict) -> None: return # Infer 'key' from 'attributes.key' - key = attributes.pop("key", None) + key = attributes.get("key", None) # Infer 'key' from 'attributes.id' if key is None: diff --git a/tests/test_utils.py b/tests/test_utils.py index 7e334dda5..e494c29b3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -188,7 +188,7 @@ def test_string_to_reactpy(case): # 8: Infer ReactJS `key` from the `key` attribute { "source": '<div key="my-key"></div>', - "model": {"tagName": "div", "key": "my-key"}, + "model": {"tagName": "div", "attributes": {"key": "my-key"}, "key": "my-key"}, }, ], ) @@ -253,7 +253,7 @@ def test_non_html_tag_behavior(): "tagName": "my-tag", "attributes": {"data-x": "something"}, "children": [ - {"tagName": "my-other-tag", "key": "a-key"}, + {"tagName": "my-other-tag", "attributes": {"key": "a-key"}, "key": "a-key"}, ], } From d457f61e6b8f39630eb45ff6389ed2e773009620 Mon Sep 17 00:00:00 2001 From: ShawnCrawley-NOAA <shawn.crawley@noaa.gov> Date: Wed, 12 Mar 2025 07:11:20 -0600 Subject: [PATCH 14/14] Alternate solution for expensive any on attributes Since "key" is the only allowed attribute, I just pop that out before checking if anything is left in attributes, and if so, the error is raised. --- src/reactpy/_html.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/reactpy/_html.py b/src/reactpy/_html.py index 6e9e331f9..ffeee7072 100644 --- a/src/reactpy/_html.py +++ b/src/reactpy/_html.py @@ -104,7 +104,8 @@ def _fragment( event_handlers: EventHandlerDict, ) -> VdomDict: """An HTML fragment - this element will not appear in the DOM""" - if (attributes and any(a != "key" for a in attributes)) or event_handlers: + attributes.pop("key", None) + if attributes or event_handlers: msg = "Fragments cannot have attributes besides 'key'" raise TypeError(msg) model = VdomDict(tagName="")