Unify Applications
Logo
Repeatable

Repeatable

Logo

11 mins READ

The Repeatable block renders a single child template for every element of a bound data array. Design the row once, and the block stamps it out for every item with that item's data in scope. It supports vertical and horizontal list layouts, CSS grid layouts, optional drag-to-reorder, page-based and scroll-based pagination, and a per-row state collection feature that lets you read every row's inner block values from outside the list.

repeatable.png
repeatable.png

Tip: Use Repeatable when your rows need a free-form layout you design — contact cards, product tiles, feeds, checklists. Prefer the Table block when users need columns, sorting, filtering, or bulk actions. Prefer Kanban when items must be dragged between groups.

Repeatable List and Repeatable Grid are sibling blocks that use the same rendering engine but ship pre-configured as a list or a two-column grid, respectively. They intentionally omit callable methods, pagination wiring, and the per-row state collection that the base Repeatable offers.

Data Binding

Two properties power everything: the array the block iterates, and the unique key for each row.

Property

Type

Default

Description

Data Source (data)

array (bindable)

Sample items

The array to repeat — one template instance per element. Usually a binding to a data source query result.

Primary Key (primaryKey)

field / Index / expression

id

Unique identity of each row. Required for selection targeting, drag-to-reorder, and scroll-to. Use Index only when order is stable.

Warning: If your data source returns {{ query1.data.items }}, bind to that path — not {{ query1.data }}. A non-array value renders no rows without any error.

Item Template

The Repeatable renders one template block — a Card by default — once per row. Select it on the canvas or layers panel and edit it like any block. Everything added to the template repeats for every row.

Row-Scoped Bindings

Inside the template, a special set of bindings expose the current row's data. Either dot or bracket notation works — they are the same expression. The builder inserts the bracket form:

{{ repeatable1['context']['item']['name'] }} ✅ correct {{ repeatable1['context']['index'] }} ✅ zero-based row position {{ repeatable1['context']['primaryKey'] }} ✅ the row's resolved key {{ repeatable1.context.item.name }} ✅ dot notation works too {{ repeatable1['currentItem']['name'] }} ❌ no such shorthand

Note: While editing bindings in the builder, the expression picker always previews with the first row's data. At runtime each row correctly receives its own data.

Per-Row Isolation

Every block placed in the template gets its own copy per row automatically:

  • A sibling reference like {{ price_input.value }} resolves to this row's copy of the input.

  • A method call on a sibling (e.g., Form submit) targets this row's copy only.

  • An event handler on an inner block fires for that row only — perfect for per-row automations.

Content Properties

Property

Type

Default

Description

Sortable (sortable)

boolean

off

Enables drag-to-reorder. Appears only after a Primary Key is set. Reordering updates {{ id.order }} but does not mutate the bound data.

Drag Handle Block (dragHandleBlockId)

block picker

— (whole row)

Restricts dragging to one block inside the template (e.g., a grip icon). Appears when Sortable is on.

Auto Select (autoSelect)

None | First Item | Last Item

None

Automatically selects the first or last row when nothing is selected. Fires On Select Item like a real click.

Collect Child States (collectChildStates)

boolean

off

Exposes each row's inner block states under {{ id.instances }}. Mutually exclusive with pagination.

Add-ons → Pagination (addOns)

add-on

—

Adds page-based or scroll-based loading. Requires wiring your data source's limit/offset to {{ id.pagination.pageSize }} and {{ id.pagination.offset }}.

Scrollable Container (containerId)

block picker

—

The container block whose scrolling drives pagination. Appears when pagination is configured.

Scrollable (mobile apps only)

toggle

on

Lets the list scroll on the phone. On Pull Down only fires on a scrollable list.

Bounce (mobile apps only)

toggle

on

Lets the list bounce at either end. Pull-to-refresh needs a scrollable list with Bounce on.

Header (mobile apps only)

slot

—

A slot for content shown at the top of the list.

Appearance Properties

Property

Type

Default

Description

Layout (layout)

List | Grid

List

Stack rows as a list or arrange them in a CSS grid.

Direction (direction)

Vertical | Horizontal

Vertical

List flow direction. Only appears when Layout is List.

Columns (columns)

number

3 when unset

Grid column count. Only appears when Layout is Grid. Columns are fixed width — not responsive.

Separator → Show (separator)

toggle + styles

off

A divider between rows with color and size options. Only when Layout is List.

Styles (styles)

style set

gap lg

Gap between rows, height, padding, overflow, border.

Visibility (visibility)

condition

always visible

Show or hide the whole block based on a condition.

Pagination

Pagination is an add-on that loads rows in chunks. It comes in two modes:

Page-Based vs Scroll-Based Pagination

Page-Based: A numbered pager bar renders below the rows. Bind your data source's limit/offset inputs to {{ id.pagination.pageSize }} and {{ id.pagination.offset }}. Bind Total Records to show the page bar (required).

Scroll-Based (Infinite): More rows load as the user nears the list bottom. Same wiring required. Also unlocks the On Pull Down event for pull-to-refresh.

Warning: Binding a static 200-element array with page size 30 renders all 200 rows. Pagination only works when the data source itself consumes the block's page size and offset and returns one page at a time.

Pagination State Keys

State KeyTypeDescription
{{ id.pagination.pageSize }}numberRows per page — bind your data source's limit input to this
{{ id.pagination.offset }}numberRows to skip — bind your data source's offset input to this
{{ id.pagination.currentPage }}numberZero-based current page
{{ id.pagination.total }}numberTotal record count (from your data source)

Events

EventTriggerPayload
On Select ItemA row is clicked, or auto-select picks oneselectedItem, selectedItemKey, selectedItemIndex
On Order ChangeUser drops a dragged row in a new positionNew order in {{ id.order }}
On Pull DownUser pulls the list down from the top— (scroll-based pagination or mobile)

Methods

Call these with the Trigger Component Method action: pick the Repeatable as the block and choose the method.

MethodParamsDescription
selectNextItem—Moves selection to the next row and scrolls it into view. Requires a Primary Key.
selectPreviousItem—Moves selection to the previous row. Requires a Primary Key.
scrollTokeyScrolls to the row with the given primary key or index.
resetOrder—Discards user drag-reordering and restores data order.
goToPagepageNumberJumps the pager. Note: loads all pages from 1 through the target.
resetPageOffset—Collapses scroll-loaded pages back to the first and refreshes.
resetInstances—Clears all collected per-row inner block states back to template defaults.
clearSelection—Clears the selected item. With Auto Select active, a new selection is immediately made.

Exposed State

State KeyTypeDescription
{{ id.selectedItem }}objectThe currently selected row's data
{{ id.selectedItemKey }}stringThe selected row's primary key
{{ id.selectedItemIndex }}numberThe selected row's position
{{ id.order }}arrayRow keys in their current (possibly user-dragged) order
{{ id.currentData }}arraySnapshot of the bound data — populated when data comes from a data source
{{ id.instances }}arrayPer-row inner block states — only when Collect Child States is on
{{ id.context.item }}objectCurrent row data — inside template only
{{ id.context.index }}numberZero-based row position — inside template only
{{ id.context.primaryKey }}stringRow's primary key — inside template only

The Shape of instances

When Collect Child States is on, each element of {{ id.instances }} holds currentIndex, currentPrimaryKey, and one entry per direct child block. Three special lookups are also available:

  • instances.byKey(key) — returns a row's full aggregate by its primary key

  • instances.indexOfKey(key) — returns a row's position by primary key

  • instances.keys — returns every row's primary key as a plain array

{{ repeatable1.instances.keys.map(key => repeatable1.instances.byKey(key).checkbox1.value) }} → [true, false, true] // one entry per row's checkbox1

Repeatable List & Repeatable Grid

Warning: Repeatable List and Repeatable Grid show the Pagination add-on and the Collect Child States toggle in the properties panel, but neither works on these blocks. Paging can't be driven, and instances isn't filled. If you need either feature, use the base Repeatable block.

These are pre-configured siblings that use the same engine as Repeatable but with different defaults and reduced capabilities:

FeatureRepeatableRepeatable ListRepeatable Grid
Default layoutlist (configurable)list (default, switchable)grid, 2 columns (default, switchable)
Callable methodsYes (full set)NoneNone
Working paginationYesNo (UI shows but inert)No (UI shows but inert)
Collect Child States / instancesYesNo (UI shows but inert)No (UI shows but inert)
context.index / context.primaryKeyYesNo (context.item only)No (context.item only)
Starter cardSimple text cardRich gradient card (web) / user row (mobile)Gradient card grid (web) / product card grid (mobile)

Note: Repeatable List and Grid are excellent for quick mockups and simple displays. For any flow that needs pagination, per-row state reading, or imperative control (next/previous selection, scroll-to), use the base Repeatable block.

Nesting Repeatables

A Repeatable's template can contain another Repeatable — ideal for 2D data like orders with line items:

// Inner Data Source binding: {{ outerRepeatable['context']['item']['lineItems'] }} // Reading inner row state from outside both: {{ outerRepeatable.instances[0].innerRepeatable.instances[1].skuInput.value }}

Behavior & Gotchas

⚠ Sortable toggle is hidden until a Primary Key is set

The Sortable toggle does not appear in the panel until a Primary Key is chosen. Set a Primary Key first, then the toggle becomes available.

⚠ Enabling pagination silently disables Collect Child States

Saving with a pagination add-on forces Collect Child States off. Paginated lists recycle off-screen rows, so per-row state collection would be partial. Choose one or the other per Repeatable.

⚠ Selection clears when the row disappears from data

When the selected row's key no longer exists (a refetch removed it), the selection resets automatically. With Auto Select active, a new item is immediately selected and On Select Item fires.

⚠ New rows land at the bottom of a user-sorted list

After a drag-reorder, incoming rows are appended after the remembered order, not at their natural data position. Call resetOrder to restore data order.

⚠ Per-row state persists across reuse

Collected instances state persists until cleared. A Drawer or modal that reuses the same Repeatable shows previous open's row state (e.g., ticked checkboxes). Call resetInstances on the Drawer's open event.

⚠ Mobile limitations

In mobile apps: drag-to-reorder and the numbered page bar are not available. Lists use infinite scroll and pull-to-refresh instead. The methods selectNextItem, selectPreviousItem and resetOrder do not work on mobile (their error branch runs). The methods scrollTo (scrollable lists), goToPage, clearSelection, resetPageOffset and resetInstances do work.

Common Patterns

Card List with Selection Drawer

  1. Bind data and set primary key: Set Data Source to your query result and Primary Key to the record's id field.

  2. Design the template card: Open the template card and bind its blocks using row-scoped pills: {{ repeatable1['context']['item']['name'] }}.

  3. Wire On Select Item: On On Select Item, open a Drawer bound to {{ repeatable1.selectedItem }}.

Paginated Product Grid

  1. Set layout to grid: Set Layout to grid and pick a column count (e.g., 3).

  2. Add pagination add-on: Enable the Pagination add-on, set type to Page based, and bind the data source's limit/offset inputs to {{ id.pagination.pageSize }} and {{ id.pagination.offset }}.

  3. Bind Total Records: Bind Total Records to the data source's count output. The page bar appears once this is set.

Sortable Checklist

  1. Enable sorting: Set a Primary Key, then toggle Sortable on. Optionally pick a Drag Handle Block inside the template.

  2. Persist order: On On Order Change, persist {{ repeatable1.order }} to your backend.

  3. Add a reset button: Offer a "Reset Order" button that calls the resetOrder method.

Block

Relationship

Repeatable Grid

Same engine, pre-configured for grid layout

Repeatable List

Same engine, pre-configured for list layout

Table

Prefer for dense records with columns, sorting, filtering, bulk actions

Kanban

Prefer when items are dragged between groups/lanes

Tree View

For hierarchical, expandable/collapsible parent-child data

FAQs

Can I nest one Repeatable inside another?

Yes. Drop a Repeatable block inside the item template of an outer Repeatable. The inner block gets its own context key. Use bracket notation to access the inner row: {{ innerRepeatable['context']['item']['field'] }}. Keep nesting shallow — more than two levels deep can make bindings difficult to reason about.

Can I use dot notation for row-scoped bindings?

Yes. {{ repeatable1.context.item.name }} and {{ repeatable1['context']['item']['name'] }} are the same expression. Use whichever you find clearer; the builder inserts the bracket form. If a row binding shows nothing, check the field name and that the bound data is an array, rather than switching notation.

What is the difference between Repeatable, Repeatable List, and Repeatable Grid?

Repeatable is the full-featured block with a freeform item template, pagination, drag-to-reorder, per-row state, and all exposed methods. Repeatable List and Repeatable Grid are simplified variants that start as a list or a 2-column grid (you can still switch Layout, Direction and Columns). They have no callable methods and are best used when you only need a quick list or grid.

How do I access the item the user clicked on?

Use the On Select Item event, or bind other blocks (e.g., a detail drawer) directly to {{ repeatable1.selectedItem }}. Selecting a row writes it to the block's selectedItem state, so you don't need to copy it into a page variable.

My pagination controls appear but clicking them does nothing. Why?

Pagination in the Repeatable block is a contract with the data source — the block does not slice the array itself. When the user changes page, the block updates {{ id.pagination.offset }} and {{ id.pagination.pageSize }}. Your data source must use those values as its offset/limit inputs and return one page at a time. If your data source always returns all records regardless of page, the pager appears but nothing visibly changes.