Skip to content

Commit 520de9b

Browse files
committed
Fix javascript component children (Fix #1262)
1 parent f6d7a07 commit 520de9b

File tree

5 files changed

+439
-7
lines changed

5 files changed

+439
-7
lines changed

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

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
ReactPyModule,
99
BindImportSource,
1010
ReactPyModuleBinding,
11+
ImportSourceBinding,
1112
} from "./types";
1213
import log from "./logger";
1314

@@ -75,13 +76,15 @@ function createImportSourceElement(props: {
7576
if (
7677
!isImportSourceEqual(props.currentImportSource, props.model.importSource)
7778
) {
78-
log.error(
79-
"Parent element import source " +
80-
stringifyImportSource(props.currentImportSource) +
81-
" does not match child's import source " +
82-
stringifyImportSource(props.model.importSource),
83-
);
84-
return null;
79+
return props.binding.create("reactpy-child", {
80+
ref: (node: ReactPyChild | null) => {
81+
if (node) {
82+
node.client = props.client;
83+
node.model = props.model;
84+
node.requestUpdate();
85+
}
86+
},
87+
});
8588
} else {
8689
type = getComponentFromModule(
8790
props.module,
@@ -270,3 +273,85 @@ function generic_reactjs_bind(node: HTMLElement) {
270273
unmount: () => preact.render(null, node),
271274
};
272275
}
276+
277+
class ReactPyChild extends HTMLElement {
278+
mountPoint: HTMLDivElement;
279+
binding: ImportSourceBinding | null = null;
280+
_client: ReactPyClientInterface | null = null;
281+
_model: ReactPyVdom | null = null;
282+
currentImportSource: ReactPyVdomImportSource | null = null;
283+
284+
constructor() {
285+
super();
286+
this.mountPoint = document.createElement("div");
287+
this.mountPoint.style.display = "contents";
288+
}
289+
290+
connectedCallback() {
291+
this.appendChild(this.mountPoint);
292+
}
293+
294+
set client(value: ReactPyClientInterface) {
295+
this._client = value;
296+
}
297+
298+
set model(value: ReactPyVdom) {
299+
this._model = value;
300+
}
301+
302+
requestUpdate() {
303+
this.update();
304+
}
305+
306+
async update() {
307+
if (!this._client || !this._model || !this._model.importSource) {
308+
return;
309+
}
310+
311+
const newImportSource = this._model.importSource;
312+
313+
if (
314+
!this.binding ||
315+
!this.currentImportSource ||
316+
!isImportSourceEqual(this.currentImportSource, newImportSource)
317+
) {
318+
if (this.binding) {
319+
this.binding.unmount();
320+
this.binding = null;
321+
}
322+
323+
this.currentImportSource = newImportSource;
324+
325+
try {
326+
const bind = await loadImportSource(newImportSource, this._client);
327+
if (this.isConnected) {
328+
this.binding = bind(this.mountPoint);
329+
if (this.binding) {
330+
this.binding.render(this._model);
331+
}
332+
}
333+
} catch (error) {
334+
console.error("Failed to load import source", error);
335+
}
336+
} else {
337+
if (this.binding) {
338+
this.binding.render(this._model);
339+
}
340+
}
341+
}
342+
343+
disconnectedCallback() {
344+
if (this.binding) {
345+
this.binding.unmount();
346+
this.binding = null;
347+
this.currentImportSource = null;
348+
}
349+
}
350+
}
351+
352+
if (
353+
typeof customElements !== "undefined" &&
354+
!customElements.get("reactpy-child")
355+
) {
356+
customElements.define("reactpy-child", ReactPyChild);
357+
}

tests/test_reactjs/__init__.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import pytest
2+
3+
import reactpy
4+
from reactpy import html
5+
from reactpy.reactjs import component_from_string, import_reactjs
6+
from reactpy.testing import BackendFixture, DisplayFixture
7+
8+
9+
@pytest.mark.anyio
10+
async def test_nested_client_side_components():
11+
async with BackendFixture(html_head=html.head(import_reactjs())) as backend:
12+
async with DisplayFixture(backend=backend) as display:
13+
# Module A
14+
ComponentA = component_from_string(
15+
"""
16+
import React from "react";
17+
export function ComponentA({ children }) {
18+
return React.createElement("div", { id: "component-a" }, children);
19+
}
20+
""",
21+
"ComponentA",
22+
name="module-a",
23+
)
24+
25+
# Module B
26+
ComponentB = component_from_string(
27+
"""
28+
import React from "react";
29+
export function ComponentB({ children }) {
30+
return React.createElement("div", { id: "component-b" }, children);
31+
}
32+
""",
33+
"ComponentB",
34+
name="module-b",
35+
)
36+
37+
@reactpy.component
38+
def App():
39+
return ComponentA(
40+
ComponentB(
41+
reactpy.html.div({"id": "server-side"}, "Server Side Content")
42+
)
43+
)
44+
45+
await display.show(App)
46+
47+
# Check that all components are rendered
48+
await display.page.wait_for_selector("#component-a")
49+
await display.page.wait_for_selector("#component-b")
50+
await display.page.wait_for_selector("#server-side")
51+
52+
53+
@pytest.mark.anyio
54+
async def test_interleaved_client_server_components():
55+
async with BackendFixture(html_head=html.head(import_reactjs())) as backend:
56+
async with DisplayFixture(backend=backend) as display:
57+
# Module C
58+
ComponentC = component_from_string(
59+
"""
60+
import React from "react";
61+
export function ComponentC({ children }) {
62+
return React.createElement("div", { id: "component-c", className: "component-c" }, children);
63+
}
64+
""",
65+
"ComponentC",
66+
name="module-c",
67+
)
68+
69+
@reactpy.component
70+
def App():
71+
return reactpy.html.div(
72+
{"id": "root-server"},
73+
ComponentC(
74+
reactpy.html.div(
75+
{"id": "nested-server"},
76+
ComponentC(
77+
reactpy.html.span({"id": "deep-server"}, "Deep Content")
78+
),
79+
)
80+
),
81+
)
82+
83+
await display.show(App)
84+
85+
await display.page.wait_for_selector("#root-server")
86+
await display.page.wait_for_selector(".component-c")
87+
await display.page.wait_for_selector("#nested-server")
88+
# We need to check that there are two component-c elements
89+
elements = await display.page.query_selector_all(".component-c")
90+
assert len(elements) == 2
91+
await display.page.wait_for_selector("#deep-server")

tests/test_reactjs/test_modules.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import pytest
2+
3+
import reactpy
4+
from reactpy import html
5+
from reactpy.reactjs import component_from_string, import_reactjs
6+
from reactpy.testing import BackendFixture, DisplayFixture
7+
8+
9+
@pytest.mark.anyio
10+
async def test_nested_client_side_components():
11+
async with BackendFixture(html_head=html.head(import_reactjs())) as backend:
12+
async with DisplayFixture(backend=backend) as display:
13+
# Module A
14+
ComponentA = component_from_string(
15+
"""
16+
import React from "react";
17+
export function ComponentA({ children }) {
18+
return React.createElement("div", { id: "component-a" }, children);
19+
}
20+
""",
21+
"ComponentA",
22+
name="module-a",
23+
)
24+
25+
# Module B
26+
ComponentB = component_from_string(
27+
"""
28+
import React from "react";
29+
export function ComponentB({ children }) {
30+
return React.createElement("div", { id: "component-b" }, children);
31+
}
32+
""",
33+
"ComponentB",
34+
name="module-b",
35+
)
36+
37+
@reactpy.component
38+
def App():
39+
return ComponentA(
40+
ComponentB(
41+
reactpy.html.div({"id": "server-side"}, "Server Side Content")
42+
)
43+
)
44+
45+
await display.show(App)
46+
47+
# Check that all components are rendered
48+
await display.page.wait_for_selector("#component-a")
49+
await display.page.wait_for_selector("#component-b")
50+
await display.page.wait_for_selector("#server-side")
51+
52+
53+
@pytest.mark.anyio
54+
async def test_interleaved_client_server_components():
55+
async with BackendFixture(html_head=html.head(import_reactjs())) as backend:
56+
async with DisplayFixture(backend=backend) as display:
57+
# Module C
58+
ComponentC = component_from_string(
59+
"""
60+
import React from "react";
61+
export function ComponentC({ children }) {
62+
return React.createElement("div", { id: "component-c", className: "component-c" }, children);
63+
}
64+
""",
65+
"ComponentC",
66+
name="module-c",
67+
)
68+
69+
@reactpy.component
70+
def App():
71+
return reactpy.html.div(
72+
{"id": "root-server"},
73+
ComponentC(
74+
reactpy.html.div(
75+
{"id": "nested-server"},
76+
ComponentC(
77+
reactpy.html.span({"id": "deep-server"}, "Deep Content")
78+
),
79+
)
80+
),
81+
)
82+
83+
await display.show(App)
84+
85+
await display.page.wait_for_selector("#root-server")
86+
await display.page.wait_for_selector(".component-c")
87+
await display.page.wait_for_selector("#nested-server")
88+
# We need to check that there are two component-c elements
89+
elements = await display.page.query_selector_all(".component-c")
90+
assert len(elements) == 2
91+
await display.page.wait_for_selector("#deep-server")

0 commit comments

Comments
 (0)