Components

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 customgenericdatafetch or customconditionaldatafetch response
  • 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…
customlabelvaluerepeaterUser-controlled row count (manual add/remove)
customrepeaterUser-controlled rows that need uniqueness, minItems, maxItems
customexpansionrepeaterLong row templates that should collapse into expandable panels

Props

PropTypeRequired?Description
labelstringrequiredHeading for the repeater section.
itemsarrayruntimePopulated by a sibling data-fetch via populates. Do not hardcode in JSON.
dynamicRepeaterConfig.itemIdFieldstringoptionalDot-path to the ID field in each item. Default "id".
dynamicRepeaterConfig.itemLabelFieldstringoptionalDot-path to the display label in each item. Default "label".
dynamicRepeaterConfig.emptyStateMessagestringoptionalMessage shown when props.items is empty.
dynamicRepeaterConfig.enableDebugLoggingbooleanoptionalEnables console debug output. Development only.
hideFieldbooleanoptionalHides 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:

KeyDescription
itemIdExtracted ID value from props.items using itemIdField.
_isApiPopulatedtrue 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:

VariableResolves 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 fields

Examples

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 itemId or _isApiPopulated inside fieldArray.fieldGroup — the component injects them automatically; duplicates cause conflicts.
  • Hardcoding props.items in the form JSON — items is populated at runtime by a data-fetch populates. Hardcoding bypasses the API-driven pattern.
  • Using customlabelvaluerepeater when rows come from an API — switch to customdynamicrepeater so auto-injection and unmodified-row filtering work.
  • Using customdynamicrepeater when the user controls the row count — there is no add/remove button. Use customlabelvaluerepeater instead.
  • Omitting dynamicRepeaterConfig.itemIdField when the API items use a key other than "id" — the component silently stores undefined if the field doesn't exist.
  • Using {{itemLabel}} or {{itemId}} outside fieldArray (e.g. on the repeater root label) — interpolation only runs inside row fields.

Checklist

  • key is UPPER_SNAKE_CASE and unique across the entire form
  • props.label is present, unique, and correctly cased
  • Field is nested at the correct depth: sections > formly-group > block > customdynamicrepeater
  • A sibling data-fetch populates props.items via populates — do not hardcode it
  • dynamicRepeaterConfig.itemIdField matches the actual ID key in the API response
  • dynamicRepeaterConfig.itemLabelField matches the actual label key in the API response
  • fieldArray is present and is an object (not an array)
  • No declarations of itemId or _isApiPopulated in fieldArray.fieldGroup
  • dynamicRepeaterConfig.enableDebugLogging is false or removed before shipping

On this page