hyperion_html/node

This module holds all the AST node types needed to build an HTML tree.

Types

pub type Attribute =
  #(String, AttributeValue)
pub opaque type AttributeValue
pub type Attributes =
  List(#(String, AttributeValue))
pub type Children(ctx) =
  List(Node(ctx))

A template node, parameterised over the render-time context ctx.

Build nodes with the functions below and the tag helpers in hyperion_html/tag.

pub opaque type Node(ctx)

Values

pub fn attr(
  key key: String,
  value value: String,
) -> #(String, AttributeValue)

An attribute with a string value.

pub fn attr_values(
  key key: String,
  values values: List(String),
) -> #(String, AttributeValue)

Pass a list of values for an attribute, joined with spaces. Empty strings are ignored, so lists can be built unconditionally.

pub fn bool_attr(
  key key: String,
  rendered value: Bool,
) -> #(String, AttributeValue)

A boolean attribute, rendered bare when True (e.g., <input disabled>), omitted entirely when False.

pub fn comment(content: String) -> Node(ctx)

An HTML comment, rendered verbatim between <!-- and -->.

pub fn element(
  tag tag: String,
  attrs attrs: List(#(String, AttributeValue)),
  children children: List(Node(ctx)),
) -> Node(ctx)

A generic element with an opening and closing tag around children.

Renders as <tag attrs>children</tag>. For standard HTML tags, prefer the typed helpers in hyperion_html/tag; use this one for tags they don’t cover, such as custom elements or Web Components.

Example

node.element(
  "my-widget",
  [node.attr("variant", "card")],
  [node.text("hello")]
)

The element above would render as

<my-widget variant="card">hello</my-widget>
pub fn float_attr(
  key key: String,
  value value: Float,
) -> #(String, AttributeValue)

An attribute with a float value, rendered as its string form.

pub fn fragment(
  id id: String,
  children children: List(Node(ctx)),
) -> Node(ctx)

A named fragment of the page that can also be rendered on its own.

Fragments are the unit of partial rendering: one tree can be used for both full page and the partial responses of hypermedia frontends such as htmx.

In a full render the children are rendered as they are.

In a partial render, when the fragment’s id is listed in hyperion_html.with_only, only the requested fragments are rendered, and the rest of the page is left out.

Use this function when the fragment renders the same in both modes, and fragment_lazy or fragment_envelope when it should render itself differently depending on the mode.

Example

node.fragment("cart_item_1", [
  tag.span([], [node.text("Coffee mug")]),
  tag.button([node.attr("hx-post", "/cart/1/remove")], [node.text("Remove")]),
])
pub fn fragment_envelope(
  id id: String,
  envelope envelope: String,
  attrs attrs: List(#(String, AttributeValue)),
  children children: List(Node(ctx)),
) -> Node(ctx)

A fragment rendered bare in a full render, and wrapped in an element in a partial render.

This is a shorthand for a common case of fragment_lazy: in hypermedia frontends such as htmx 4, partials need to be enveloped in a plain element (e.g. <hx-partial>) telling where their content should be swapped.

For any other shape, use fragment_lazy.

Example

node.fragment_envelope(
  id: "cart_item_1",
  envelope: "hx-partial",
  attrs: [node.attr("hx-target", "#cart"), node.attr("hx-swap", "beforeend")],
  children: [
    tag.span([], [node.text("Coffee mug")]),
    tag.button([node.attr("hx-post", "/cart/1/remove")], [node.text("Remove")]),
  ],
)

In a full render the children are rendered as they are; in a partial render they are wrapped:

<hx-partial hx-target="#cart" hx-swap="beforeend">
  <span>Coffee mug</span>
  <button hx-post="/cart/1/remove">Remove</button>
</hx-partial>
pub fn fragment_lazy(
  id id: String,
  render render: fn(Bool) -> List(Node(ctx)),
) -> Node(ctx)

A fragment whose children are built at render time, depending on the mode.

The callback runs when the tree is rendered and receives a boolean telling whether the fragment was requested (meaning its id is in the only list given to hyperion_html.with_only). This lets the fragment render itself differently in a full or in a partial page render.

For example, you could wrap it in an envelope element (e.g. htmx 4’s <hx-partial>) carrying its own placement instructions. When the wrap is a plain element around unchanged children, prefer fragment_envelope.

Fragments that don’t care about the mode can ignore the parameter, or simply use fragment.

Example

node.fragment_lazy("cart_item_1", fn(selected) {
  let content = [
    tag.span([], [node.text("Coffee mug")]),
    tag.button([node.attr("hx-post", "/cart/1/remove")], [node.text("Remove")]),
  ]
  case selected {
    True -> [
      node.element(
        "hx-partial",
        [node.attr("hx-target", "#cart"), node.attr("hx-swap", "beforeend")],
        content,
      ),
    ]
    False -> content
  }
})

In a full render the fragment renders as its content:

<span>Coffee mug</span>
<button hx-post="/cart/1/remove">Remove</button>

In a partial render it wraps itself:

<hx-partial hx-target="#cart" hx-swap="beforeend">
  <span>Coffee mug</span>
  <button hx-post="/cart/1/remove">Remove</button>
</hx-partial>
pub fn int_attr(
  key key: String,
  value value: Int,
) -> #(String, AttributeValue)

An attribute with an integer value, rendered as its string form.

pub fn lazy(callback: fn(ctx) -> List(Node(ctx))) -> Node(ctx)

Defer building part of the tree until render time.

The callback receives the context passed to hyperion_html.to_html and returns the nodes to render. Use it when part of the tree can only be built during rendering, or to read the context deep in a tree without having to pass it through every component.

Example

type Ctx { Ctx(username: String) }

node.lazy(fn(ctx: Ctx) { [node.text("Hello " <> ctx.username)] })
pub fn none() -> Node(ctx)

This function renders nothing; it is the same as an empty text node. It’s a convenience to be used in conditional cases.

Example

fn maybe_render_name(name: option.Option(String)) -> node.Node(ctx) {
  case name {
    option.None -> node.none()
    option.Some(name) -> node.text(name)
  }
}
pub fn none_attr() -> #(String, AttributeValue)

This function renders nothing; it is the same as a false boolean attribute. It’s a convenience to be used in conditional cases.

Example

fn maybe_add_class(id: option.Option(String)) -> node.Attribute {
  case id {
    option.None -> node.none_attr()
    option.Some(id) -> node.attr("id", id)
  }
}
pub fn scope(
  update: fn(ctx) -> ctx,
  children: List(Node(ctx)),
) -> Node(ctx)

Render children with a modified context. Only the subtree is affected; nodes outside of it keep rendering with the surrounding context.

Example

type Ctx { Ctx(theme: String, csrf: String) }

node.scope(fn(ctx: Ctx) { Ctx(..ctx, theme: "dark") }, [always_dark_section()])
pub fn self_closing_element(
  tag: String,
  attrs: List(#(String, AttributeValue)),
) -> Node(ctx)

An element rendered as <tag attrs/>.

The self-closing slash is only meaningful in foreign content such as SVG and MathML, where XML-like syntax applies.

For HTML void elements (br, img, …) use void_element instead.

Example

node.self_closing_element("circle", [node.attr("r", "10")])

The element above would render as

<circle r="10"/>
pub fn text(content: String) -> Node(ctx)

A text node. Special characters are HTML-escaped at render time (< becomes &lt;, & becomes &amp;, etc.).

pub fn unsafe_raw_text(content: String) -> Node(ctx)

A text node rendered verbatim, without escaping.

Only use it for content you control or that is already escaped, as interpolating user input here would make your app vulnerable to cross-site scripting!

This is also how the raw content of script and style elements is rendered.

pub fn void_element(
  tag: String,
  attrs: List(#(String, AttributeValue)),
) -> Node(ctx)

An HTML void element that can’t have children by spec.

Renders as <tag attrs> with no closing tag, which is how void elements such as br, img, input, link or meta are written. For the standard ones, prefer the helpers in hyperion_html/tag, which already route to this function (tag.br, tag.img, tag.input, …).

Example

node.void_element("img", [node.attr("src", "/cat.png")])

The element above would render as

<img src="/cat.png">
✨ Search Document