Usage¶
Elements are imported directly from the htpy
module as their name. HTML attributes are specified by parenthesis (()
/ "call"). Children are specified using square brackets ([]
/ "getitem").
Elements¶
Children can be strings, markup, other elements or lists/iterators.
Elements can be arbitrarily nested:
>>> from htpy import article, section, p
>>> print(section[article[p["Lorem ipsum"]]])
<section><article><p>Lorem ipsum</p></article></section>
Text/Strings¶
It is possible to pass a string directly:
>>> from htpy import h1
>>> print(h1["Welcome to my site!"])
<h1>Welcome to my site!</h1>
Strings are automatically escaped to avoid XSS vulnerabilities. It is convenient and safe to directly insert variable data via f-strings:
>>> from htpy import h1
>>> user_supplied_name = "bobby </h1>"
>>> print(h1[f"hello {user_supplied_name}"])
<h1>hello bobby </h1></h1>
Conditional Rendering¶
True
, False
and None
will not render anything. Python's and
and or
operators will
short-circuit.
You can use this to conditionally render content with inline and
and
or
.
>>> from htpy import div, b
>>> error = None
>>> # No <b> tag will be rendered since error is None
>>> print(div[error and b[error]])
<div></div>
>>> error = 'Enter a valid email address.'
>>> 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>
>>> 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:
>>> from htpy import ul, li
>>> print(ul[(li[letter] for letter in "abc")])
<ul><li>a</li><li>b</li><li>c</li></ul>
Note
The generator will be lazily evaluated when rendering the element, not directly when the element is constructed. See Streaming for more information.
A list
can be used similar to a JSX fragment:
>>> from htpy import div, img
>>> my_images = [img(src="a.jpg"), img(src="b.jpg")]
>>> print(div[my_images])
<div><img src="a.jpg"><img src="b.jpg"></div>
Custom Elements / Web Components¶
Custom elements / web
components
are HTML elements that contain at least one dash (-
). Since -
cannot be
used in Python identifiers, use underscore (_
) instead:
>>> from htpy import my_custom_element
>>> print(my_custom_element['hi!'])
<my-custom-element>hi!</my-custom-element>
Injecting Markup¶
If you have HTML markup that you want to insert without further escaping, wrap
it in Markup
from the markupsafe
library. markupsafe is a dependency of htpy and is automatically installed:
>>> from htpy import div
>>> from markupsafe import Markup
>>> print(div[Markup("<foo></foo>")])
<div><foo></foo></div>
If you are generating Markdown and want to insert it into an element,
use Markup
to mark it as safe:
>>> from markdown import markdown
>>> from markupsafe import Markup
>>> from htpy import div
>>> print(div[Markup(markdown('# Hi'))])
<div><h1>Hi</h1></div>
HTML Doctype¶
The HTML doctype is automatically prepended to the <html>
tag:
HTML Comments¶
Since the Python code is the source of the HTML generation, to add a comment to
the code, most of the time regular Python comments (#
) are used.
If you want to emit HTML comments that will be visible in the browser, use the comment
function:
>>> from htpy import div, comment
>>> print(div[comment("This is a HTML comment, visible in the browser!")])
<div><!-- This is a HTML comment, visible in the browser! --></div>
It is safe to pass arbitrary text to the comment function. Double dashes (--
)
will be removed to avoid being able to break out of the comment.
If you need full control over the exact rendering of the comment, you can create comments or arbitrary text by injecting your own markup. See the Injecting Markup section above for details.
Attributes¶
HTML attributes are defined by calling the element. They can be specified in a couple of different ways.
Elements Without Attributes¶
Some elements do not have attributes, they can be specified by just the element itself:
Keyword Arguments¶
Attributes can be specified via keyword arguments:
In Python, class
and for
cannot be used as keyword arguments. Instead, they can be specified as class_
or for_
when using keyword arguments:
Attributes that contain dashes -
can be specified using underscores:
Id/Class Shorthand¶
Defining id
and class
attributes is common when writing HTML. A string shorthand
that looks like a CSS selector can be used to quickly define id and classes:
>>> from htpy import div
>>> print(div(".foo.bar"))
<div class="foo bar"></div>
>>> from htpy import div
>>> print(div("#myid.foo.bar"))
<div id="myid" class="foo bar"></div>
Attributes as Dict¶
Attributes can also be specified as a dict
. This is useful when using
attributes that are reserved Python keywords (like for
or class
), when the
attribute name contains a dash (-
) or when you want to define attributes
dynamically.
>>> from htpy import button
>>> print(button({"@click.shift": "addToSelection()"}))
<button @click.shift="addToSelection()"></button>
>>> from htpy import label
>>> print(label({"for": "myfield"}))
<label for="myfield"></label>
Boolean/Empty Attributes¶
In HTML, boolean attributes such as disabled
are considered "true" when they
exist. Specifying an attribute as True
will make it appear (without a value).
False
will make it hidden. This is useful and brings the semantics of bool
to
HTML.
>>> from htpy import button
>>> print(button(disabled=True))
<button disabled></button>
Conditionally Mixing CSS Classes¶
To make it easier to mix CSS classes, the class
attribute
accepts a list of class names or a dict. Falsey values will be ignored.
>>> from htpy import button
>>> is_primary = True
>>> print(button(class_=["btn", {"btn-primary": is_primary}]))
<button class="btn btn-primary"></button>
>>> is_primary = False
>>> print(button(class_=["btn", {"btn-primary": is_primary}]))
<button class="btn"></button>
>>>
Combining Modes¶
Attributes via id/class shorthand, keyword arguments and dictionary can be combined:
>>> from htpy import label
>>> print(label("#myid.foo.bar", {'for': "somefield"}, name="myname",))
<label id="myid" class="foo bar" for="somefield" name="myname"></label>
Escaping of Attributes¶
Attributes are always escaped. This makes it possible to pass arbitrary HTML fragments or scripts as attributes. The output may look a bit obfuscated since all unsafe characters are escaped but the browser will interpret it correctly:
>>> from htpy import button
>>> print(button(id="example", onclick="let name = 'andreas'; alert('hi' + name);")["Say hi"])
<button onclick="let name = 'andreas'; alert('hi' + name);">Say hi</button>
In the browser, the parsed attribute as returned by
document.getElementById("example").getAttribute("onclick")
will be the
original string let name = 'andreas'; alert('hi' + name);
.
Escaping will happen whether or not the value is wrapped in markupsafe.Markup
or not. This may seem confusing at first but is useful when embedding HTML
snippets as attributes:
>>> from htpy import ul
>>> from markupsafe import Markup
>>> # This markup may come from another library/template engine
>>> some_markup = Markup("""<li class="bar"></li>""")
>>> print(ul(data_li_template=some_markup))
<ul data-li-template="<li class="bar"></li>"></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:
>>> from htpy import render_node, tr
>>> print(render_node([tr["a"], tr["b"]]))
<tr>a</tr><tr>b</tr>
render_node()
accepts all kinds of Node
objects.
You may use it to render anything that would normally be a children of another
element.
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:
>>> from htpy import ul, li
>>> 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(), 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>'
Passing Data with Context¶
Usually, you pass data via regular function calls and arguments via your components. Contexts can be used to avoid having to pass the data manually between components. Contexts in htpy is conceptually similar to contexts in React.
Using contexts in htpy involves:
- Creating a context object with
my_context = Context(name[, *, default])
to define the type and optional default value of a context variable. - Using
my_context.provider(value, lambda: children)
to set the value of a context variable for a subtree. - Adding the
@my_context.consumer
decorator to a component that requires the context value. The decorator will add the context value as the first argument to the decorated function:
The Context
class is a generic and fully supports static type checking.
The values are passed as part of the tree used to render components without using global state. It is safe to use contexts for lazy constructs such as callables and generators.
A context value can be passed arbitrarily deep between components. It is possible to nest multiple context provider and different values can be used in different subtrees.
A single component can consume as many contexts as possible by using multiple decorators:
Example¶
This example shows how context can be used to pass data between components:
theme_context: Context[Theme] = Context("theme", default="light")
creates a context object that can later be used to define/retrieve the value. In this case,"light"
acts as the default value if no other value is provided.theme_context.provider(value, lambda: subtree)
defines the value of thetheme_context
for the subtree. In this case the value is set to"dark"
which overrides the default value.- The
sidebar
component uses the@theme_context.consumer
decorator. This will make htpy pass the current context value as the first argument to the component function. - In this example, a
Theme
type is used to ensure that the correct types are used when providing the value as well as when it is consumed.
from typing import Literal
from htpy import Context, Node, div, h1
Theme = Literal["light", "dark"]
theme_context: Context[Theme] = Context("theme", default="light")
def my_page() -> Node:
return theme_context.provider(
"dark",
lambda: div[
h1["Hello!"],
sidebar("The Sidebar!"),
],
)
@theme_context.consumer
def sidebar(theme: Theme, title: str) -> Node:
return div(class_=f"theme-{theme}")[title]
print(my_page())
Output: