Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ComponentType children in vdom_to_html #1257

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading