Templates + slots¶
<template name="…"> defines a reusable markup body. Slots, id
auto-namespacing, and default attribute values are collectively enough
to refactor any repeated block out of a .lmn file and keep
per-instance state isolated.
This chapter walks the surface end-to-end: a before/after refactor, the core features, and the deferred work.
Why templates?¶
Without templates, repeated UI looks like this:
<column gap="12">
<column class="card card-primary">
<label class="card-title" text="User profile" />
<label class="card-body" text="Edit your name and avatar." />
</column>
<column class="card card-primary">
<label class="card-title" text="Team settings" />
<label class="card-body" text="Configure shared resources." />
</column>
<column class="card card-danger">
<label class="card-title" text="Danger zone" />
<label class="card-body" text="Permanent destructive actions." />
</column>
</column>
Templates cut the repetition without introducing a component lifecycle or a custom view-model:
<template name="card" variant="primary">
<column class="card card-{variant}">
<slot />
</column>
</template>
<column gap="12">
<card>
<label class="card-title" text="User profile" />
<label class="card-body" text="Edit your name and avatar." />
</card>
<card>
<label class="card-title" text="Team settings" />
<label class="card-body" text="Configure shared resources." />
</card>
<card variant="danger">
<label class="card-title" text="Danger zone" />
<label class="card-body" text="Permanent destructive actions." />
</card>
</column>
Three features are doing work here: <slot/> (the inner content
substitution point), {variant} placeholder (per-use-site attribute
substitution), and the variant="primary" default (kicked in for the
first two <card>s that didn't override it).
Defining a template¶
<template name="X" {default-attrs}>…body…</template> defines a
reusable subtree. Defaults are key="value" pairs on the <template>
tag itself.
<template name="card" variant="primary" size="md">
<column class="card card-{variant} card-{size}">
<slot />
</column>
</template>
name="card"— the use-site tag name.variant="primary"andsize="md"— defaults. Use-sites that omit them inherit; use-sites that passvariant="danger"override.{variant}and{size}inside the body — textual placeholders. At expand-time the parser substitutes the resolved value (use-site override > template default).
Templates may live anywhere in the file. The parser collects them in a first pass before walking the layout tree, so use-sites can appear before the definition.
Using a template¶
Two equivalent forms:
<!-- Tag name = template name. -->
<card variant="danger">
<label text="Heads up" />
</card>
<!-- Explicit form — useful when the template name shadows something. -->
<use template="card" variant="danger">
<label text="Heads up" />
</use>
Self-closing uses get no slot content:
Pair with a default for graceful empty:
<slot default="…"> falls back to the literal text when the use-site
provides no child elements.
Id auto-namespacing¶
When a template instance gets an id, inner ids stack:
<template name="actions">
<row>
<button id="save" text="Save" />
<button id="cancel" text="Cancel" />
</row>
</template>
<actions id="user-card" />
<actions id="team-card" />
The first instance spawns buttons with ids user-card:save and
user-card:cancel; the second instance produces team-card:save and
team-card:cancel. The colon is the namespace delimiter.
The matching Rhai routing handles both shapes:
// Fires for BOTH user-card:save AND team-card:save via suffix fallback.
on("click", "save", "do_save");
// Per-instance routing — only fires for the user-card instance.
on("click", "user-card:save", "do_user_save");
fn do_save(id) {
// `id` is the fully-qualified id; use local_id to find siblings.
let label_id = local_id(id, "status");
set_text(label_id, "Saved!");
}
local_id(source, suffix) returns a sibling id within the same
template instance. Multi-level prefixes (a:b:save) stack — the result
is a:b:status. Source ids without a colon (top-level entities)
return suffix unchanged.
Defaults vs use-site overrides¶
<template name="banner" tone="info" closable="false">
<row class="banner banner-{tone}">
<label grow="1" text="{message}" />
<button id="close" text="×" />
</row>
</template>
<banner message="Build started." />
<banner message="Build failed." tone="danger" />
<banner message="Login expires soon." tone="warn" closable="true" />
Three rules:
- Use-site wins.
<banner tone="danger">overrides thetone="info"default for that one instance. - Missing default = empty.
messagehas no default, so the first<banner>would rendertext=""— that's a parse choice, not a parse error. Author defensively. - Defaults stack with placeholders.
class="banner banner-{tone}"resolves with the chosentoneper-instance.
Slot content¶
The slot content is the use-site's inline children. Attributes on the
use-site tag itself flow through the placeholder substitution; the
inner element tree slots in where <slot/> lives.
<template name="dialog">
<overlay class="modal-dim">
<column class="modal-body" width="420">
<slot />
</column>
</overlay>
</template>
<dialog>
<label text="Are you sure?" />
<row gap="10">
<button id="cancel" text="Cancel" />
<button id="confirm" text="OK" />
</row>
</dialog>
The two children of <dialog> land where <slot/> was. There's only
one slot per template today — named slots (<slot name="header"/>)
are on the deferred list.
A bigger refactor — list items¶
Before:
<for each="todos" key="id">
<row class="list-row" align="center" gap="10">
<label width="32" text="{row.idx}" />
<label grow="1" text="{row.label}" />
<label class="pill" text="{row.status}" />
<button id="dismiss-{row.id}" text="×" />
</row>
</for>
After:
<template name="todo-row">
<row class="list-row" align="center" gap="10">
<label width="32" text="{idx}" />
<label grow="1" text="{label}" />
<label class="pill" text="{status}" />
<button id="dismiss" text="×" />
</row>
</template>
<for each="todos" key="id">
<todo-row id="row-{row.id}" idx="{row.idx}" label="{row.label}" status="{row.status}" />
</for>
// Auto-namespacing: dismiss button ids are row-1:dismiss, row-2:dismiss, …
on("click", "dismiss", "handle_dismiss");
fn handle_dismiss(id) {
// local_id(id, "label") = row-3:label etc.
let row_id = id.split(":")[0]; // e.g. "row-3"
// … remove the row from todos signal_array …
}
Two wins: the markup is denser, and the dismiss handler is one
function regardless of how many rows exist.
Combining with <for>¶
<for each> row-field substitution ({row.label}) and <template>
attribute substitution ({tone}) use the same expansion rule and
compose cleanly. <for> evaluates first (per-row item context), then
the template expansion runs on the substituted use-site tag.
Deferred features¶
These are not shipped yet:
- Named slots (
<slot name="header"/>and<header slot="header">…</header>at the use-site) for multi-slot layouts like cards with separate title + body + footer sections. - Scoped signals — today
signal("count")is global. Per-instance signals scoped to a template instance would let two<counter />uses keep independent state without the author writingsignal("count-" + instance_id)manually. on_mount(id, fn)/on_unmount(id, fn)lifecycle hooks.:hostCSS pseudo-class to target the template root element from the template's own stylesheet.
Limitation: placeholder substitution is textual.
{variant}resolves through a literal string replace at parse time, so placeholders inside attribute values that need careful escaping (e.g.text="Click for {kind} info"where{kind}contains<) currently inherit XML's escaping rules. The widget-garden app's template demos use only safe characters as a workaround.