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.
| Name | Description |
|---|---|
| id | identifier for repeat |
| ref | XPath reference pointing to the bound node |
| size | Caps 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). |
| virtual | Opt-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). |
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>
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.
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.
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>.
| Name | Description | Details |
|---|---|---|
| item-created | dispatched when a new repeat entry is created | ‘path’ - a canonical xpath ‘index’ - the index of the new item |
| path-mutated | dispatched when repeat nodeset changes | ‘path’ - the mutation path ‘index’ - the index of the changed repeat item. |
| no-template-error | dispatched when there’s no template defined | ‘id’ - the repeat id |