Skip to main content
Version: Upcoming

Repeating Units

Repeating units are repeatable tab contents. Once a tab is configured as repeatable, users or custom code can add and remove copies of that tab content within the configured limits.

For new form definitions, use the object notation on data-hf-repeating. It supports the full feature set, including data source synchronization and lifecycle callbacks.

Note

Setting the id attribute on the tab is mandatory for repeating units.

Object Notation

The object notation is the recommended way to configure repeating units. It keeps all repeating-unit settings in one JSON object and is the only notation that supports dataSource and eventCallbacks.

HTML Definition

Repeating Unit - object notation
<li
id="tabRepeatingUnit"
data-hf-title="Contact"
data-hf-repeating='{
"min": 1,
"max": 10,
"label": "Contact",
"labelAdd": "Add contact",
"labelRemove": "Remove contact",
"anchors": true,
"toolbar": true,
"order": "desc",
"showOnlyLastToolbar": false,
"alwaysShowHeaderFooter": true,
"header": ["contactHeaderBlock1", "contactHeaderBlock2"],
"footer": {
"blocks": [
"../DemoForm.templ/DemoForm.templ.html#contactFooterBlock1",
"../DemoForm.templ/DemoForm.templ.html#contactFooterBlock2"
],
"idPrefix": "contact_",
"phPrefix": "contact_",
"phHeadline": "Footer headline"
},
"dataSource": {
"mapping": {
"name": {
"sourceIds": ["contact_firstname", "contact_lastname"],
"callback": "HFFormdefinition.CustomCode.buildContactName"
},
"email": "contact_email",
"status": {
"fieldId": "contact_type",
"shadowField": "HFValue"
}
},
"target": ["contact_selector", "contact_data_control"],
"onChanged": "HFFormdefinition.CustomCode.onContactUnitsChanged"
},
"eventCallbacks": {
"beforeadd": "HFFormdefinition.CustomCode.beforeAddContact",
"added": "HFFormdefinition.CustomCode.afterAddContact",
"beforeremove": "HFFormdefinition.CustomCode.beforeRemoveContact",
"removed": "HFFormdefinition.CustomCode.afterRemoveContact"
}
}'
>
<a href="#contact_block_1"></a>
<a href="#contact_block_2"></a>
</li>

Object Notation Properties

These properties live inside the JSON object assigned to data-hf-repeating.

id - required

Set the id of the repeatable tab.
How to use:
id="contact"
Type: string

alwaysShowHeaderFooter

Keeps header and footer visible even when there are currently no repeating units.
How to use:
"alwaysShowHeaderFooter": true
Type: boolean
Default: false

anchors

Enables anchor navigation for the repeating units in the tab menu.
How to use:
"anchors": true
Type: boolean
Default: true

dataSource

Builds and maintains a data source array from the values of the current repeating units.
How to use:
"dataSource": { "mapping": { "email": "contact_email" } }
Type: object

eventCallbacks

Registers lifecycle callbacks for add and remove actions on the repeating units.
How to use:
"eventCallbacks": { "added": "HFFormdefinition.CustomCode.afterAddContact" }
Type: object

One or two footer blocks rendered once below the repeating units. You can use direct block ids or an include object with blocks, idPrefix, phPrefix, and custom placeholders.
How to use:
"footer": { "blocks": ["../DemoForm.templ/DemoForm.templ.html#contactFooterBlock1"] }
Type: string[] | include-object

One or two block ids rendered once above the repeating units.
How to use:
"header": ["contactHeaderBlock1", "contactHeaderBlock2"]
Type: string[]

label

General label used for the repeating unit toolbar and related UI.
How to use:
"label": "Contact"
Type: string
Default: "Repeating unit"

labelAdd

Label shown for the add action.
How to use:
"labelAdd": "Add contact"
Type: string
Default: "Add new unit"

labelRemove

Label shown for the remove action.
How to use:
"labelRemove": "Remove contact"
Type: string
Default: "Remove last unit"

max

Maximum number of repeating units that can exist for the tab.
How to use:
"max": 10
Type: number
Default: 99

min

Minimum number of repeating units rendered for the tab.
How to use:
"min": 1
Type: number
Default: 1

order

Controls whether units are rendered in ascending or descending order.
How to use:
"order": "desc"
Type: "asc" | "desc"
Default: "asc"

showOnlyLastToolbar

Shows only the last toolbar instead of one toolbar per visible unit.
How to use:
"showOnlyLastToolbar": true
Type: boolean
Default: false

toolbar

Controls whether the repeating-unit toolbar is rendered.
How to use:
"toolbar": false
Type: boolean
Default: true

Data Source Properties

Use dataSource to build a live array from repeating-unit field values. The array is initialized when the page is rendered, updated whenever mapped fields change, and updated again after units are added or removed.

mapping - required

Required mapping definition. Each key becomes a property in the generated data source entry. Values can reference a field id, a shadow field, or a callback configuration.
How to use:
"mapping": { "email": "contact_email", "status": { "fieldId": "contact_type", "shadowField": "HFValue" } }
Type: Record

onChanged

Optional callback that is executed after the generated data source changes.
How to use:
"onChanged": "HFFormdefinition.CustomCode.onContactUnitsChanged"
Type: string

target

Optional target control id or list of ids that should receive the generated data source. Use a DataControl or a control with setDataSource().
How to use:
"target": ["contact_selector", "contact_data_control"]
Type: string | string[]

Mapping Entry Formats

Use one of these formats inside dataSource.mapping:

  1. A field id string to read the value from the current repeating unit, for example "email": "contact_email".
  2. An object with fieldId and shadowField to read a shadow field, for example "status": { "fieldId": "contact_type", "shadowField": "HFValue" }.
  3. An object with sourceIds and callback to compute a value, for example "name": { "sourceIds": ["contact_firstname", "contact_lastname"], "callback": "HFFormdefinition.CustomCode.buildContactName" }.

The mapping callback receives the configured sourceIds array and the current repeating-unit postfix. The onChanged callback receives a cloned copy of the generated data source array plus the tab id.

Custom Code for dataSource callbacks
export function buildContactName(sourceIds, postfix) {
const [firstNameId, lastNameId] = sourceIds;
const firstName = HybridForms.API.Form.fieldGet(`${firstNameId}${postfix}`)?.value || '';
const lastName = HybridForms.API.Form.fieldGet(`${lastNameId}${postfix}`)?.value || '';
return `${firstName} ${lastName}`.trim();
}

export function onContactUnitsChanged(dataSource, tabId) {
console.log('Repeating unit data source changed', tabId, dataSource);
}

If you assign target, the generated array is pushed into every listed target control. Use either a DataControl or a control that exposes setDataSource(). For selectable controls, make sure every mapped entry contains the keys used by that control as its text field and value field.

Event Callback Properties

Use eventCallbacks to react to add and remove actions. Each callback receives the zero-based repeating-unit index and the tab id.

added

Callback executed after a repeating unit was added.
How to use:
"added": "HFFormdefinition.CustomCode.afterAddContact"
Type: string

beforeadd

Callback executed before a repeating unit is added. The process waits for this callback.
How to use:
"beforeadd": "HFFormdefinition.CustomCode.beforeAddContact"
Type: string

beforeremove

Callback executed before a repeating unit is removed. The process waits for this callback.
How to use:
"beforeremove": "HFFormdefinition.CustomCode.beforeRemoveContact"
Type: string

removed

Callback executed after a repeating unit was removed.
How to use:
"removed": "HFFormdefinition.CustomCode.afterRemoveContact"
Type: string

beforeadd and beforeremove run before the unit is changed and can return a Promise. Processing waits for those callbacks to finish. added and removed run after the unit was changed and are not awaited.

Custom Code for lifecycle callbacks
export function beforeAddContact(index, tabId) {
console.log('Before add', tabId, index);
return Promise.resolve();
}

export function afterAddContact(index, tabId) {
console.log('Added', tabId, index);
}

export function beforeRemoveContact(index, tabId) {
console.log('Before remove', tabId, index);
return Promise.resolve();
}

export function afterRemoveContact(index, tabId) {
console.log('Removed', tabId, index);
}
Very Important

If you use alwaysShowHeaderFooter: false to hide the header and footer, do not place controls with required: true inside those header or footer blocks.

Legacy Attribute Notation

The legacy attributes are still supported for existing forms, but they are deprecated. New form definitions should use the object notation instead.

Important

The legacy attribute notation does not support dataSource or eventCallbacks.

Legacy HTML Definition

Repeating Unit - legacy attribute notation
<li
id="tabRepeatingUnit"
data-hf-title="Contact"
data-hf-expandable="true"
data-hf-repeatable="true"
data-hf-repeatablemin="0"
data-hf-repeatablemax="10"
data-hf-repeatablelabel="Contact"
data-hf-repeatablelabeladd="Add"
data-hf-repeatablelabelremove="Remove"
data-hf-repeatableheader="contactHeaderBlock1,contactHeaderBlock2"
data-hf-repeatablefooter="contactFooterBlock1,contactFooterBlock2"
data-hf-repeatablealwaysshowheaderfooter="true"
data-hf-repeatableanchors="true"
data-hf-repeatabletoolbar="true"
data-hf-repeatableorder="desc"
data-hf-repeatableshowonlylasttoolbar="false"
></li>

Legacy Attribute Properties

These attributes map to the older repeating-unit syntax. Keep them only for backwards compatibility.

id - required

Set the id of the repeatable tab.
How to use:
id="contact"
Type: string

data-hf-repeatable

Enables repeating units for the tab.
How to use:
data-hf-repeatable="true"
Type: boolean

data-hf-repeatablealwaysshowheaderfooter

Keeps header and footer visible even when there are currently no repeating units.
How to use:
data-hf-repeatablealwaysshowheaderfooter="true"
Type: boolean

data-hf-repeatableanchors

Enables anchor navigation for the repeating units in the tab menu.
How to use:
data-hf-repeatableanchors="true"
Type: boolean

data-hf-repeatablefooter

Comma-separated list of one or two footer block ids.
How to use:
data-hf-repeatablefooter="contactFooterBlock1,contactFooterBlock2"
Type: string

data-hf-repeatableheader

Comma-separated list of one or two header block ids.
How to use:
data-hf-repeatableheader="contactHeaderBlock1,contactHeaderBlock2"
Type: string

data-hf-repeatablelabel

General label used for the repeating unit toolbar and related UI.
How to use:
data-hf-repeatablelabel="Contact"
Type: string

data-hf-repeatablelabeladd

Label shown for the add action.
How to use:
data-hf-repeatablelabeladd="Add contact"
Type: string

data-hf-repeatablelabelremove

Label shown for the remove action.
How to use:
data-hf-repeatablelabelremove="Remove contact"
Type: string

data-hf-repeatablemax

Maximum number of repeating units that can exist for the tab.
How to use:
data-hf-repeatablemax="10"
Type: number
Default: 99

data-hf-repeatablemin

Minimum number of repeating units rendered for the tab.
How to use:
data-hf-repeatablemin="1"
Type: number
Default: 1

data-hf-repeatableorder

Controls whether units are rendered in ascending or descending order.
How to use:
data-hf-repeatableorder="desc"
Type: "asc" | "desc"

data-hf-repeatableshowonlylasttoolbar

Shows only the last toolbar instead of one toolbar per visible unit.
How to use:
data-hf-repeatableshowonlylasttoolbar="false"
Type: boolean

data-hf-repeatabletoolbar

Controls whether the repeating-unit toolbar is rendered.
How to use:
data-hf-repeatabletoolbar="true"
Type: boolean

Common Patterns

Counter in the Heading

Use the automatically calculated repeatableCount binding to display the current unit number inside a heading.

<div class="grid column1">
<div class="r1 c1">
<h3>Contact #<span data-hf-bind="textContent: repeatableCount"></span></h3>
</div>
</div>

Repeating Unit CounterDark Repeating Unit Counter
Repeating Unit Counter

Anchor Navigation

Set anchors: true in object notation, or data-hf-repeatableanchors="true" in the legacy notation, to add navigation anchors for every repeating unit in the tab menu.

Visual Separation with CSS

Use CSS to distinguish adjacent repeating units visually.

.hf-formpage .repeatingunit:nth-child(odd) {
background-color: lightgray;
}

You can reuse header and footer blocks with includes. A repeating-unit header or footer may contain one or two blocks.

<li
data-hf-repeating='{
"footer": {
"blocks": [
"../DemoForm.templ/DemoForm.templ.html#repeating1FooterBlock1",
"../DemoForm.templ/DemoForm.templ.html#repeating1FooterBlock2"
],
"idPrefix": "ru1_",
"phPrefix": "ru1_",
"phPlaceholder1": "Footer Placeholder1",
"phPlaceholder2": "Footer Placeholder2"
}
}'
></li>
Note

For includes inside repeating units, use idPrefix instead of data-id-prefix, phPrefix instead of data-ph-prefix, and ph[customValue] instead of data-ph-[customValue].

Provide only one header or footer block to render it full width.

<li
data-hf-repeating='{
"header": ["repeating1HeaderBlock1"],
"footer": {
"blocks": ["../DemoForm.templ/DemoForm.templ.html#repeating1FooterBlock1"],
"idPrefix": "ru1_",
"phPrefix": "ru1_",
"phPlaceholder1": "Footer Placeholder1"
}
}'
></li>