# builtIns.tree
`builtIns.tree` builds DOM elements from a view model (VM), and on later calls **updates them in place**. It's a handler for [assignFrom](./assignFrom.md): you describe the elements in a `map`, one entry per VM key, and Tree creates them inside a target element.
The description is a terse, 100% declarative, JSON-friendly notation, which is quick to parse and free of side effects. Writing it by hand can be fiddly, so an optional lit-like helper, `` tree`…` ``, lets you write HTML-like templates that produce the same notation.
**Demo:** [demos/tree-demo.html](../demos/tree-demo.html) builds a launch summary from a JSON API response. Run `npm run serve`, then open `/demos/tree-demo.html`.
**Contents:**
- **Getting started:** [Basic Example](#basic-example) · [Element Specifications](#element-specifications) · [Keys](#keys) · [Second and Later Calls](#second-and-later-calls)
- **Building blocks:** [`` tree`…` ``](#authoring-with-tree-optional) · [Arrays](#arrays) · [Switches](#switches) · [Nested Trees](#nested-trees) · [Itemscope Managers](#itemscope-managers-as-view-models) · [Events](#events) · [Snippets](#snippets-static-html)
- **Going further:** [Targets](#targets) · [Config](#config) · [Compiling](#compiling-compile-experimental) · [Performance Notes](#performance-notes)
- **Server rendering:** [Server Side Rendering and hydration](#server-side-rendering-wip) · [Pure building blocks](#pure-building-blocks-for-rendering-elsewhere)
- **Reference:** [More on `` tree`…` ``](#more-on-tree) · [Editor support](#editor-support-vs-code)
## Basic Example
```html
API Response
```
```JavaScript
const vm = {
patch: { small: './data/mission-patch.svg', alt: 'Demo Mission 7 patch' },
};
assignFrom(document.getElementById('api-response'), {
'?. =>': {
do: 'builtIns.tree',
map: {
patch: ['img', { src: '?.small', alt: '?.alt' }],
}
}
}, { from: vm });
```
Result:
```html
API Response
```
Use `assignFromAsync` when you need the tree to exist after the `await`. The sync `assignFrom` starts handlers fire-and-forget. The first call has to load `tree.js`, so the tree appears just after `assignFrom` returns. Once `tree.js` is loaded, later calls render synchronously.
The `🌲` emoji alias works too, when `builtInEmoji` is passed as `handlers`.
### A little more
Each map value is an **element specification**. Here are the common shapes, one per key: a text shorthand, an attribute with static children, and a literal:
```JavaScript
const vm = {
name: 'Demo Mission 7',
rocket: { name: 'Falcon 9', type: 'rocket' },
success: true,
};
assignFrom(container, {
'?. =>': {
do: 'builtIns.tree',
map: {
name: ['h2', '?.'], // [tag, text]: '?.' is the key's value
rocket: ['section', { 'class :=': 'card' }, [ // [tag, pattern, children]
['h3', '?.name'], // children resolve against vm.rocket too
['p', '?.type'],
]],
success: ['span', { 'class :=': 'badge', textContent: 'Success' }], // literals
},
},
}, { from: vm });
```
Result (line breaks added):
```html
Demo Mission 7
Falcon 9
rocket
Success
```
The same map, written with the optional [`` tree`…` ``](#authoring-with-tree-optional) helper:
```JavaScript
import { tree } from 'assign-gingerly/DX/tree.js';
map: {
name: tree`
${'?.'}
`,
rocket: tree`
${'?.name'}
${'?.type'}
`,
success: tree`Success`,
}
```
**Call it again** with a changed VM, and Tree updates these same elements in place rather than rebuilding them (see [Second and Later Calls](#second-and-later-calls)).
## Element Specifications
```TypeScript
type ElementSpecification =
| Header
| [Header, Pattern | string]
| [Header, Pattern | string, ElementSpecification[] | Repeat | NestedTree] // static children, a repeat (see Arrays) or a nested tree (see Nested Trees)
| Switch // one of several specifications, picked by the data (see Switches)
| Snippet; // static HTML from the snippets option (see Snippets)
type Header = tagName | { tag: tagName; ish?: string | ManagerClass }; // see Itemscope Managers below
type Repeat = { each: ElementSpecification; of?: string; key?: string; empty?: ElementSpecification };
type NestedTree = { map: Record; markerName?: string; keepNullish?: boolean; aka?: Record; compile?: boolean };
type Switch = { switch: string; cases: Record; default?: ElementSpecification | null };
type Snippet = { snippet: string; holes?: Record }; // see Snippets
type Hole = Pattern | string | [Pattern | string, (ElementSpecification[] | Repeat | NestedTree)?];
```
- **The pattern** is an [assignFrom](./assignFrom.md) pattern applied to the created element, with `from` set to **the key's VM value**. So `'?.small'` above reads `vm.patch.small`.
- **A string in the pattern slot is `textContent`**: `['h3', 'Crew']` (literal) or `['b', '?.name']` (a path, reference or pipe, resolved like any RHS value). It's shorthand for `{ textContent: … }`.
- **Shorter keys inside pattern objects:** give the tree config an `aka`, e.g. `aka: { _: 'textContent' }`, and write `{ className: 'name', _: '?.' }`, `'_ ?=': [...]` or `'_ =&': { join: [...] }`. It's merged over `options.aka` and applies to this tree's patterns only. Like any `aka`, it also rewrites RHS path segments, so pick an alias that isn't also a data key.
- **Children** are static structure. Their patterns resolve against **the same value as their parent**:
```JavaScript
rocket: ['section', { className: 'card' }, [
['h3', { textContent: '?.name' }], // vm.rocket.name
['p', { textContent: '?.type' }], // vm.rocket.type
]]
```
- **The whole VM** is available as `$parent` (and `$root`, for a top-level tree), at every depth: `'$parent?.upcoming'`. See [root-references.md](./root-references.md).
- **`template` elements** receive their children in `.content`, not as light-DOM children.
- **Patterns set properties** (via assignGingerly). For attributes, use the [` :=` operator](./attribute-operator.md): `{ 'class :=': 'card', 'aria-label :=': '?.label' }`. Literal ` :=` values are what a future compile step can bake into a template, so prefer them for statics.
- **Formatting** (dates, units, conditional text) comes from [pipes](./pipes.md) passed in the options: `'?.date_utc?.parseDate?.longUtc'`.
- **Don't combine `textContent` with children**: setting `textContent` replaces them. Tree puts them back on the next call, after the text.
Bare class headers and `$:` headers are reserved for a later phase and throw for now. Use `{ tag, ish }` instead.
## Keys
| Situation | Behavior |
|---|---|
| Order | Map order, then `'*'` keys (below) in VM key order |
| Value is `null` / `undefined` | Skipped (an existing element is removed), unless `keepNullish: true` |
| Map key missing from the VM | Skipped as above, with a `console.warn` on the tree's first render (likely a typo) |
| The same VM key more than once | Add a trailing comment to the extra keys: `'rocket /* summary */'` (see below) |
| The whole VM as one element, or a nested value | A path key: `'?.'`, `'?.congress?.house'` (see [Path keys](#path-keys-the-whole-vm-or-a-value-deeper-in-it)) |
### The same VM key more than once: a comment in the key
A map key can end with a `/* … */` comment. The comment is ignored when looking up the VM key, so a map can render one VM key in several places:
```JavaScript
map: {
name: ['h2', '?.'],
rocket: ['section', { 'class :=': 'card' }, [['h3', '?.name'], ['p', '?.type']]],
success: ['span', { 'class :=': 'badge', textContent: 'Success' }],
'rocket /* summary */': ['p', { 'class :=': 'summary', textContent: '?.name' }], // vm.rocket again, after success
}
```
- **Each key is its own entry,** with its own element, rebound on its own. The key *as written* is the entry's identity, so `rocket` and `'rocket /* summary */'` don't interfere.
- **Spaces around the comment are trimmed:** `'rocket/*summary*/'` and `'rocket /* summary */'` both name `rocket`.
- **Prefer descriptive comments** (`/* summary */`) over numbers (`/*2*/`): they say why the entry exists.
- **The other key rules use the VM key:**
- nullish values skip every entry for that key;
- `'*'` treats a commented key as naming its VM key, so it isn't rendered again;
- the typo warning checks the VM key, and names the key as written.
- **One comment, at the end:** a comment anywhere else (`'roc /* x */ ket'`), two comments, or a key that's only a comment throws.
- **o2h** renders commented keys the same way (they share `mapEntries`). A VM key that really contains `/*` can't be named in a map; shape the VM instead.
### Path keys: the whole VM, or a value deeper in it
A map key starting with `?.` is a **path** into the VM, instead of a VM key. `'?.'` is the VM itself, so one specification can render a whole object as one element, with any wrappers it needs:
```JavaScript
// /products/3 → { id, title, price, image, rating: { rate, count }, … }
map: {
'?.': tree`
${'?.title'}
${'?.price?.usd'}
`,
}
```
Deeper paths render the value they lead to, and mix with ordinary keys and comments:
```JavaScript
map: {
title: ['h1', '?.'],
'?.congress?.houseOfRepresentatives': ['p', '?.seats'], // vm.congress.houseOfRepresentatives
'?. /* footer */': tree``, // the whole VM again
}
```
- **Plain property steps:** a path key follows properties (`?.a?.b`), with no pipes, references or methods. Format inside the specification instead (`'?.price?.usd'` above).
- **Inside the specification,** `?.` is the value at the path (the VM itself for `'?.'`), and `$parent` is the VM, as for any top-level key.
- **The key rules apply to the path's value:** nullish skips the entry (unless `keepNullish`), and a path leading nowhere gets the typo warning on the first render. `'?.'` works with a primitive VM too.
- **`'*'`:** a one-step path key (`'?.title'`) names its VM key, as `'title'` would. `'?.'` and deeper paths name none.
- **o2h, `compile` and `adopt`** all handle path keys, since they share `mapEntries`.
### Default specification: `'*'`
The `'*'` key applies to every VM key the map doesn't name. A `'*'` specification without a pattern gets `{ textContent: '?.' }`, so this gives a "just show me the data" view in one line:
```JavaScript
map: { name: ['h2', { textContent: '?.' }], '*': 'span' }
// { id: 'demo-7', name: 'Demo Mission 7', flight: 7 }
// →
Demo Mission 7
demo-77
```
Object values render as `[object Object]` with the default pattern. Give `'*'` its own pattern, or map those keys explicitly.
## Second and Later Calls
Tree remembers the elements it created, keyed by its start marker. On later calls it walks the specification and those elements in lockstep, and **re-applies each pattern in place**. Nothing is queried or diffed:
- A key whose **structure** changed (a different tag, or a different number of children) is rebuilt; the rest stay untouched. Structure is compared structurally, so a config written inline (new objects on every call) still rebinds in place.
- A key whose value **appears or disappears** is created or removed, in map order.
- An element **removed or moved by other code** is rebuilt.
- Markers found **without remembered state** (cloned or server-rendered markup) are cleared and the tree is rebuilt, unless the config has [`adopt: true`](#hydration-adopt): then Tree adopts the existing elements when they match the spec.
## Authoring with `` tree`…` `` (optional)
Nested `[tag, pattern, children]` arrays are easy to get wrong by hand. [`DX/tree.js`](../DX/tree.ts) provides a lit-like tagged template that **produces the same tree notation**. (In VS Code, two extensions highlight and check it; see [Editor support](#editor-support-vs-code).)
Ideally, use of the DX library would be done during the build or server-side processing step, keeping the assign-gingerly instructions easy to digest via its terse, JSON, declarative, side-effect free syntax. However, it can be used in the browser as well.
For example:
```JavaScript
import { tree, each } from 'assign-gingerly/DX/tree.js';
import { paths } from 'assign-gingerly/DX/paths.js';
const p = paths(); // untyped here; see "Typing the paths" below
const map = {
launch: tree`
${p.name}
Launched ${p.date} by ${p.rocket}
${'?.name'}
`,
};
// → ['article', { 'class :=': 'card', 'aria-label :=': '?.label' }, [
// ['h3', { 'class :=': 'title', textContent: '?.name' }],
// ['p', { '?. =&': { microJoin: ['Launched ', '?.date', ' by ', '?.rocket'] } }],
// ['ul', {}, { each: ['li', { textContent: '?.name' }], of: '?.crew', key: '?.id' }],
// ['button', { ' +=': { on: 'click', call: 'launch' }, textContent: 'Launch' }],
// ]]
```
**Typing the paths.** With a type for the view model, `p.name`, `p.crew` and so on are checked and autocompleted, and a typo is an error.
In **TypeScript**, pass the type to `paths`:
```TypeScript
interface Launch { label: string; name: string; date: string; rocket: string; crew: { id: string; name: string }[] }
const p = paths();
```
In **JavaScript with JSDoc**, annotate `p` with `PathProxy`, the type `paths()` returns:
```JavaScript
// @ts-check (or "checkJs": true in jsconfig.json / tsconfig.json)
import { paths } from 'assign-gingerly/DX/paths.js';
/**
* @typedef {object} Launch
* @property {string} label
* @property {string} name
* @property {string} date
* @property {string} rocket
* @property {{ id: string, name: string }[]} crew
*/
/** @type {import('assign-gingerly/DX/paths.js').PathProxy} */
const p = paths();
```
The type can also come from elsewhere: `@typedef {import('./launch-api.js').Launch} Launch`, or `@typedef {typeof launchJson} Launch` for an imported JSON sample.
| In the template | Becomes |
|---|---|
| `name="literal"`, `name` (no value) | `'name :=': 'literal'` / `true`: an **attribute**, baked under `compile` |
| `name=${value}` | `'name :=': value` |
| `name="text ${value}"` (text mixed with values) | `'name :=': { join: ['text ', value] }`: one string |
| `.prop=${value}` | `prop: value` (a property, as in lit) |
| `@event=${{ call: 'm' }}` or `@event=${'m'}` | an event binding (`' +='`, then `'?. +='`; at most two per element) |
| `ish=${Manager}` | the `{ tag, ish }` header |
| `${each(of, { key, empty })}` in attribute position | the repeat; the element's single child is the item |
| one `${value}`, or literal text, as content | `textContent` |
| text mixed with `${…}` | a [`microJoin`](./sync-ops.md#microjoin) (an element per value) |
| `
Launched ${p.date}
` | a `microJoin` whose values have props: `{ prop: 'date', val: '?.date' }` (see below) |
| `` ${tree`…`} `` as content | a child element (composition) |
| `${choose(on, cases, fallback)}` as content, or as a whole map value | a [switch](#switches) (`{ switch, cases, default }`) |
| `${snippet(name)}` as content, or as a whole map value | a [snippet](#snippets-static-html) (`{ snippet: name }`) |
| `${{ '_ ?=': […], … }}` in attribute position | merged into the pattern: for keys markup can't express (ternaries, handlers, sync ops) |
| `${null}`, `${undefined}`, `${false}`, `${''}` as content | nothing (e.g. an optional child) |
[`demos/tree-demo.html`](../demos/tree-demo.html) is written with `tree`, including a pattern spread for its ternaries, an optional child, and `choose` switches. [`demos/tree-demo-with-managed-template-list.html`](../demos/tree-demo-with-managed-template-list.html) writes a similar tree in raw notation, for comparison.
More: [whitespace, microdata props, errors and static analysis](#more-on-tree), and [editor support](#editor-support-vs-code).
## Arrays
Put a **repeat object** in the children slot, in place of the array of static children:
```JavaScript
crew: ['ul', { className: 'crew' }, {
each: ['li', {}, [
['strong', { textContent: '?.name' }],
['span', { textContent: '?.role' }],
]],
key: '?.name',
}],
failures: ['ul', {}, {
each: ['li', { textContent: '?.reason' }],
empty: ['li', { className: 'muted', textContent: 'No failures recorded.' }],
}],
links: [{ tag: 'nav', ish: 'links-manager' }, {}, [
['ul', {}, { of: '?.linkList', each: ['li', {}, [['a', { href: '?.href', textContent: '?.name' }]]], key: '?.name' }],
]],
```
| Field | Meaning |
|---|---|
| `each` (required) | The specification stamped once per item. It can have children, a `{ tag, ish }` header (one manager per item, each with its own `ctx.rebind()`), and nested repeats. |
| `of` | Path to the array (any iterable), relative to the element's value. Default `'?.'`: the value itself. Anything nullish or not iterable counts as empty. |
| `key` | Path into each item that identifies it across calls (`'?.id'`, or `'?.'` for primitives). Default: the index. Duplicate keys log one warning and fall back to the index for the duplicates. |
| `empty` | Rendered **instead of** the items when there are none. |
- **Inside `each`:** `?.` is the **item**, and `$parent` is the **object holding the array**: the VM for a top-level key like `crew`, or the value `of` was resolved against (e.g. the links manager). `$root` is unchanged.
- **The repeating element owns its content.** Its own pattern still applies (resolving against its value, e.g. `title: '?.length'`), but it can't also have static children.
- **Reconciliation on later calls:** items whose key is still present are rebound in place (and moved if needed), new keys are created, missing keys are removed, and `empty` comes and goes as the array empties and fills.
- **Synchronous**, like the rest of Tree, so the lists are rendered as soon as `assignFromAsync` resolves.
- **Large lists:** see [Compiling](#compiling-compile-experimental), which clones precompiled templates instead of creating each element.
### When `manageTemplateList` is still the better fit
Native arrays are the default. Delegating a list to [`builtIns.manageTemplateList`](./manage-template-list.md) (as a ` =>` handler key in the list element's pattern) still makes sense when:
- **the item markup lives in HTML:** a `` in the page, possibly server-rendered, and possibly filled by `infer` (e.g. by `itemprop`);
- **an item is several sibling elements**, such as `
` / `
` pairs (`configs`), whereas a Tree `each` is one element;
- **a very long list needs time-slicing** (`yieldEvery`), or `waitForSettled`;
- **removed items should be hidden rather than discarded** (`forget: false`).
The trade-off is that the handler runs asynchronously. Inside Tree it's fire-and-forget, so the list fills in just after the tree. [demos/tree-demo-with-managed-template-list.html](../demos/tree-demo-with-managed-template-list.html) shows that approach.
## Switches
A **switch** renders one of several specifications, picked from the data **on every render**. It goes anywhere an element specification goes: a `map` value, a static child, a repeat's `each` (so each item picks its own markup) or `empty`, or a case of another switch.
```JavaScript
mission: {
switch: '?.phase',
cases: {
hold: ['section', { 'class :=': 'card hold' }, [['h3', 'Hold'], ['p', '?.reason']]],
scrubbed: ['section', { 'class :=': 'card scrubbed' }, [['h3', 'Scrubbed'], ['p', '?.reason'], ['p', '?.newDate']]],
success: ['p', { 'class :=': 'ok', _: 'Mission success' }],
quiet: null, // renders nothing
},
default: ['p', { 'class :=': 'muted', _: 'Status unknown.' }], // optional
}
```
With `` tree`…` ``, use **`choose(on, cases, fallback?)`** (after lit's `choose` directive). It produces the same object, as a whole map value or as content, where it stands for one child:
```JavaScript
import { tree, each, choose } from 'assign-gingerly/DX/tree.js';
map: {
mission: choose('?.phase', {
hold: tree`
Hold
${'?.reason'}
`,
success: tree`
Mission success
`,
}, tree`
Status unknown.
`),
// Per item: each()'s single child is the item, and here the item is a switch
timeline: tree`
${choose('?.kind', {
milestone: tree`
${'?.label'}
`,
anomaly: tree`
${'?.label'}${'?.detail'}
`,
})}
`,
}
```
- **Matching:** the discriminant (`switch`) is resolved against the switch position's value, like any RHS value (`'?.phase'`, `'$parent?.mode'`, pipes too). It's matched with `String(value)` against the `cases` keys, so numbers and booleans work (`true: […]`, `2: […]`). Nullish, or no matching key, means `default`.
- **Nothing to render:** a `null` case, or no match and no `default`. A placeholder comment (``) keeps the switch's slot, so its siblings and its parent aren't rebuilt.
- **The chosen case** renders against the **same value** as the switch: a switch is a choice, not a new scope.
- **Rebinding:**
- **the same case as last time** is rebound in place, like any element;
- **another case** replaces the previous one **in its slot**. The previous case is discarded, managers and their state included. (Keeping cases alive but hidden may become an option later.)
- **Why not `` ${cond ? tree`…` : tree`…`} ``?** A `${…}` is evaluated once, when the template is built, so it bakes one branch into the spec. And `' ?='` ternaries switch values (text, classes), not elements.
- **`compile`:** each case is compiled to its own template (ids `/case:` and `/default`). A switch among static children has no element in its parent's template; its case is cloned and placed when the parent is bound.
See the *Mission status* card and the *Flight timeline* in [demos/tree-demo.html](../demos/tree-demo.html).
## Nested Trees
Put a **nested tree object**, a tree config without `do`, in the children slot. The element's value becomes the nested tree's VM:
```JavaScript
person: ['section', { className: 'card' }, {
map: {
name: ['h2', '?.'], // vm.person.name
tags: ['ul', {}, { each: ['li', '?.'] }], // vm.person.tags
},
}],
```
- **Same as a nested ` =>`**: it renders what `['section', { className: 'card', '?. =>': { do: 'builtIns.tree', map } }]` would, but without the handler dispatch.
- **Told apart from a repeat** by having `map` and no `each`. `markerName`, `keepNullish` and `aka` work as in a top-level config. `aka` is merged over the enclosing tree's.
- **Inside the nested tree:** `?.` is each key's value, as at the top level. `$parent` is the element's value (`vm.person`), and the enclosing VMs are further up the stack.
- **Keys missing from the element's value** are skipped, as in any tree. That differs from static children, which always render.
- **Rebinding:** the nested tree keeps its own state, between its own markers inside the element, so later calls rebind it in place. Switching a key between static children, a repeat and a nested tree rebuilds the element.
- **Synchronous**, always. A nested ` =>` builtIns.tree is synchronous too, once `tree.js` is loaded, which it always is while a tree is running. (` =>` handlers in general run synchronously when they can: see [sync ops](./sync-ops.md).)
- **`compile`:** an enclosing `compile` tree clones the element and renders the nested tree at bind time. The nested tree's own `compile` only takes effect once the compiler is loaded, for example by an enclosing `compile` tree.
## Itemscope Managers as View Models
An object header `{ tag, ish }` attaches an [itemscope manager](../README.md#itemscope-managers-chrome-146) to the element. The manager then acts as the **view model for that element's subtree**: the element's own pattern and its children's patterns resolve against the **manager instance** (`el.ish`) instead of the raw value. Computed values are just getters, and the structure stays in the config, so managers don't render DOM.
```JavaScript
class RocketManager {
static is = 'rocket-manager';
#metric = true;
#rebind;
constructor(element, initVals, { rebind }) {
Object.assign(this, initVals); // the raw vm.rocket
this.#rebind = rebind;
}
get metric() { return this.#metric; }
set metric(value) { this.#metric = value; this.#rebind(); }
get heightLabel() { return this.#metric ? `${this.height.meters} m` : `${this.height.feet} ft`; }
}
customElements.itemscopeRegistry.define(RocketManager.is, RocketManager); // shorthand for { manager: RocketManager }
map: {
rocket: [{ tag: 'section', ish: 'rocket-manager' }, { className: 'card' }, [
['span', { textContent: '?.heightLabel' }], // a getter on the manager
]],
}
```
- **`ish`** is a **registered name** (keeps the config JSON) or a **class**.
- **A class** is attached directly and **registered nowhere**, so rendering has no side effect on any registry.
- **A name** is looked up in the call's [`registry.itemscopeRegistry`](./code-interjection.md#the-registry-option) first, then the tree's target's registry, then the global one. An unregistered name throws. Defining names only in `registry.itemscopeRegistry` keeps them local to the call: `assignFrom(host, pattern, { from: vm, registry: { itemscopeRegistry } })`.
- **No `itemscope` attribute** is set. The manager is attached directly through the element's `ish` property.
- **The manager is structure.** It's attached once, when the element is created. A different `ish` (like a different tag) rebuilds the element.
- **Every call** assigns the key's value to `el.ish`, with the usual `ish` semantics: the first value constructs the manager, and later values merge into it (or go to a static `onAssigned`). The subtree is then re-applied against the instance.
- **`$parent` / `$root`** inside the subtree still refer to the enclosing VM(s), not to the manager.
### Local managers: `registry.itemscopeRegistry`
A module can keep its managers to itself and still write its tree as plain JSON. It defines them in its own `ItemscopeRegistry` and passes that with each call, so nothing is added to `customElements.itemscopeRegistry`, and other code can't see the names or collide with them:
```JavaScript
import { assignFrom, ItemscopeRegistry } from 'assign-gingerly';
// launch-card.js: managers private to this module
class CrewMemberView {
constructor(element, initVals) { Object.assign(this, initVals); }
get badge() { return `${this.name} (${this.role})`; }
}
class StageView {
constructor(element, initVals) { Object.assign(this, initVals); }
get summary() { return `Stage ${this.number}: ${this.engines} engines`; }
}
// Each entry is a manager class (or a full config); the same as calling define() for each
const itemscopeRegistry = new ItemscopeRegistry({
'crew-member-view': CrewMemberView,
'stage-view': StageView,
});
// Pure JSON: managers are referenced by name
const pattern = {
'?. =>': {
do: 'builtIns.tree',
map: {
crew: ['ul', {}, { each: [{ tag: 'li', ish: 'crew-member-view' }, '?.badge'], key: '?.name' }],
stages: ['ol', {}, { each: [{ tag: 'li', ish: 'stage-view' }, '?.summary'] }],
},
},
};
export function renderLaunchCard(host, launch) {
assignFrom(host, pattern, { from: launch, registry: { itemscopeRegistry } });
}
```
- **Names resolve only in the call's registry** when it gives one. There's no per-name fallback to the target's or the global registry, so a typo or a missing `define` fails here, instead of silently picking up a same-named manager from elsewhere.
- **It flows down:** repeats (as above), nested trees and other handlers inside the tree all get the same `registry` option.
- **Classes need none of this:** `{ tag: 'li', ish: CrewMemberView }` attaches the class directly, also with no global footprint. Use a local registry when you want the config to stay JSON (stored, sent, or generated) and still keep the names private.
- **A scoped registry works the same way:** `registry: shadowRoot.customElementRegistry` uses that registry's `itemscopeRegistry` (and its `enhancementRegistry`). See [the `registry` option](./code-interjection.md#the-registry-option).
### `ctx.rebind()`
The manager receives a context with **`rebind()`**: as the constructor's third argument, and merged into `onAssigned`'s `ctx`. Calling it re-applies the element's own pattern and its children's patterns against the manager, without a tree rerun. Call it after changing the manager's own state, typically from a setter (above: `rocketEl.ish.metric = false`). State kept on the manager survives later tree reruns.
Calls made while Tree itself is binding that element (for example from the constructor) are ignored, since Tree is about to bind it anyway.
### Async `onAssigned`
`onAssigned` may return a promise. Tree binds the subtree **immediately**, with whatever state the synchronous part of `onAssigned` set, and then **calls `rebind()` by itself once the promise settles** (after any values queued behind it). For example, a links bar can show "verifying…" right away and "✓ verified" when the check finishes.
See [demos/tree-demo.html](../demos/tree-demo.html): the rocket card and the links bar each have a manager.
## Events
An element's own pattern can bind its events with an empty ` +=` LHS. The listener's **host is the element's `from`** — inside a manager subtree, the manager instance — so a button can call a manager method directly:
```JavaScript
rocket: [{ tag: 'section', ish: 'rocket-manager' }, {}, [
['span', '?.heightLabel'],
['button', { _: 'Increase by one', ' +=': { on: 'click', call: 'increaseHeightByOne' } }],
]]
```
- No `itemscope` attribute or DOM host lookup is involved; the host comes from the data context.
- **Reruns don't stack listeners:** Tree gives each `' +='` binding without a `get.key` a default dedup key, so re-applying the pattern replaces the listener (and its host is always the current `from` — which matters outside manager subtrees, where the VM value changes between calls).
- The manager method typically updates its own state and calls `ctx.rebind()`.
See [event-binding.md](./event-binding.md) for the full ` +=` event syntax.
## Snippets: static HTML
A **snippet** is a piece of static HTML: an SVG icon, a legal footer, a static table header. The config names it, and the `snippets` option supplies the HTML:
```JavaScript
await assignFromAsync(container, {
'?. =>': {
do: 'builtIns.tree',
map: {
banner: { snippet: 'banner' }, // a whole map value
card: ['article', {}, [
{ snippet: 'logo' }, // a static child
['h3', '?.title'],
]],
},
},
}, {
from: vm,
snippets: {
logo: String.raw``,
banner: String.raw`
Example
Two root elements are fine
`,
},
});
```
With `` tree`…` ``, use **`snippet(name)`**, as content or as a whole map value:
```JavaScript
card: tree`${snippet('logo')}
${'?.title'}
`,
```
**Where it goes:** `{ snippet: 'name' }` goes anywhere an element specification goes: a map value, a static child, a repeat's `each` or `empty`, or a switch case.
**What happens:**
- **Parsed once** per distinct string, by the browser's HTML parser, into a ``, then **cloned** for each instance. Because the browser parses it, **SVG and MathML get their namespaces**. Tree's own `createElement` doesn't do that yet.
- **One record per instance,** holding all its nodes, and **never visited again**, apart from its [holes](#holes-dynamic-parts-inside-a-snippet). Later runs skip its static parts, so other code may change them freely. If the `snippets` option holds another string under that name on a later run, the instance is replaced.
- **Several root elements** are fine. The top level may only hold elements and the whitespace between them: no text, no comments. Wrap text in an element.
- **Checked when parsed,** and a refused snippet **throws**, naming it:
- no `