diff --git a/docs/usage.md b/docs/usage.md index 3e4b247..90b517a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -290,6 +290,30 @@ snippets as attributes: ``` +## Render elements without a parent (orphans) + +In some cases such as returning partial content it is useful to render elements +without a parent element. This is useful in HTMX partial responses. + +You may use `render_node` to achieve this: + +```pycon title="Render elements without a parent" +>>> from htpy import render_orphans, tr +>>> print(render_node([tr["a"], tr["b"]])) +ab +``` + +`render_node()` accepts all kinds of [`Node`](static-typing.md#node) objects. +You may use it to render anything that would normally be a children of another +element. + +!!! note "Best practice: Only use render_node() to render non-Elements" + + You can render regular elements by using `str()`, e.g. `str(p["hi"])`. While + `render_node()` would give the same result, it is more straightforward and + better practice to just use `str()` when rendering a regular element. Only + use `render_node()` when you do not have a parent element. + ## Iterating of the Output Iterating over a htpy element will yield the resulting contents in chunks as @@ -297,13 +321,32 @@ they are rendered: ```pycon >>> from htpy import ul, li ->>> for chunk in ul[li["a", "b"]]: +>>> for chunk in ul[li["a"], li["b"]]: ... print(f"got a chunk: {chunk!r}") ... got a chunk: '' ``` + +Just like [render_node()](#render-elements-without-a-parent-orphans), there is +`iter_node()` that can be used when you need to iterate over a list of elements +without a parent: + +``` +>>> from htpy import li, iter_node +>>> for chunk in iter_node([li["a"], li["b"]]): +... print(f"got a chunk: {chunk!r}") +... +got a chunk: '
  • ' +got a chunk: 'a' +got a chunk: '
  • ' +got a chunk: '
  • ' +got a chunk: 'b' +got a chunk: '
  • ' +``` diff --git a/htpy/__init__.py b/htpy/__init__.py index 20b1d05..a5af5ac 100644 --- a/htpy/__init__.py +++ b/htpy/__init__.py @@ -103,7 +103,7 @@ def _attrs_string(attrs: dict[str, Attribute]) -> str: return " " + result -def _iter_children(x: Node) -> Iterator[str]: +def iter_node(x: Node) -> Iterator[str]: while not isinstance(x, BaseElement) and callable(x): x = x() @@ -116,7 +116,7 @@ def _iter_children(x: Node) -> Iterator[str]: yield str(_escape(x)) elif isinstance(x, Iterable): for child in x: - yield from _iter_children(child) + yield from iter_node(child) else: raise ValueError(f"{x!r} is not a valid child element") @@ -188,7 +188,7 @@ def __call__(self: BaseElementSelf, *args: Any, **kwargs: Any) -> BaseElementSel def __iter__(self) -> Iterator[str]: yield f"<{self._name}{_attrs_string(self._attrs)}>" - yield from _iter_children(self._children) + yield from iter_node(self._children) yield f"" def __repr__(self) -> str: @@ -217,6 +217,10 @@ def __iter__(self) -> Iterator[str]: yield f"<{self._name}{_attrs_string(self._attrs)}>" +def render_node(node: Node) -> _Markup: + return _Markup("".join(iter_node(node))) + + class _HasHtml(Protocol): def __html__(self) -> str: ... diff --git a/tests/test_nodes.py b/tests/test_nodes.py new file mode 100644 index 0000000..897bfa0 --- /dev/null +++ b/tests/test_nodes.py @@ -0,0 +1,50 @@ +from typing import Any + +from markupsafe import Markup + +from htpy import div, iter_node, render_node, tr + + +def assert_markup(result: Any, expected: str) -> None: + assert isinstance(result, Markup) + assert result == expected + + +class Test_render_node: + def test_element(self) -> None: + result = render_node(div["a"]) + assert_markup(result, "
    a
    ") + + def test_list(self) -> None: + result = render_node([tr["a"], tr["b"]]) + + assert_markup(result, "ab") + + def test_none(self) -> None: + result = render_node(None) + assert_markup(result, "") + + def test_string(self) -> None: + result = render_node("hej!") + assert_markup(result, "hej!") + + +class Test_iter_node: + def test_element(self) -> None: + result = list(iter_node(div["a"])) + + # Ensure we get str back, not markup. + assert type(result[0]) is str + assert result == ["
    ", "a", "
    "] + + def test_list(self) -> None: + result = list(iter_node([tr["a"], tr["b"]])) + assert result == ["", "a", "", "", "b", ""] + + def test_none(self) -> None: + result = list(iter_node(None)) + assert result == [] + + def test_string(self) -> None: + result = list(iter_node("hej!")) + assert result == ["hej!"]