<fx-repeat>

Description

Repeats template for each node of the referenced nodeset.

For each data node in the referenced nodeset one <fx-repeatitem> element will be created that will contain the evaluated template as content.

Attributes

NameDescription
ididentifier for repeat
refXPath reference pointing to the bound node
sizeCaps how many fx-repeatitem elements are materialized from the (always-complete) bound nodeset. Without virtual, this is a growing prefix cap: a trailing sentinel reveals the next size items via IntersectionObserver as the user scrolls, and once rendered, rows are never un-rendered. With virtual, size instead becomes the size of a sliding window. Omit for full, uncapped rendering (the default).
virtualOpt-in true windowed virtualization, only meaningful together with a finite size. Instead of an ever-growing prefix, the DOM stays bounded at roughly size rows regardless of nodeset size: scrolling down appends rows below and evicts rows from the top; scrolling up prepends rows above and evicts from the bottom. virtual without size is ignored (behaves as fully uncapped).

Progressive rendering and virtualization

size only limits DOM materialization. The logical nodeset - and therefore index(), iterate actions, and explicit fx-bind model semantics (calculate, constraint, required, readonly, relevant) - always covers every item, regardless of how many rows are currently rendered. Programmatic or keyboard navigation to an index beyond the currently rendered window (e.g. via setIndex, an index('id')-driven dependent, or an insert/append past the window) materializes rows on demand: without virtual, it front-fills up through the target row; with virtual, it performs a hard jump - the current window is discarded and a fresh one is rendered starting at the target index, resetting scroll position to the top of that window.

Adding virtual alongside size turns on a sliding window: only about size rows are ever in the DOM, no matter how large the nodeset is. Eviction never estimates row height - it measures the actual height of the rows being removed and corrects the scroll position by that exact amount, so the visible content doesn’t jump. One consequence: the native scrollbar no longer represents absolute position in the full dataset (it behaves like a bounded infinite-scroll list, not “thumb at 45% of 10,000 rows”). Item recycling (reusing existing fx-repeatitem subtrees instead of destroying/recreating them) is a possible later enhancement.

<fx-repeat ref="item" size="50" virtual>
  <template>
    ...
  </template>
</fx-repeat>

Recursive templates

ref is evaluated relative to the current context, so a statically nested fx-repeat (one written by hand inside another’s template) already renders correctly at any fixed depth. For a self-similar tree of unknown depth (file trees, org charts, threaded comments), use fx-repeat-ref instead of writing out a depth-capped chain by hand:

<fx-repeat id="tree" ref="folder|file" recursive="true">
  <template>
    <li>
      <span>{@name}</span>
      <ul>
        <fx-repeat-ref></fx-repeat-ref>
      </ul>
    </li>
  </template>
</fx-repeat>

<fx-repeat-ref> re-applies the enclosing template to the current node’s own matching children, one level deeper each time, terminating naturally once a node has no more matching children. See its own page for the ref override attribute and further detail.

Drag and drop

Set draggable="true" and drop-target="<this repeat's id>" on the template (not on fx-repeat itself) to let items be reordered or moved between repeats by drag and drop - see the Kanban Board and Drag and Drop demos, and fx-droptarget for dropping onto non-repeat targets.

By default, a drop is accepted when the dragged item and the drop target share the same nearest ancestor element carrying an id - a plain string comparison, not real data parentage. This is what lets a Kanban card be dragged between different columns of one repeat; but for two structurally identical repeats at the same depth (notably: every level of a recursive tree built with fx-repeat-ref, since every level shares one template and therefore one literal id), it cannot tell “true siblings under the same real parent” apart from “cousins at the same depth under a different parent.”

Add drop-scope="parent" to the template to switch the check to real parent equality (the dragged item’s and the drop target’s actual bound data node having the same parent), independent of id:

<fx-repeat id="tree" ref="folder|file" recursive="true">
  <template draggable="true" drop-target="tree" drop-scope="parent">
    ...
  </template>
</fx-repeat>

drop-scope="parent" is opt-in; existing repeats that don’t set it keep today’s id-equality behavior unchanged. See the standalone Drop scope demo, which sets it to block drags between two structurally identical sibling groups.

The Recursive tree demo deliberately does the opposite: it does not set drop-scope="parent", since a file-browser-style tree wants a file draggable into any folder, not just reorderable within its own. Because every recursion level is synthesized with no id of its own, the default id-equality check’s “nearest ancestor with an id” walk passes straight through every synthesized level and lands on the one real id on the outermost fx-repeat - so every node in the tree resolves to that same scope, making the whole tree one drop target, which is exactly the file-browser behavior wanted here.

Accessibility

Each fx-repeatitem is exposed as a list item (role="listitem") of a list. It also gets aria-posinset (its position) and aria-setsize (the number of items in the whole nodeset). These are kept up to date on inserts and deletes. With size or virtual only part of the rows are in the DOM, so a screen reader relies on these two attributes to report “3 of 200”.

Ids in templates. The template is stamped out once per row, so an id written in the template exists once per row. This is intended: Fore resolves such ids relative to the row (fx-trigger/action targets, index() and the like), and nothing renumbers them when rows are inserted or deleted. Do not try to make them unique. The one exception is an fx-control with a <label for="..."> whose target is its own widget: the browser would resolve for to the first matching id in the document, so every label would point at the first row’s input. In a repeat row Fore therefore gives that label/widget pair a row-unique id automatically. Ids on Fore elements (fx-control, fx-trigger, …) are never touched.

Naming repeated buttons. A “delete” button is repeated in every row. Give it a name that identifies its row with a template expression, which is evaluated per row:

<fx-trigger>
  <button aria-label="Delete {name}">🗑</button>
  <fx-delete ref="."></fx-delete>
</fx-trigger>

To name a whole row, wrap the template content in your own element, e.g. <div role="group" aria-label="{name}">...</div>.

Adding rows with create-nodes (origin="#<repeat id>")

With create-nodes on the <fx-fore>, an <fx-insert origin="#r-lines" ref="..."> that points at a repeat by id inserts a new row whose data nodes come from that repeat’s own <template>:

<fx-fore create-nodes>
  <fx-repeat id="r-lines" ref="cac:InvoiceLine">
    <template>
      <fx-control ref="cbc:ID"><input type="text"></fx-control>
      <fx-control ref="cac:Item/cbc:Name"><input type="text"></fx-control>
    </template>
  </fx-repeat>
  <fx-trigger>
    <button>add</button>
    <fx-insert ref="cac:InvoiceLine" origin="#r-lines"></fx-insert>
  </fx-trigger>
</fx-fore>
  • The row is built from the template and the repeat’s ref, not from the loaded data: Fore creates a blank row element for ref and lets every bound control of the template create its nodes in it (the same step that completes the rows of loaded data). A new row therefore has the same shape for every document. It also works for a repeat that has no rows (yet, or any more) and when the loaded rows are incomplete.
  • It is built once, on first use, and without text values. Values that the repeat’s own ref depends on stay: for ref="cac:AllowanceCharge[cbc:ChargeIndicator = 'true']" a new row gets ChargeIndicator = true, else the repeat would not select it.
  • The build leaves no trace: no temporary row stays in the repeat, no ModelItems are kept and the instance is not touched.
  • Multi-step refs (ref="cac:TaxTotal/cac:TaxSubtotal"): the row is the last step. If the repeat is empty, fx-insert also creates the missing parent steps (cac:TaxTotal) so that the new row is selected by ref.
  • Give fx-insert the same ref as the repeat (relative to the same context). Inside another repeat or a group that is the repeat’s relative ref; an absolute path from the document root does not select the rows of a nested repeat.
  • Namespace prefixes of the refs are resolved from the xmlns: declarations in the page, as everywhere in Fore.
  • If no blank row can be created for the ref (no context node, instance() refs), Fore falls back to a copy of the last existing row.

Events

NameDescriptionDetails
item-createddispatched when a new repeat entry is created‘path’ - a canonical xpath
‘index’ - the index of the new item
path-mutateddispatched when repeat nodeset changes‘path’ - the mutation path
‘index’ - the index of the changed repeat item.
no-template-errordispatched when there’s no template defined‘id’ - the repeat id

Examples