hyperion_html

Function-based HTML templates for Hypermedia-Driven Applications.

Build node trees with hyperion_html/tag and hyperion_html/node, then render them to a string with to_html. Text and attribute values are HTML-escaped (via houdini); use node.unsafe_raw_text for markup you already trust.

One tree can be used to render the full page or fragments of it. Declare the fragments with node.fragment, then render only some of them by id by using with_only.

An optional typed context can be attached with with_context and read at render time by node.lazy callbacks (or modified per-subtree with node.scope).

Types

Render options for to_html.

pub type RenderOptions(ctx) {
  RenderOptions(only: List(String), doctype: Bool, context: ctx)
}

Constructors

  • RenderOptions(only: List(String), doctype: Bool, context: ctx)
    • with_only: which fragments to render
    • with_doctype: whether to prepend a doctype
    • with_context: the context to use in this tree

Values

pub fn options() -> RenderOptions(Nil)

Default options: full render, no doctype, and Nil as the context.

pub fn to_html(
  nodes nodes: List(node.Node(ctx)),
  options options: RenderOptions(ctx),
) -> String

Render nodes to an HTML string.

The same tree renders in two modes:

  • Full render (empty only list): every node is rendered, fragments included.
  • Partial render (list of IDs given to with_only): only the requested fragments are rendered.

Fragment callbacks are told which mode is being rendered (see node.fragment). The context value is handed to node.lazy callbacks, possibly updated along the way by node.scope.

Example

Renders "<p>Hello Quentin</p>" using an explicit parameter.

fn component_without_context(username: String) {
  hyperion_html.to_html(
    [tag.p([], [node.text("Hello " <> username)])],
    hyperion_html.options(),
  )
}

Renders "<p>Hello Quentin</p>" using a context. In most cases you should favor using explicit parameters, as shown above, since it makes the code clearer, but the context option can be useful to avoid “prop drilling”, which refers to passing data through multiple layers of components.

type SomeContext {
  SomeContext(username: String)
}

fn component_with_context() {
  let options =
    hyperion_html.options()
    |> hyperion_html.with_context(SomeContext(username: "Quentin"))

  hyperion_html.to_html(
    [
      tag.p([], [
        node.text("Hello "),
        node.lazy(fn(c: SomeContext) { [text(c.username)] }),
      ]),
    ],
    options,
  )
}
pub fn with_context(
  options: RenderOptions(ctx),
  context: ctx,
) -> RenderOptions(ctx)

Attach a render-time context, read by node.lazy callbacks.

The context is any value — typically a record holding request-scoped data such as the current user, a CSRF token or a locale. Because it is typed, reading a field is compile-checked, unlike string-keyed lookup tables. Trees that don’t read the context are polymorphic and work with any context value; options() defaults it to Nil.

Use it when you want part of the tree to be built at render-time, or to read the context deep in a tree without having to pass it through every component.

Example

type Ctx {
 Ctx(username: String)
}

hyperion_html.options()
|> hyperion_html.with_context(Ctx(username: "Quentin"))
|> hyperion_html.to_html(page, _)
pub fn with_doctype(
  options: RenderOptions(ctx),
  doctype: Bool,
) -> RenderOptions(ctx)

Prepend <!doctype html>\n to the output. Use it when rendering a complete document.

pub fn with_only(
  options: RenderOptions(ctx),
  only: List(String),
) -> RenderOptions(ctx)

Render only the fragments whose IDs are in the list. An empty list (the default) renders the full page instead.

✨ Search Document