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.
Classes
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. |
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. |
Returns:
Its top-level nodes, in order.
(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. |
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. |
Returns:
The elements to render.