A repeating group of fields where the applicant adds one or more rows of structured data using a shared row template.
Read Form Rules before using this component — keys, labels, required fields, expressions, visibility, and validation all follow shared conventions.
minItems / maxItemsInstead of customrepeater, use… |
For… |
|---|---|
customexpansionrepeater |
Long row templates that should collapse into expandable panels |
customlabelvaluerepeater |
Simple label/value pairs where each row is essentially a single labelled value |
customdynamicrepeater |
Rows that come from an API and the user only fills in extra fields per item |
customcascadingdropdowns or a single field |
A single value — a repeater is overkill when there's only ever one item |
| Prop | Type | Required? | Description |
|---|---|---|---|
label |
string |
required | Section label (Title Case, unique across form). |
required |
boolean |
required | Always false. The expression controls the runtime value. |
defaultRequired |
boolean |
required | true to require at least one row when the repeater is visible, false for optional. |
addText |
string |
optional | Label on the "Add row" button — e.g. "Add New Shareholder". Defaults to a generic add label if omitted. |
minItems |
number |
optional | Minimum number of rows the applicant must enter; the form blocks submission until the count is met. |
maxItems |
number |
optional | Maximum number of rows; the "Add" button is disabled once this count is reached. Can also be set dynamically via expressions.props.maxItems. |
hideLabel |
boolean |
optional | When true, hides the section label — use when the surrounding block already provides context. |
disabled |
boolean |
optional | When true, the repeater is read-only; all rows and the "Add/Remove" buttons are locked. |
uniqueRepeaterValues |
array |
optional | Array of sub-field keys whose values must be unique across all rows — pair with the uniqueRepeaterValues validator. |
hideField |
boolean |
optional | Hides the field and excludes its value from submission. Toggle via expressions["props.hideField"]. |
Every customrepeater must include this exact expression, regardless of whether the repeater is required or optional.
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
}
Sub-fields must live inside fieldArray.fieldGroup, not inside fieldGroup at the repeater level.
customrepeater
└── fieldArray
└── fieldGroup ← sub-fields live here
└── leaf fields
fieldArray is a single field-config object, not an array.
{
"key": "CHILD_DETAILS",
"type": "customrepeater",
"props": {
"label": "Child Details",
"addText": "Add Child",
"required": false,
"defaultRequired": true,
"maxItems": 4
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "Please add at least one child."
}
},
"fieldArray": {
"fieldGroup": [
{
"key": "CHILD_SURNAME",
"type": "input",
"props": {
"label": "Surname",
"required": false,
"defaultRequired": true
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
}
},
{
"key": "CHILD_DATE_OF_BIRTH",
"type": "customdatepicker",
"props": {
"label": "Date Of Birth",
"required": false,
"defaultRequired": true
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
}
}
]
}
}
Enforces unique shareholder document numbers, requires at least one row, is conditionally shown, and disables on resubmission.
{
"key": "SHAREHOLDER_DETAILS",
"type": "customrepeater",
"props": {
"label": "Shareholder Details",
"addText": "Add New Shareholder",
"required": false,
"defaultRequired": true,
"hideLabel": true,
"uniqueRepeaterValues": ["SHAREHOLDER_DOCUMENT_NUMBER"]
},
"expressions": {
"hide": "!model?.INCLUDES_SHAREHOLDERS",
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired",
"props.disabled": "model?.IS_RESUBMITTED"
},
"validation": {
"messages": {
"required": "This field is required.",
"uniqueRepeaterValues": "Values in the repeater field must be unique."
}
},
"validators": {
"validation": [
{
"name": "uniqueRepeaterValues",
"options": {
"propKeys": ["SHAREHOLDER_DOCUMENT_NUMBER"]
}
}
]
},
"fieldArray": {
"fieldGroup": [
{
"key": "SHAREHOLDER_NAME",
"type": "input",
"props": {
"label": "Full Name",
"required": false,
"defaultRequired": true
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
}
},
{
"key": "SHAREHOLDER_DOCUMENT_NUMBER",
"type": "input",
"props": {
"label": "Document Number",
"required": false,
"defaultRequired": true
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
}
},
{
"key": "SHAREHOLDER_PERCENTAGE",
"type": "input",
"props": {
"label": "Ownership Percentage",
"type": "number",
"required": false,
"defaultRequired": true
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
}
}
]
}
}
fieldGroup at the repeater level instead of inside fieldArray.fieldGroup — the rows do not render.fieldArray as an array ([]) instead of an object ({}) — fieldArray is a single field config object, not a list.uniqueRepeaterValues in props without also adding the matching validator in validators.validation — the prop alone does not enforce uniqueness; both must be present.validators.validation[].options.propKeys to a different key than the one in props.uniqueRepeaterValues — the validator runs but evaluates the wrong field.validation.messages.uniqueRepeaterValues when the uniqueness validator is active — the validator fires but the user sees no error text.required: true directly instead of using the defaultRequired + expression pattern — a hidden repeater with required: true blocks form submission.minItems without validation.messages.required — the form prevents submission but shows no message explaining why.customrepeater inside another customrepeater — deeply nested repeaters cause model-path resolution issues. Use a flat structure with block wrappers inside a single repeater instead.key is UPPER_SNAKE_CASE and unique across the entire formprops.label is present, unique, and correctly casedsections > formly-group > block > customrepeaterrequired: false is set (never true)defaultRequired is setexpressions["props.required"] is present with the exact required expressionfieldArray.fieldGroup, not fieldGroupfieldArray is an object, not an arrayuniqueRepeaterValues in props and validators.validation reference the same sub-field key, with a matching validation.messages.uniqueRepeaterValuescustomrepeater inside another customrepeatervalidation.messages.required is present when defaultRequired: true or minItems is set