Module: lib/core/renderer/program/blocks

The runtime half of control flow: conditionals, lists and slots.

What these replace

Control flow used to live in the runtime as a search. ListManager ran root.querySelectorAll('template[data-ax-for]') on every update, read the list expression out of an attribute, unescaped the body markup, rendered it to a string per item and diffed the result into the document. The runtime was doing the compiler's job, once per update, with a string as the only record of what the author had written.

A compiled block arrives already understood. { k: 'for', x: 4, as: 'row', key: 7, b: 2 } says: evaluate expression 4, bind each element to row, identify rows by expression 7, and render block 2 for each. Block 2 is a skeleton parsed once for the life of the page and cloned per row, with its own ops writing to its own nodes.

Anchors, and why each binding owns a range

Every construct here occupies a range of sibling nodes rather than one node, and the range is empty for a false condition or an empty list. So the compiler reserves a text anchor at the position, and the binding inserts after it and remembers exactly what it inserted.

Remembering matters: clearing by emptying the parent would take the siblings that belong to other bindings, which is the bug that makes two adjacent <@if> blocks erase each other.

Reconciliation

The list binding is keyed. Entries are matched by key, reused in place, moved with the cursor as the new order is walked, and torn down when their key disappears. An unkeyed list falls back to the item's position, which is the documented behaviour and is why keys matter for a list that reorders.

Why the instance factory is injected

A block is rendered by a TemplateInstance, and a TemplateInstance creates these bindings for its range ops. Importing the class here would make that mutual, and the bundler refuses a cycle rather than relying on which half happens to initialise first. The owner passes a factory instead, so the dependency runs one way.

Source:

Classes

DeferBinding
ForBinding
IfBinding
RangeBinding
SlotBinding
BlockRange

Methods

(inner) applyLocals(instance, locals) → {boolean}

Writes new local bindings into a mounted instance.

Parameters:
Name Type Description
instance object

The instance to update.

locals object

The new bindings.

Source:
Returns:

True when any bound value changed.

Type
boolean

(inner) fragmentNodes(fragment) → {Array:.<Node:>}

Collects the top-level nodes of a fragment before it is inserted.

Read before insertion because a fragment is emptied by insertBefore, and the binding needs the list to remove exactly these nodes later.

Parameters:
Name Type Description
fragment DocumentFragment

The fragment about to be inserted.

Source:
Returns:

Its top-level nodes, in order.

Type
Array:.<Node:>

(inner) mergeLocals(parent, own) → {object|null}

Merges a parent block's local bindings with a child's.

A loop inside a loop reads both bindings, so locals chain rather than replace. The merge is a copy because a reused entry's values are written in place, and sharing one object between entries would make every row show the last row's values.

Parameters:
Name Type Description
parent object | null

The enclosing block's locals.

own object | null

This block's own bindings.

Source:
Returns:

The merged locals.

Type
object | null

(inner) normaliseList(value) → {Array:.<any:>}

Coerces whatever a list expression returned into an array of elements.

Arrays pass through. A Map or Set iterates. A plain object iterates its own values, which is what <@for value in someObject> has always meant. Anything else -- null, a number, a string that was not meant to be iterated -- renders nothing rather than throwing, because a list that has not loaded yet is the normal state of a list, not an error.

Parameters:
Name Type Description
value any

The evaluated list expression.

Source:
Returns:

The elements to render.

Type
Array:.<any:>