Skip to content

Commit

Permalink
Add support for ComponentType children in vdom_to_html (#1257)
Browse files Browse the repository at this point in the history
  • Loading branch information
Archmonger authored Jan 26, 2025
1 parent a54ce4e commit 178fc05
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 30 deletions.
1 change: 1 addition & 0 deletions docs/source/about/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``<data-table>`` 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**

Expand Down
58 changes: 33 additions & 25 deletions src/reactpy/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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:

# <p><a>hello</a>world</p>

# In this code sample, "hello" is the `text` attribute of the `<a>` element
# and "world" is the `tail` attribute of that same `<a>` 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:
<p><a>hello</a>world</p>
In this code sample, "hello" is the `text` attribute of the `<a>` element
and "world" is the `tail` attribute of that same `<a>` 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:
Expand Down Expand Up @@ -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`.
Expand Down
23 changes: 18 additions & 5 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
[
Expand Down Expand Up @@ -254,10 +269,8 @@ def test_del_html_body_transform():
'<div data-something="1" data-something-else="2" dataisnotdashed="3"></div>',
),
(
html.div(
{"dataSomething": 1, "dataSomethingElse": 2, "dataisnotdashed": 3}
),
'<div data-something="1" data-something-else="2" dataisnotdashed="3"></div>',
html.div(example_parent()),
'<div><div id="sample" style="padding:15px"><h1>Sample Application</h1></div></div>',
),
],
)
Expand Down

0 comments on commit 178fc05

Please sign in to comment.