customdynamicrepeater
An API-driven repeater that creates one row per item returned from a fetch. The user fills in the visible fields per row but does not control row count.
An API-driven repeater that creates one row per item returned from a fetch. The user fills in the visible fields per row but does not control row count.
Read Form Rules before using this component — keys, labels, required fields, expressions, visibility, and validation all follow shared conventions.
Extends Formly's FieldArrayType. Reads props.items at runtime (populated by a sibling data-fetch via populates), creates one row per item, and auto-injects hidden tracking fields (itemId, _isApiPopulated) into every row. Rows the user never modifies are excluded from the submitted form value.
When to use
- Rows come from a
customgenericdatafetchorcustomconditionaldatafetchresponse - The row count is controlled by the API, not the user
- Each row needs the same set of user-input fields keyed against an API-supplied ID/label
When NOT to use
Instead of customdynamicrepeater, use… | For… |
|---|---|
customlabelvaluerepeater | User-controlled row count (manual add/remove) |
customrepeater | User-controlled rows that need uniqueness, minItems, maxItems |
customexpansionrepeater | Long row templates that should collapse into expandable panels |
Props
| Prop | Type | Required? | Description |
|---|---|---|---|
label | string | required | Heading for the repeater section. |
items | array | runtime | Populated by a sibling data-fetch via populates. Do not hardcode in JSON. |
dynamicRepeaterConfig.itemIdField | string | optional | Dot-path to the ID field in each item. Default "id". |
dynamicRepeaterConfig.itemLabelField | string | optional | Dot-path to the display label in each item. Default "label". |
dynamicRepeaterConfig.emptyStateMessage | string | optional | Message shown when props.items is empty. |
dynamicRepeaterConfig.enableDebugLogging | boolean | optional | Enables console debug output. Development only. |
hideField | boolean | optional | Hides the field and excludes its value from submission. Toggle via expressions["props.hideField"]. |
Auto-injected hidden fields
The component prepends these to every row automatically — do not declare them in fieldArray:
| Key | Description |
|---|---|
itemId | Extracted ID value from props.items using itemIdField. |
_isApiPopulated | true for rows that came from the API unchanged. |
Template interpolation
Inside fieldArray sub-fields, use these template variables in props.label, props.placeholder, and props.hint:
| Variable | Resolves to |
|---|---|
{{itemLabel}} | The value at dynamicRepeaterConfig.itemLabelField for that row's item |
{{itemId}} | The value at dynamicRepeaterConfig.itemIdField for that row's item |
Template variables only work inside fieldArray fields, not on the repeater root props.
Sub-field placement
customdynamicrepeater
└── fieldArray
└── fieldGroup ← sub-fields live here
└── leaf fieldsExamples
Minimal — document verifications
{
"key": "DOCUMENT_VERIFICATIONS",
"type": "customdynamicrepeater",
"props": {
"label": "Document Verifications",
"dynamicRepeaterConfig": {
"itemIdField": "documentId",
"itemLabelField": "documentName",
"emptyStateMessage": "No documents found for verification"
}
},
"fieldArray": {
"fieldGroup": [
{
"key": "VERIFICATION_STATUS",
"type": "customdropdown",
"props": {
"label": "Status for {{itemLabel}}",
"required": true,
"bindLabel": "label",
"bindValue": "value",
"options": [
{ "label": "Approved", "value": "APPROVED" },
{ "label": "Rejected", "value": "REJECTED" }
]
},
"validation": {
"messages": { "required": "Status is required." }
}
}
]
}
}Full example — shareholder share counts driven by API lookup
props.items is populated at runtime by a sibling customgenericdatafetch field via populates. The repeater only declares the row template:
{
"key": "SHAREHOLDER_LIST",
"type": "customdynamicrepeater",
"props": {
"label": "Shareholders",
"dynamicRepeaterConfig": {
"itemIdField": "shareholderId",
"itemLabelField": "shareholderName",
"emptyStateMessage": "No shareholders found for this registration number"
}
},
"fieldArray": {
"fieldGroup": [
{
"key": "SHARE_COUNT",
"type": "customcurrencyformatinput",
"props": {
"label": "Number of Shares — {{itemLabel}}",
"hint": "Shareholder ID: {{itemId}}",
"required": true
},
"validation": {
"messages": { "required": "Share count is required." }
}
},
{
"key": "SHARE_CLASS",
"type": "customdropdown",
"props": {
"label": "Share Class — {{itemLabel}}",
"required": true,
"bindLabel": "label",
"bindValue": "value",
"options": [
{ "label": "Ordinary", "value": "ORDINARY" },
{ "label": "Preference", "value": "PREFERENCE" }
]
},
"validation": {
"messages": { "required": "Share class is required." }
}
}
]
}
}Common mistakes
- Declaring
itemIdor_isApiPopulatedinsidefieldArray.fieldGroup— the component injects them automatically; duplicates cause conflicts. - Hardcoding
props.itemsin the form JSON —itemsis populated at runtime by a data-fetchpopulates. Hardcoding bypasses the API-driven pattern. - Using
customlabelvaluerepeaterwhen rows come from an API — switch tocustomdynamicrepeaterso auto-injection and unmodified-row filtering work. - Using
customdynamicrepeaterwhen the user controls the row count — there is no add/remove button. Usecustomlabelvaluerepeaterinstead. - Omitting
dynamicRepeaterConfig.itemIdFieldwhen the API items use a key other than"id"— the component silently storesundefinedif the field doesn't exist. - Using
{{itemLabel}}or{{itemId}}outsidefieldArray(e.g. on the repeater rootlabel) — interpolation only runs inside row fields.
Checklist
-
keyisUPPER_SNAKE_CASEand unique across the entire form -
props.labelis present, unique, and correctly cased - Field is nested at the correct depth:
sections > formly-group > block > customdynamicrepeater - A sibling data-fetch populates
props.itemsviapopulates— do not hardcode it -
dynamicRepeaterConfig.itemIdFieldmatches the actual ID key in the API response -
dynamicRepeaterConfig.itemLabelFieldmatches the actual label key in the API response -
fieldArrayis present and is an object (not an array) - No declarations of
itemIdor_isApiPopulatedinfieldArray.fieldGroup -
dynamicRepeaterConfig.enableDebugLoggingisfalseor removed before shipping
customdropdownpaginated
A dropdown that loads options in pages from a direct backend API. Use when the list is too large to load at once but does not need to go through the integration engine.
customeditabletable
An inline-editable tabular grid where each cell is a typed input (text, number, email, date, select); use for structured data with a fixed or user-managed list of rows and known columns.