diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst index a9ddbe854..178fbba19 100644 --- a/docs/source/about/changelog.rst +++ b/docs/source/about/changelog.rst @@ -21,6 +21,7 @@ Unreleased - :pull:`1239` - Script elements no longer support behaving like effects. They now strictly behave like plain HTML script elements. - :pull:`1255` - The ``reactpy.html`` module has been modified to allow for auto-creation of any HTML nodes. For example, you can create a ```` element by calling ``html.data_table()``. - :pull:`1256` - Change ``set_state`` comparison method to check equality with ``==`` more consistently. +- :pull:`1257` - Add support for rendering ``@component`` children within ``vdom_to_html``. **Removed** diff --git a/src/reactpy/utils.py b/src/reactpy/utils.py index a20194902..77df473fb 100644 --- a/src/reactpy/utils.py +++ b/src/reactpy/utils.py @@ -3,13 +3,13 @@ import re from collections.abc import Iterable from itertools import chain -from typing import Any, Callable, Generic, TypeVar, cast +from typing import Any, Callable, Generic, TypeVar, Union, cast from lxml import etree from lxml.html import fromstring, tostring -from reactpy.core.types import VdomDict -from reactpy.core.vdom import vdom +from reactpy.core.types import ComponentType, VdomDict +from reactpy.core.vdom import vdom as make_vdom _RefValue = TypeVar("_RefValue") _ModelTransform = Callable[[VdomDict], Any] @@ -144,7 +144,7 @@ def _etree_to_vdom( children = _generate_vdom_children(node, transforms) # Convert the lxml node to a VDOM dict - el = vdom(node.tag, dict(node.items()), *children) + el = make_vdom(node.tag, dict(node.items()), *children) # Perform any necessary mutations on the VDOM attributes to meet VDOM spec _mutate_vdom(el) @@ -160,7 +160,7 @@ def _add_vdom_to_etree(parent: etree._Element, vdom: VdomDict | dict[str, Any]) try: tag = vdom["tagName"] except KeyError as e: - msg = f"Expected a VDOM dict, not {vdom}" + msg = f"Expected a VDOM dict, not {type(vdom)}" raise TypeError(msg) from e else: vdom = cast(VdomDict, vdom) @@ -174,29 +174,29 @@ def _add_vdom_to_etree(parent: etree._Element, vdom: VdomDict | dict[str, Any]) element = parent for c in vdom.get("children", []): + if hasattr(c, "render"): + c = _component_to_vdom(cast(ComponentType, c)) if isinstance(c, dict): _add_vdom_to_etree(element, c) + + # LXML handles string children by storing them under `text` and `tail` + # attributes of Element objects. The `text` attribute, if present, effectively + # becomes that element's first child. Then the `tail` attribute, if present, + # becomes a sibling that follows that element. For example, consider the + # following HTML: + + #

helloworld

+ + # In this code sample, "hello" is the `text` attribute of the `` element + # and "world" is the `tail` attribute of that same `` element. It's for + # this reason that, depending on whether the element being constructed has + # non-string a child element, we need to assign a `text` vs `tail` attribute + # to that element or the last non-string child respectively. + elif len(element): + last_child = element[-1] + last_child.tail = f"{last_child.tail or ''}{c}" else: - """ - LXML handles string children by storing them under `text` and `tail` - attributes of Element objects. The `text` attribute, if present, effectively - becomes that element's first child. Then the `tail` attribute, if present, - becomes a sibling that follows that element. For example, consider the - following HTML: - -

helloworld

- - In this code sample, "hello" is the `text` attribute of the `` element - and "world" is the `tail` attribute of that same `` element. It's for - this reason that, depending on whether the element being constructed has - non-string a child element, we need to assign a `text` vs `tail` attribute - to that element or the last non-string child respectively. - """ - if len(element): - last_child = element[-1] - last_child.tail = f"{last_child.tail or ''}{c}" - else: - element.text = f"{element.text or ''}{c}" + element.text = f"{element.text or ''}{c}" def _mutate_vdom(vdom: VdomDict) -> None: @@ -249,6 +249,14 @@ def _generate_vdom_children( ) +def _component_to_vdom(component: ComponentType) -> VdomDict | str | None: + """Convert a component to a VDOM dictionary""" + result = component.render() + if hasattr(result, "render"): + result = _component_to_vdom(cast(ComponentType, result)) + return cast(Union[VdomDict, str, None], result) + + def del_html_head_body_transform(vdom: VdomDict) -> VdomDict: """Transform intended for use with `html_to_vdom`. diff --git a/tests/test_utils.py b/tests/test_utils.py index b071fdc9f..ef67766e5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,7 +3,7 @@ import pytest import reactpy -from reactpy import html +from reactpy import component, html from reactpy.utils import ( HTMLParseError, del_html_head_body_transform, @@ -193,6 +193,21 @@ def test_del_html_body_transform(): SOME_OBJECT = object() +@component +def example_parent(): + return example_middle() + + +@component +def example_middle(): + return html.div({"id": "sample", "style": {"padding": "15px"}}, example_child()) + + +@component +def example_child(): + return html.h1("Sample Application") + + @pytest.mark.parametrize( "vdom_in, html_out", [ @@ -254,10 +269,8 @@ def test_del_html_body_transform(): '
', ), ( - html.div( - {"dataSomething": 1, "dataSomethingElse": 2, "dataisnotdashed": 3} - ), - '
', + html.div(example_parent()), + '

Sample Application

', ), ], )