Skip to content

Commit d311e6a

Browse files
committed
Adds object-based subcomponent accessor and tests
1 parent 0b41dc9 commit d311e6a

File tree

3 files changed

+124
-10
lines changed

3 files changed

+124
-10
lines changed

src/reactpy/core/vdom.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,14 @@ def __init__(
134134
if module_name:
135135
self.__module__ = module_name
136136
self.__qualname__ = f"{module_name}.{tag_name}"
137-
137+
138+
def __getattr__(self, attr: str) -> Vdom:
139+
return Vdom(
140+
f"{self.__name__}.{attr}",
141+
allow_children=self.allow_children,
142+
import_source=self.import_source
143+
)
144+
138145
@overload
139146
def __call__(
140147
self, attributes: VdomAttributes, /, *children: VdomChildren
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import React from "https://esm.sh/react@19.0"
22
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};
3+
import {InputGroup, Form} from "https://esm.sh/react-bootstrap@2.10.2?deps=react@19.0,react-dom@19.0,react-is@19.0&exports=InputGroup,Form";
4+
export {InputGroup, Form};
55

66
export function bind(node, config) {
77
const root = ReactDOM.createRoot(node);
88
return {
99
create: (type, props, children) =>
10-
React.createElement(type, props, children),
11-
render: (element) => root.render(element, node),
10+
React.createElement(type, props, ...children),
11+
render: (element) => root.render(element),
1212
unmount: () => root.unmount()
1313
};
1414
}

tests/test_web/test_module.py

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -268,18 +268,125 @@ async def test_keys_properly_propagated(display: DisplayFixture):
268268
assert len(children) == 3
269269

270270

271-
async def test_subcomponent_notation(display: DisplayFixture):
271+
async def test_subcomponent_notation_as_str_attrs(display: DisplayFixture):
272272
module = reactpy.web.module_from_file(
273273
"subcomponent-notation", JS_FIXTURES_DIR / "subcomponent-notation.js",
274274
)
275-
BootstrapFormLabel = reactpy.web.export(module, "Form.Label")
275+
InputGroup, InputGroupText, FormControl, FormLabel = reactpy.web.export(
276+
module,
277+
["InputGroup", "InputGroup.Text", "Form.Control", "Form.Label"]
278+
)
279+
280+
content = reactpy.html.div({"id": "the-parent"},
281+
InputGroup(
282+
InputGroupText({"id": "basic-addon1"}, "@"),
283+
FormControl({
284+
"placeholder": "Username",
285+
"aria-label": "Username",
286+
"aria-describedby": "basic-addon1",
287+
}),
288+
),
289+
290+
InputGroup(
291+
FormControl({
292+
"placeholder": "Recipient's username",
293+
"aria-label": "Recipient's username",
294+
"aria-describedby": "basic-addon2",
295+
}),
296+
InputGroupText({"id": "basic-addon2"}, "@example.com"),
297+
),
298+
299+
FormLabel({"htmlFor": "basic-url"}, "Your vanity URL"),
300+
InputGroup(
301+
InputGroupText({"id": "basic-addon3"},
302+
"https://example.com/users/"
303+
),
304+
FormControl({"id": "basic-url", "aria-describedby": "basic-addon3"}),
305+
),
306+
307+
InputGroup(
308+
InputGroupText("$"),
309+
FormControl({"aria-label": "Amount (to the nearest dollar)"}),
310+
InputGroupText(".00"),
311+
),
312+
313+
InputGroup(
314+
InputGroupText("With textarea"),
315+
FormControl({"as": "textarea", "aria-label": "With textarea"}),
316+
)
317+
)
276318

277319
await display.show(
278-
lambda: BootstrapFormLabel({"htmlFor": "test-123"}, "Test 123")
320+
lambda: content
279321
)
280322

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
323+
parent = await display.page.wait_for_selector("#the-parent", state="visible")
324+
input_group_text = await parent.query_selector_all(".input-group-text")
325+
form_control = await parent.query_selector_all(".form-control")
326+
form_label = await parent.query_selector_all(".form-label")
327+
328+
assert len(input_group_text) == 6
329+
assert len(form_control) == 5
330+
assert len(form_label) == 1
331+
332+
333+
async def test_subcomponent_notation_as_obj_attrs(display: DisplayFixture):
334+
module = reactpy.web.module_from_file(
335+
"subcomponent-notation", JS_FIXTURES_DIR / "subcomponent-notation.js",
336+
)
337+
InputGroup, Form = reactpy.web.export(module, ["InputGroup", "Form"])
338+
339+
content = reactpy.html.div({"id": "the-parent"},
340+
InputGroup(
341+
InputGroup.Text({"id": "basic-addon1"}, "@"),
342+
Form.Control({
343+
"placeholder": "Username",
344+
"aria-label": "Username",
345+
"aria-describedby": "basic-addon1",
346+
}),
347+
),
348+
349+
InputGroup(
350+
Form.Control({
351+
"placeholder": "Recipient's username",
352+
"aria-label": "Recipient's username",
353+
"aria-describedby": "basic-addon2",
354+
}),
355+
InputGroup.Text({"id": "basic-addon2"}, "@example.com"),
356+
),
357+
358+
Form.Label({"htmlFor": "basic-url"}, "Your vanity URL"),
359+
InputGroup(
360+
InputGroup.Text({"id": "basic-addon3"},
361+
"https://example.com/users/"
362+
),
363+
Form.Control({"id": "basic-url", "aria-describedby": "basic-addon3"}),
364+
),
365+
366+
InputGroup(
367+
InputGroup.Text("$"),
368+
Form.Control({"aria-label": "Amount (to the nearest dollar)"}),
369+
InputGroup.Text(".00"),
370+
),
371+
372+
InputGroup(
373+
InputGroup.Text("With textarea"),
374+
Form.Control({"as": "textarea", "aria-label": "With textarea"}),
375+
)
376+
)
377+
378+
await display.show(
379+
lambda: content
380+
)
381+
382+
parent = await display.page.wait_for_selector("#the-parent", state="visible")
383+
input_group_text = await parent.query_selector_all(".input-group-text")
384+
form_control = await parent.query_selector_all(".form-control")
385+
form_label = await parent.query_selector_all(".form-label")
386+
387+
assert len(input_group_text) == 6
388+
assert len(form_control) == 5
389+
assert len(form_label) == 1
283390

284391

285392
def test_module_from_string():

0 commit comments

Comments
 (0)