Skip to content

Commit 0b41dc9

Browse files
committed
Adds support for web module subcomponent notation
1 parent 9c32dc1 commit 0b41dc9

File tree

4 files changed

+59
-5
lines changed

4 files changed

+59
-5
lines changed

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ function createImportSourceElement(props: {
6868
}): any {
6969
let type: any;
7070
if (props.model.importSource) {
71+
let rootType = props.model.tagName.split(".")[0];
7172
if (
7273
!isImportSourceEqual(props.currentImportSource, props.model.importSource)
7374
) {
@@ -78,15 +79,16 @@ function createImportSourceElement(props: {
7879
stringifyImportSource(props.model.importSource),
7980
);
8081
return null;
81-
} else if (!props.module[props.model.tagName]) {
82+
} else if (!props.module[rootType]) {
8283
log.error(
8384
"Module from source " +
8485
stringifyImportSource(props.currentImportSource) +
85-
` does not export ${props.model.tagName}`,
86+
` does not export ${rootType}`,
8687
);
8788
return null;
8889
} else {
89-
type = props.module[props.model.tagName];
90+
type = tryGetSubType(props.module, props.model.tagName);
91+
if (!type) return null;
9092
}
9193
} else {
9294
type = props.model.tagName;
@@ -103,6 +105,30 @@ function createImportSourceElement(props: {
103105
);
104106
}
105107

108+
function tryGetSubType(
109+
module: ReactPyModule,
110+
component: string
111+
) {
112+
let subComponents: string[] = component.split(".");
113+
let rootComponent: string = subComponents[0];
114+
let subComponentAccessor: string = rootComponent;
115+
let type: any = module[rootComponent];
116+
117+
subComponents = subComponents.slice(1);
118+
for (let i = 0; i < subComponents.length; i++) {
119+
let subComponent = subComponents[i];
120+
subComponentAccessor += "." + subComponent;
121+
type = type[subComponent];
122+
if (!type) {
123+
console.error(
124+
`Component ${rootComponent} does not have subcomponent ${subComponentAccessor}`
125+
);
126+
break
127+
}
128+
}
129+
return type;
130+
}
131+
106132
function isImportSourceEqual(
107133
source1: ReactPyVdomImportSource,
108134
source2: ReactPyVdomImportSource,

src/reactpy/web/module.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,14 +260,14 @@ def export(
260260
if isinstance(export_names, str):
261261
if (
262262
web_module.export_names is not None
263-
and export_names not in web_module.export_names
263+
and export_names.split(".")[0] not in web_module.export_names
264264
):
265265
msg = f"{web_module.source!r} does not export {export_names!r}"
266266
raise ValueError(msg)
267267
return _make_export(web_module, export_names, fallback, allow_children)
268268
else:
269269
if web_module.export_names is not None:
270-
missing = sorted(set(export_names).difference(web_module.export_names))
270+
missing = sorted(set([e.split(".")[0] for e in export_names]).difference(web_module.export_names))
271271
if missing:
272272
msg = f"{web_module.source!r} does not export {missing!r}"
273273
raise ValueError(msg)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from "https://esm.sh/react@19.0"
2+
import ReactDOM from "https://esm.sh/react-dom@19.0/client"
3+
import Form from "https://esm.sh/react-bootstrap@2.10.9/Form";
4+
export {Form};
5+
6+
export function bind(node, config) {
7+
const root = ReactDOM.createRoot(node);
8+
return {
9+
create: (type, props, children) =>
10+
React.createElement(type, props, children),
11+
render: (element) => root.render(element, node),
12+
unmount: () => root.unmount()
13+
};
14+
}

tests/test_web/test_module.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,20 @@ async def test_keys_properly_propagated(display: DisplayFixture):
268268
assert len(children) == 3
269269

270270

271+
async def test_subcomponent_notation(display: DisplayFixture):
272+
module = reactpy.web.module_from_file(
273+
"subcomponent-notation", JS_FIXTURES_DIR / "subcomponent-notation.js",
274+
)
275+
BootstrapFormLabel = reactpy.web.export(module, "Form.Label")
276+
277+
await display.show(
278+
lambda: BootstrapFormLabel({"htmlFor": "test-123"}, "Test 123")
279+
)
280+
281+
await display.page.wait_for_selector(".form-label", state="attached")
282+
# The above will fail due to timeout if it does not work as expected
283+
284+
271285
def test_module_from_string():
272286
reactpy.web.module_from_string("temp", "old")
273287
with assert_reactpy_did_log(r"Existing web module .* will be replaced with"):

0 commit comments

Comments
 (0)