SuperAPI AI Directory

arrow-js-obsidian-templates

Frontend & websites

View repository

Updated 10-15 23:48

About this skill

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>`        // ❌ renders ONCE — never updates
html`<span>${() => data.count}</span>`  // ✅ tracked — updates only this slot

${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" : ""}">`        // ❌ THROWS

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}" />`                 // .prop = IDL property
html`<button @click="${() => data.count++}">+</button>`     // @event
html`${() => data.items.map((i) => html`<li>${i.text}</li>`.key(i.id))}`  // keyed list

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>` }   // shown while pending
)
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.

// ❌ TS2345 — narrowed param
html`<div @mousedown="${(e: MouseEvent) => resize(e)}">`
// ✅ widen, narrow inside
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):

  1. 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.
  2. No partial attribute values — the expression must be the whole value (see Attributes above), else Invalid HTML position.

Type-time (caught by tsc):

  1. 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):

  1. 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:

    // ❌ passes tsc, renders as "[object Object]" at runtime
    return ({ strings: [...] }) as unknown as ArrowTemplate;
    
    // ✓ use a real template expression or a type-guard conversion
    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.