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.
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
<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.
alwaysShowHeaderFooter
"alwaysShowHeaderFooter": truebooleanfalseanchors
"anchors": truebooleantruedataSource
"dataSource": { "mapping": { "email": "contact_email" } }objecteventCallbacks
"eventCallbacks": { "added": "HFFormdefinition.CustomCode.afterAddContact" }objectfooter
blocks, idPrefix, phPrefix, and custom placeholders."footer": { "blocks": ["../DemoForm.templ/DemoForm.templ.html#contactFooterBlock1"] }string[] | include-objectheader
"header": ["contactHeaderBlock1", "contactHeaderBlock2"]string[]label
"label": "Contact"string"Repeating unit"labelAdd
"labelAdd": "Add contact"string"Add new unit"labelRemove
"labelRemove": "Remove contact"string"Remove last unit"max
"max": 10number99min
"min": 1number1order
"order": "desc""asc" | "desc""asc"showOnlyLastToolbar
"showOnlyLastToolbar": truebooleanfalsetoolbar
"toolbar": falsebooleantrueData 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
"mapping": { "email": "contact_email", "status": { "fieldId": "contact_type", "shadowField": "HFValue" } }RecordonChanged
"onChanged": "HFFormdefinition.CustomCode.onContactUnitsChanged"stringtarget
DataControl or a control with setDataSource()."target": ["contact_selector", "contact_data_control"]string | string[]Mapping Entry Formats
Use one of these formats inside dataSource.mapping:
- A field id string to read the value from the current repeating unit, for example
"email": "contact_email". - An object with
fieldIdandshadowFieldto read a shadow field, for example"status": { "fieldId": "contact_type", "shadowField": "HFValue" }. - An object with
sourceIdsandcallbackto 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.
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
"added": "HFFormdefinition.CustomCode.afterAddContact"stringbeforeadd
"beforeadd": "HFFormdefinition.CustomCode.beforeAddContact"stringbeforeremove
"beforeremove": "HFFormdefinition.CustomCode.beforeRemoveContact"stringremoved
"removed": "HFFormdefinition.CustomCode.afterRemoveContact"stringbeforeadd 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.
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);
}
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.
The legacy attribute notation does not support dataSource or eventCallbacks.
Legacy HTML Definition
<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.
data-hf-repeatable
data-hf-repeatable="true"booleandata-hf-repeatablealwaysshowheaderfooter
data-hf-repeatablealwaysshowheaderfooter="true"booleandata-hf-repeatableanchors
data-hf-repeatableanchors="true"booleandata-hf-repeatablefooter
data-hf-repeatablefooter="contactFooterBlock1,contactFooterBlock2"stringdata-hf-repeatableheader
data-hf-repeatableheader="contactHeaderBlock1,contactHeaderBlock2"stringdata-hf-repeatablelabel
data-hf-repeatablelabel="Contact"stringdata-hf-repeatablelabeladd
data-hf-repeatablelabeladd="Add contact"stringdata-hf-repeatablelabelremove
data-hf-repeatablelabelremove="Remove contact"stringdata-hf-repeatablemax
data-hf-repeatablemax="10"number99data-hf-repeatablemin
data-hf-repeatablemin="1"number1data-hf-repeatableorder
data-hf-repeatableorder="desc""asc" | "desc"data-hf-repeatableshowonlylasttoolbar
data-hf-repeatableshowonlylasttoolbar="false"booleandata-hf-repeatabletoolbar
data-hf-repeatabletoolbar="true"booleanCommon 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 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;
}
Reusing Header and Footer Blocks
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>
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].
Single Full-Width Header or Footer
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>