glimr/loom/runtime
Template Runtime
Generated Loom templates compile down to string concatenation expressions, but they need a shared set of helpers for escaping, conditional rendering, loop iteration, and attribute management. This module provides those building blocks so generated code stays minimal and focused on template structure while the runtime handles the messy details of safe HTML output.
Types
HTML has two fundamentally different attribute types: standard name=“value” pairs and boolean attributes (like disabled, checked) that render only their name when true and are omitted entirely when false. A sum type lets render_attributes handle both correctly.
pub type Attribute {
Attribute(name: String, value: String)
BoolAttribute(name: String, condition: Bool)
}
Constructors
-
Attribute(name: String, value: String) -
BoolAttribute(name: String, condition: Bool)
Template authors frequently need to style the first/last items differently, apply zebra striping, or display counts. Pre-computing all loop metadata into a record lets templates access these values without manual index arithmetic or length checks in the template itself.
pub type Loop {
Loop(
index: Int,
iteration: Int,
first: Bool,
last: Bool,
even: Bool,
odd: Bool,
count: Int,
remaining: Int,
)
}
Constructors
-
Loop( index: Int, iteration: Int, first: Bool, last: Bool, even: Bool, odd: Bool, count: Int, remaining: Int, )
Values
pub fn append(acc: String, value: String) -> String
Generated templates build HTML via a chain of pipe operations. This function provides a named entry point for string concatenation that integrates cleanly with Gleam’s pipe syntax, keeping generated code readable and consistent.
pub fn append_each(
acc: String,
items: List(item),
render_fn: fn(String, item) -> String,
) -> String
l-for loops need to render the same template body for each item while threading the accumulator through. A fold-based approach builds the output in a single pass without allocating intermediate string lists that would need joining afterward.
pub fn append_each_with_loop(
acc: String,
items: List(item),
render_fn: fn(String, item, Loop) -> String,
) -> String
When a template uses loop in its l-for body, it needs
metadata like index and first/last flags. Computing the Loop
record once per iteration and passing it to the render
callback avoids repeated list.length calls and keeps the
metadata fresh per item.
pub fn append_if(
acc: String,
condition: Bool,
render_fn: fn(String) -> String,
) -> String
l-if directives generate conditional blocks that either render content or skip it entirely. Using a callback for the true branch lets generated code defer rendering — the template body only executes when the condition holds, avoiding unnecessary work.
pub fn build_classes(items: List(#(String, Bool))) -> String
Templates often need to toggle CSS classes based on state (e.g., “active” when selected, “disabled” when locked). A list of #(name, Bool) tuples lets authors express this declaratively, and this function handles the filtering and space-joining at render time.
pub fn build_styles(items: List(#(String, Bool))) -> String
Same pattern as build_classes but for inline styles. Templates may need to toggle style rules based on state (e.g., “display: none” when hidden). Filtering by boolean and joining produces a valid style attribute value without manual string manipulation.
pub fn class(value: String) -> #(String, Bool)
:class lists expect uniform #(String, Bool) tuples, but static classes that are always present shouldn’t need a redundant True flag. This helper lets authors write class(“btn”) instead of #(“btn”, True), keeping the template syntax clean for the common case.
pub fn diff_tree_json(
old_json: String,
new_json: String,
) -> String
Compare two JSON-serialized trees and return a JSON diff containing only the changed dynamics, keyed by index. Returns “{}” if nothing changed.
pub fn display(value: a) -> String
The most common template operation: convert a value to a string and escape it. Combining both steps here means generated code for {{ variable }} is a single function call, keeping the generated output compact while ensuring XSS safety by default.
pub fn escape(value: String) -> String
User-provided data rendered into HTML can contain characters that would be interpreted as markup, enabling XSS attacks. Escaping through houdini neutralizes these characters so template output is safe by default without author intervention.
pub fn flatten_tree(tree: loom.LiveTree) -> String
Interleave statics and dynamics into a single HTML string. statics[0] + flatten(dynamics[0]) + statics[1] + …
pub fn inject_live_wrapper(
html: String,
module_name: String,
props_json: String,
) -> String
Live templates render through layout components that produce the /
structure. The live container div and script tag must be injected inside the body rather than wrapping the entire output, otherwise the HTML structure would be invalid with nested body tags.pub fn live_component_wrapper(
html: String,
module_name: String,
props_json: String,
) -> String
Wraps a live component’s rendered HTML in a data-l-live container with a signed token. Used when a live component is embedded inside a page — gives the component its own independent WebSocket actor via multiplexing.
pub fn live_ws_url() -> String
Dev environments use a proxy (e.g., Vite) that doesn’t forward WebSocket connections properly. Detecting the DEV_PROXY_PORT env var lets live templates connect directly to the app port in dev while using a relative path in production that works behind any reverse proxy.
pub fn map_each(
items: List(item),
render_fn: fn(item) -> loom.LiveTree,
) -> loom.Dynamic
Like append_each but returns a DynList for tree mode.
pub fn map_each_with_loop(
items: List(item),
render_fn: fn(item, Loop) -> loom.LiveTree,
) -> loom.Dynamic
Like append_each_with_loop but returns a DynList for tree mode.
pub fn merge_attributes(
base: List(Attribute),
extra: List(Attribute),
) -> List(Attribute)
Parent-provided attributes must combine with a component’s base attributes, but the merge rules differ by attribute type: classes and styles should concatenate (so both parent and component classes apply), while other attributes should override (parent wins for id, etc.).
pub fn render_attributes(attrs: List(Attribute)) -> String
Generated code builds attribute lists as runtime values, but the final HTML needs a flat string. Rendering them here with proper escaping and boolean attribute handling centralizes the HTML output rules so generated code doesn’t need to know about escaping or attribute syntax.
pub fn style(value: String) -> #(String, Bool)
Mirrors the class() helper for :style lists. Static styles that are always applied can be written as style(“color: red”) instead of #(“color: red”, True), keeping template syntax consistent between :class and :style.
pub fn to_string(value: a) -> String
Template variables can hold any Gleam type, but HTML output requires strings. Using string.inspect() as a universal converter means template authors don’t need explicit conversions for Int, Bool, or custom types — the runtime handles it transparently.
pub fn tree_to_json(tree: loom.LiveTree) -> String
Serialize a LiveTree to the JSON wire format for initial send. Format: { “s”: […statics], “d”: […dynamics] }