Skip to content

Lists and iteration

Studio writes this format for you. Converting an element with Repeat… and binding its data source ( Repeaters ) writes the Array pseudo-elements on this page.

A dynamic list is an Array pseudo-element : an object with $prototype: "Array" sitting inside a children array. It names the data ( items ) and a template ( map ) rendered once per item. The list re-renders automatically whenever the data changes.

{
  "tagName": "ul",
  "children": [
    {
      "$prototype": "Array",
      "items": { "$ref": "#/state/todoList" },
      "map": { "tagName": "li", "textContent": { "$ref": "$map/item" } }
    }
  ]
}

The iteration context

Inside the map template, the $map/ reference scheme reads the current iteration:

Reference Resolves to
{ "$ref": "$map/item" } The current array item
{ "$ref": "$map/index" } The current zero-based integer index

Deeper paths reach into item fields, as in "$map/item/title" , and template strings inside the template can read the same context as or :

{
  "$prototype": "Array",
  "items": { "$ref": "#/state/posts" },
  "map": {
    "tagName": "article",
    "children": [{ "tagName": "h2", "textContent": { "$ref": "$map/item/title" } }]
  }
}

The map template is an ordinary element def, so attributes , id , className , style , and event handlers all work there and can read the iteration context, which is how a list row gets a per-item link or a selected state:

{
  "$prototype": "Array",
  "items": { "$ref": "#/state/posts" },
  "map": {
    "tagName": "a",
    "id": ,
    "attributes": {
      "href": ,
      "class": 
    },
    "textContent": 
  }
}

Mixing with sibling elements

The Array object is a member of children , so it can sit among ordinary siblings, such as a static header row followed by a dynamic list. It renders wrapper-less : mapped items become direct children of the parent element, with no container in between.

{
  "tagName": "ul",
  "children": [
    { "tagName": "li", "textContent": "Header" },
    {
      "$prototype": "Array",
      "items": { "$ref": "#/state/todoList" },
      "map": { "tagName": "li", "textContent": { "$ref": "$map/item" } }
    }
  ]
}

Reading the row from a handler

One handler serves every row. Which row invoked it is on state as state.$map , with item and index :

{
  "state": {
    "toggle": {
      "$prototype": "Function",
      "body": "state.items[state.$map.index].done = !state.items[state.$map.index].done"
    }
  },
  "children": {
    "$prototype": "Array",
    "items": { "$ref": "#/state/items" },
    "map": {
      "tagName": "li",
      "children": [{ "tagName": "input", "onclick": { "$ref": "#/state/toggle" } }]
    }
  }
}

This works on the map body and on any descendant of it. A nested list shadows the outer context for handlers inside it.

Passing a row to a component

$props on the map body hands each item's data to a component. Template values are bindings, so they read the current row:

{
  "$prototype": "Array",
  "items": { "$ref": "#/state/posts" },
  "map": {
    "tagName": "post-card",
    "$props": { "title": , "index": { "$ref": "$map/index" } }
  }
}

Setting a property on each row

Properties that live on the DOM element rather than in markup ( value , checked , selected , disabled ) go directly on the map body, the same as anywhere else. They may interpolate $map :

{
  "$prototype": "Array",
  "items": { "$ref": "#/state/rows" },
  "map": {
    "tagName": "option",
    "value": ,
    "textContent": 
  }
}

Each <option> gets its row's id as its value, so a change handler reads the key rather than the label shown on screen. Put it in attributes instead and you get an HTML attribute, which for value sets only the default , and the two diverge as soon as the user interacts.

Filtering and sorting

filter and sort reference functions declared in state . The filter function receives each item and returns true to keep it; the sort function receives two items and returns a number, like a standard comparator:

{
  "$prototype": "Array",
  "items": { "$ref": "#/state/allItems" },
  "filter": { "$ref": "#/state/isVisible" },
  "sort": { "$ref": "#/state/sortByDate" },
  "map": { "tagName": "list-item", "item": { "$ref": "$map/item" } }
}

Filtering and sorting never mutate the source array; they shape what renders.

How it works

The runtime places an invisible anchor where the Array object sits, then renders the mapped items inline ahead of it. The whole render runs inside a reactive effect: when items (or a filter or sort dependency) changes, the previous generation of item nodes and their bindings is disposed and the list re-renders in place. Each item's template renders in a child scope carrying $map , and the surrounding document's state remains fully visible inside the template.

Rules

  • The Array object must have $prototype: "Array" , an items source, and a map template.

  • items must resolve to an array: a state entry, a data prototype such as a content collection, or a literal array.

  • The list renders wrapper-less; give structure a container by making the parent the container element ( ul , tbody , a grid div ).

  • $map/ references are valid only inside the map template.

  • filter and sort must be $ref s to functions in state .

  • The legacy form where children is itself the Array object is still accepted; Studio normalizes it to a single array member on load.