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 render_node() to render elements without parents. #32

Closed
wants to merge 1 commit into from
Closed
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
45 changes: 44 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,20 +290,63 @@ snippets as attributes:
<ul data-li-template="&lt;li class=&#34;bar&#34;&gt;&lt;/li&gt;"></ul>
```

## 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"]]))
<tr>a</tr><tr>b</tr>
```

`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
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: '<ul>'
got a chunk: '<li>'
got a chunk: 'a'
got a chunk: '</li>'
got a chunk: '<li>'
got a chunk: 'b'
got a chunk: '</li>'
got a chunk: '</ul>'
```

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: '<li>'
got a chunk: 'a'
got a chunk: '</li>'
got a chunk: '<li>'
got a chunk: 'b'
got a chunk: '</li>'
```
10 changes: 7 additions & 3 deletions htpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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")

Expand Down Expand Up @@ -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"</{self._name}>"

def __repr__(self) -> str:
Expand Down Expand Up @@ -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: ...

Expand Down
50 changes: 50 additions & 0 deletions tests/test_nodes.py
Original file line number Diff line number Diff line change
@@ -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, "<div>a</div>")

def test_list(self) -> None:
result = render_node([tr["a"], tr["b"]])

assert_markup(result, "<tr>a</tr><tr>b</tr>")

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 == ["<div>", "a", "</div>"]

def test_list(self) -> None:
result = list(iter_node([tr["a"], tr["b"]]))
assert result == ["<tr>", "a", "</tr>", "<tr>", "b", "</tr>"]

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!"]