Unify Logo Footer.svg
Unify Applications
Logo
Repeatable

Repeatable

Logo

11 mins READ

Overview

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.

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.

PropertyTypeDefaultDescription
dataarray (bindable)Sample itemsThe array to repeat — one template instance per element. Usually a binding to a data source query result.
primaryKeyfield / Index / expressionidUnique 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. These must use bracket notation — dot notation is not supported:

{{ 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 not supported {{ 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

PropertyTypeDefaultDescription
sortablebooleanoffEnables drag-to-reorder. Appears only after a Primary Key is set. Reordering updates {{ id.order }} but does not mutate the bound data.
dragHandleBlockIdblock picker— (whole row)Restricts dragging to one block inside the template (e.g., a grip icon). Appears when Sortable is on.
autoSelectnone | firstItem | lastItemnoneAutomatically selects the first or last row when nothing is selected. Fires On Select Item like a real click.
collectChildStatesbooleanoffExposes each row's inner block states under {{ id.instances }}. Mutually exclusive with pagination.
addOns (Pagination)add-onAdds page-based or scroll-based loading. Requires wiring your data source's limit/offset to {{ id.pagination.pageSize }} and {{ id.pagination.offset }}.
containerIdblock pickerThe container block whose scrolling drives pagination. Appears when pagination is configured.

Appearance Properties

PropertyTypeDefaultDescription
layoutlist | gridlistStack rows as a list or arrange them in a CSS grid.
directionvertical | horizontalverticalList flow direction. Only appears when Layout is list.
columnsnumber3 when unsetGrid column count. Only appears when Layout is grid. Columns are fixed width — not responsive.
separatortoggle + stylesoffA divider between rows with color and size options. Only when Layout is list.
stylesstyle setgap lgGap between rows, height, padding, overflow, border.
visibilityconditionalways visibleShow 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

Callable via the Control Block Method action or from a script at the block's own name.

MethodParamsDescription
selectNextItemMoves selection to the next row and scrolls it into view. Requires a Primary Key.
selectPreviousItemMoves selection to the previous row. Requires a Primary Key.
scrollTokeyScrolls to the row with the given primary key or index.
resetOrderDiscards user drag-reordering and restores data order.
goToPagepageNumberJumps the pager. Note: loads all pages from 1 through the target.
resetPageOffsetCollapses scroll-loaded pages back to the first and refreshes.
resetInstancesClears all collected per-row inner block states back to template defaults.
clearSelectionClears 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, use bracket notation
{{ 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

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 (fixed)grid (fixed, 2 columns)
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. Only the scrollTo method is wired. Lists use infinite scroll and pull-to-refresh instead.

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.

BlockRelationship
Repeatable GridSame engine, pre-configured for grid layout
Repeatable ListSame engine, pre-configured for list layout
TablePrefer for dense records with columns, sorting, filtering, bulk actions
KanbanPrefer when items are dragged between groups/lanes
Tree ViewFor hierarchical, expandable/collapsible parent-child data

Frequently Asked Questions

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.

Why does {{ repeatable1.context.item.name }} not work?

Row-scoped context bindings in Repeatable only support bracket notation: {{ repeatable1['context']['item']['name'] }}. Dot notation is not supported inside the item template. This is by design to allow dynamic key access. Switching to bracket notation resolves the issue.

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

Repeatable is the full-featured block with a freeform item template, pagination, sorting, filtering, per-row state, and all exposed methods. Repeatable List and Repeatable Grid are simplified variants with a fixed layout. They support fewer configuration options and are best used when you only need a quick list or grid with minimal customization.

How do I access the item the user clicked on?

Listen to the On Item Click event. In the event action, the clicked record is available as the event payload. Store it in a page variable using a Set Variable action, then bind other blocks (e.g., a detail drawer) to that 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 its exposed currentPage state and fires the pagination event. You must use those values in your data source query (as offset/limit parameters) and re-fetch. If your data source always returns all records regardless of page, pagination controls will appear but nothing will visually change.