# 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 Demo Mission 7 patch
``` 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'}

${'?.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`
#${'?.id'}
`, // 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 `