| name | arrow-js-obsidian-templates |
| description | Use when writing @arrow-js/core (v1.0.6) html template literals — reactive vs static (wrap reactive reads in arrow functions), full-value attribute binding where returning false removes the attribute, .property and @event binding, keyed lists, and async component(fn, { fallback }) via boundary(); plus the footguns — no literal HTML comments and no partial attribute values (both throw Invalid HTML position at render), and @event handlers must type the param as Event not a narrowed subtype like MouseEvent (TS2345). |
Arrow.js Templates (v1.0.6)
Rules for writing html\`` templates that render correctly in the browser and
inside an Obsidian plugin. Several of these are hard runtime errors, not style.
Reactive vs static — the core rule
import { html, reactive } from "@arrow-js/core"
const data = reactive({ count: 0 })
html`<span>${data.count}</span>`
html`<span>${() => data.count}</span>`
${value} is read once at mount. ${() => value} is tracked: Arrow records the
reactive reads inside the function and re-runs only that slot when they
change. Forgetting () => is the #1 "why isn't it updating" bug.
Attributes
An attribute expression must be the entire attribute value:
html`<div class="${() => (active() ? "tab is-active" : "tab")}">`
html`<div class="tab ${() => active() ? "is-active" : ""}">`
Partial values ("static ${…}") are not registered as placeholders and throw
Invalid HTML position. Build the full string in one expression.
Returning false from an attribute expression removes the attribute (vs ""
which keeps it present-but-empty) — the clean way to toggle disabled/hidden:
html`<button disabled="${() => !canSubmit()}">Save</button>`
Properties, events, lists
html`<input .value="${() => data.text}" />`
html`<button @click="${() => data.count++}">+</button>`
html`${() => data.items.map((i) => html`<li>${i.text}</li>`.key(i.id))}`
Keyed lists preserve DOM across reorders; mutate item fields in place for
fine-grained updates instead of replacing the whole array.
Async sections
import { boundary } from "@arrow-js/framework"
const Card = component(
async () => { const d = await load(); return html`<div>${d.label}</div>` },
{ fallback: html`<div>Loading…</div>` }
)
html`${boundary(Card())}`
Works client-side with no SSR. boundary() only takes { idPrefix }; the
visible loading state comes from the async component's fallback option.
Event handler typing
An @event handler must be assignable to (e: Event) => void. A handler typed
with a narrowed subtype fails (parameter contravariance) — tsc reports
TS2345 … not assignable to 'ArrowExpression'. Type the param Event and
narrow inside; no-arg handlers are always fine.
html`<div @mousedown="${(e: MouseEvent) => resize(e)}">`
html`<div @mousedown="${(e: Event) => resize(e as MouseEvent)}">`
(document.addEventListener handlers are unaffected — they're DOM-lib typed.)
Hard footguns
Render-time (pass tsc, fail only at render — always verify in a browser):
- No literal HTML comments inside templates. Arrow uses HTML comments as
expression-slot markers, so a literal
<!-- … --> inflates the slot count and
throws Invalid HTML position. Use JS // comments outside the template.
- No partial attribute values — the expression must be the whole value
(see Attributes above), else
Invalid HTML position.
Type-time (caught by tsc):
- No narrowed
Event subtype in @event handlers (see Event handler
typing above).
CI guards all three: test/template-footguns.test.mjs scans for <!-- and for
inline handlers typed with a narrowed Event subtype; tsc covers the rest.
Type-time (silently passes tsc, fails at render — CI cannot catch):
-
as unknown as ArrowTemplate / as unknown as ArrowExpression double-cast.
Casting through unknown to the Arrow template type silences tsc while
producing a value that renders as escaped text or [object Object] at runtime.
This exact pattern is the standard "fix the types later" shortcut, but for a
template library it converts a correctness bug into a passing build:
return ({ strings: [...] }) as unknown as ArrowTemplate;
return html`${realExpression}`;
If a genuine narrowing is needed, prefer a helper function with an explicit
type guard over a double-cast through unknown. Treat any as unknown as
targeting the Arrow template types as a red flag in code review.
Beyond templates — runtime/lifecycle footguns
The footguns above are about the html\`template itself. There is a second class of trap in **reactivity + imperative DOM** (floating UI, positioning, focus) that also passestscand only bites at render — no DOM nodes inreactive(); requestAnimationFrameis paused in background tabs (usenextTick); always-mount floating elements and toggle is-hiddenrather than conditionally creating them; drive open/close side-effects from a class getter, notwatch. These live in the **arrow-js-obsidian-patterns** skill and in [docs/arrow-notes.md`](../../docs/arrow-notes.md).
Read them before building popovers, menus, or anything positioned.