Skip to content

Commit

Permalink
Allow conditional rendering based on bool.
Browse files Browse the repository at this point in the history
Technically this commit just avoids rendering True and False. It allows
for using short-circuiting `and` for conditional rendering.
  • Loading branch information
pelme committed Aug 3, 2024
1 parent 47165fc commit 8aeb291
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 7 deletions.
29 changes: 26 additions & 3 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,13 @@ and safe to directly insert variable data via f-strings:

### Conditional Rendering

`None` will not render anything. This can be useful to conditionally render some content.
`True`, `False` and `None` will not render anything. Python's `and` and `or`
operators will
[short-circuit](https://docs.python.org/3/library/stdtypes.html#boolean-operations-and-or-not).
You can use this to conditionally render content with inline `and` and
`or`.

```pycon title="Conditional rendering"
```pycon title="Conditional rendering with a value that may be None"

>>> from htpy import div, b
>>> error = None
Expand All @@ -55,14 +59,33 @@ and safe to directly insert variable data via f-strings:
<div></div>

>>> error = 'Enter a valid email address.'
>>> print(div[error and b[error]])
>>> print(div[has_error and b[error_message]])
<div><b>Enter a valid email address.</b></div>

# Inline if/else can also be used:
>>> print(div[b[error] if error else None])
<div><b>Enter a valid email address.</b></div>
```

```pycon title="Conditional rendering based on a bool variable"
>>> from htpy import div
>>> is_happy = True
>>> print(div[is_happy and "😄"])
<div>😄</div>

>>> is_sad = False
>>> print(div[is_sad and "😔"])
<div></div>

>>> is_allowed = True
>>> print(div[is_allowed or "Access denied!"])
<div></div>

>>> is_allowed = False
>>> print(div[is_allowed or "Access denied!"])
<div>Access denied</div>
```

### Loops / Iterating Over Children

You can pass a list, tuple or generator to generate multiple children:
Expand Down
10 changes: 9 additions & 1 deletion htpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ def iter_node(x: Node) -> Iterator[str]:
if x is None:
return

if x is True:
return

if x is False:
return

if isinstance(x, BaseElement):
yield from x
elif isinstance(x, str) or hasattr(x, "__html__"):
Expand Down Expand Up @@ -227,7 +233,9 @@ def __html__(self) -> str: ...

_ClassNamesDict: TypeAlias = dict[str, bool]
_ClassNames: TypeAlias = Iterable[str | None | bool | _ClassNamesDict] | _ClassNamesDict
Node: TypeAlias = None | str | BaseElement | _HasHtml | Iterable["Node"] | Callable[[], "Node"]
Node: TypeAlias = (
None | bool | str | BaseElement | _HasHtml | Iterable["Node"] | Callable[[], "Node"]
)

Attribute: TypeAlias = None | bool | str | _HasHtml | _ClassNames

Expand Down
7 changes: 4 additions & 3 deletions tests/test_children.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ def test_custom_element() -> None:
assert str(el) == "<my-custom-element></my-custom-element>"


def test_ignore_none() -> None:
assert str(div[None]) == "<div></div>"
@pytest.mark.parametrize("ignored_value", [None, True, False])
def test_ignored(ignored_value: Any) -> None:
assert str(div[ignored_value]) == "<div></div>"


def test_iter() -> None:
Expand Down Expand Up @@ -197,7 +198,7 @@ def test_callable_in_generator() -> None:
assert str(div[((lambda: "hi") for _ in range(1))]) == "<div>hi</div>"


@pytest.mark.parametrize("not_a_child", [1234, True, False, b"foo", object(), object])
@pytest.mark.parametrize("not_a_child", [1234, b"foo", object(), object, 1, 0])
def test_invalid_child(not_a_child: Any) -> None:
with pytest.raises(ValueError, match="is not a valid child element"):
str(div[not_a_child])

0 comments on commit 8aeb291

Please sign in to comment.