A repeater whose rows render as collapsible expansion panels. Use when each row contains many fields and a flat list would overwhelm the page.
Read Form Rules before using this component — keys, labels, required fields, expressions, visibility, and validation all follow shared conventions.
Behaves like customrepeater — same fieldArray mechanism, same required pattern, same uniqueness validator — but each row is rendered as an expandable accordion panel instead of an always-open row. Only the active row is expanded; the others collapse to a summary header.
Instead of customexpansionrepeater, use… |
For… |
|---|---|
customrepeater |
Short row templates (1–3 fields) where always-open rows are easier to scan |
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 |
The prop list mirrors customrepeater. Only differences are noted in the Description column.
| Prop | Type | Required? | Description |
|---|---|---|---|
label |
string |
required | Section label, also used as the fallback panel header (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 Director". 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 customexpansionrepeater must include this exact expression.
"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.
customexpansionrepeater
└── fieldArray
└── fieldGroup ← sub-fields live here
└── leaf fields
{
"key": "DIRECTORS",
"type": "customexpansionrepeater",
"props": {
"label": "Directors",
"addText": "Add Director",
"required": false,
"defaultRequired": true,
"minItems": 1
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "Please add at least one director."
}
},
"fieldArray": {
"fieldGroup": [
{
"key": "DIRECTOR_NID",
"type": "customidinput",
"props": {
"label": "National ID",
"required": false,
"defaultRequired": true
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
}
},
{
"key": "DIRECTOR_FIRST_NAME",
"type": "input",
"props": {
"label": "First Name",
"readonly": true,
"required": false,
"defaultRequired": true
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
}
},
{
"key": "DIRECTOR_LAST_NAME",
"type": "input",
"props": {
"label": "Last Name",
"readonly": true,
"required": false,
"defaultRequired": true
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
}
},
{
"key": "DIRECTOR_PHONE",
"type": "customphonenumber",
"props": {
"label": "Phone Number",
"required": false,
"defaultRequired": true
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
}
}
]
}
}
{
"key": "BRANCH_DETAILS",
"type": "customexpansionrepeater",
"props": {
"label": "Branch Details",
"addText": "Add Branch",
"required": false,
"defaultRequired": true,
"minItems": 1,
"maxItems": 10,
"uniqueRepeaterValues": ["BRANCH_NAME"]
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "Please add at least one branch.",
"uniqueRepeaterValues": "Branch names must be unique."
}
},
"validators": {
"validation": [
{
"name": "uniqueRepeaterValues",
"options": {
"propKeys": ["BRANCH_NAME"]
}
}
]
},
"fieldArray": {
"fieldGroup": [
{
"key": "BRANCH_NAME",
"type": "input",
"props": {
"label": "Branch Name",
"required": false,
"defaultRequired": true
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
}
},
{
"key": "BRANCH_ADDRESS",
"type": "input",
"props": {
"label": "Branch Address",
"required": false,
"defaultRequired": true
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
}
},
{
"key": "BRANCH_CONTACT_PHONE",
"type": "customphonenumber",
"props": {
"label": "Branch Contact Phone",
"required": false,
"defaultRequired": true
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
}
}
]
}
}
customexpansionrepeater when the row template has 1–3 fields — prefer customrepeater so users don't have to click to see content.fieldGroup at the repeater level instead of inside fieldArray.fieldGroup.fieldArray as an array ([]) instead of an object ({}).uniqueRepeaterValues in props without the matching validator in validators.validation.required: true directly instead of using the defaultRequired + expression pattern.customexpansionrepeater inside another repeater — keep the structure flat.key is UPPER_SNAKE_CASE and unique across the entire formprops.label is present, unique, and correctly casedsections > formly-group > block > customexpansionrepeaterrequired: false is set (never true)defaultRequired is setexpressions["props.required"] is present with the exact required expressionfieldArray.fieldGroup, not fieldGroupcustomrepeater)uniqueRepeaterValues in props and validators.validation reference the same key, with a matching validation.messages.uniqueRepeaterValuesvalidation.messages.required is present when defaultRequired: true or minItems is set