🧩

Template & binding
Dynamic attributes, {#if}/{#if:destroy}, {#each}, bind:.

A .thr component's template is always valid XML — every node id is automatically cached (no hand-written findNode), and every {expr} attribute becomes a real, generated reactive push.

Static and dynamic attributes

<Rectangle id="background" color="0x1A1A1AFF" width="{width}" height="{height}">
  <Label id="label" text="{favoritesLabel}" color="{textColor}" />
</Rectangle>

A static attribute value containing </>/& can be written with the raw character (text="a < b") — the compiler escapes it correctly when generating the underlying XML. Don't pre-escape it by hand (text="a &lt; b"): the compiler would escape that text too, so the literal entity code ends up rendered on screen instead of the character you meant.

{#if} — toggle mode

Reactively binds visible on a synthetic wrapper Group. The subtree is always constructed and stays registered (including for focus purposes) — cheap toggling, no teardown.

FavoriteCounter.thr (excerpt)
{#if favoriteCount == 0}
  <Label id="emptyBadge" text="Add your first!" color="{textColor}" />
{/if}

{#if:destroy} — construct/destroy mode

Hand-constructs the subtree at runtime when the condition first becomes true, and tears it down (with focus correctly handed back, per the vacuum rule) when it goes false again — a genuinely clean slate on every remount, not a cheap visibility flip.

FavoriteCounter.thr (excerpt)
{#if:destroy showCelebration}
  <Label id="celebration" text="Nice streak!" color="{textColor}" />
{/if}

{#each} — keyed list rendering

{#each <collection> as <item> (<key>)} does a real keyed add/remove/reposition diff, preserving node identity across reorders. Nests with {#if}/{#if:destroy} in both directions, including loop-in-loop. The collection may be a plain array or a SceneGraph node (iterated over its own children).

ScheduleList.thr (excerpt)
{#if:destroy hasLoaded}
  {#each schedule as day (day.id)}
    <Label id="row" text="{prefix + day.title}" translation="{[rowX, day.y]}"
           focusable="true" on:key[OK]="{selectDay(day)}" />
    {#if:destroy day.isToday}
      <Label id="todayBadge" text="(today)" translation="{[todayBadgeX, day.y]}" />
    {/if}
  {/each}
{/if}

bind:

bind:<childField>={<stateName>} pulls a value out of a child node's field into a state, reactively, whenever the child's field changes. Despite the "two-way binding" name in early planning docs, this is one-directional only: child → state. It never pushes a value into the child — pushing, when wanted, is just an ordinary attr="{expr}" dynamic attribute on a separate attribute.

ScheduleList.thr (excerpt)
<TextEditBox id="searchBox" bind:text="{searchText}" focusable="true" />
<Label id="searchEcho" text="{searchText}" />

Reference implementation — apps/template-and-binding-demo

Every mechanism on this page has a router-mounted, scaled chapter in apps/template-and-binding-demo — 4 chapters (/attributes through /bind), reachable with REWIND/FAST-FORWARD once compiled and sideloaded. Each chapter shows a default example alongside a deliberately different, customized one — e.g. /each's plain 5-row list next to a "Reverse order"/"Rotate order" pair that reorders the SAME backing items, proving each row's own color travels with it (node identity preserved across the keyed diff, not destroyed and rebuilt), and /if-toggle-vs-destroy's two side-by-side counters showing that a {#if} panel's internal state survives a hide/show cycle while a {#if:destroy} panel's resets to zero. See findings/template-and-binding-demo-app.md for what each chapter covers and findings/demo-app-conventions.md for the app-structure convention it follows.

⚠️ Not (yet) supported

  • ○ bind: inside an {#each} body — rejected at compile time.
  • ○ No built-in loop index variable inside {#each} — a list item that needs its own layout position must carry that position as part of its own data (see the real-world workaround in ScheduleList.thr).
  • ○ A {#each} block's items are not reachable by an enclosing LayoutGroup's automatic child layout — the wrapper Group sits between them.
  • ○ Every {#each} item binding re-runs unconditionally on every reconcile — no fine-grained per-field diff.

Exact grammar: GRAMMAR.md. Full feature status: docs/features.md.