customexpansionrepeater
A repeater whose rows render as collapsible expansion panels. Use when each row contains many fields and a flat list would overwhelm the page.
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.
When to use
- Each row has more than a handful of fields (e.g. 6+ inputs, sub-blocks, file uploads)
- The applicant typically completes one row at a time and benefits from seeing collapsed summaries of the others
- Director details, branch information, complex shareholder records, or any structure where the visual weight of a flat repeater becomes a usability problem
When NOT to use
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 |
Props
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"]. |
The required mechanism
Every customexpansionrepeater must include this exact expression.
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
}Sub-field placement
Sub-fields must live inside fieldArray.fieldGroup, not inside fieldGroup at the repeater level.
customexpansionrepeater
└── fieldArray
└── fieldGroup ← sub-fields live here
└── leaf fieldsExamples
Minimal — directors with multi-field rows
{
"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"
}
}
]
}
}Full example — branches with uniqueness and bounded count
{
"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"
}
}
]
}
}Common mistakes
- Using
customexpansionrepeaterwhen the row template has 1–3 fields — prefercustomrepeaterso users don't have to click to see content. - Placing sub-fields in
fieldGroupat the repeater level instead of insidefieldArray.fieldGroup. - Using
fieldArrayas an array ([]) instead of an object ({}). - Setting
uniqueRepeaterValuesinpropswithout the matching validator invalidators.validation. - Setting
required: truedirectly instead of using thedefaultRequired+ expression pattern. - Nesting a
customexpansionrepeaterinside another repeater — keep the structure flat.
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 > customexpansionrepeater -
required: falseis set (nevertrue) -
defaultRequiredis set -
expressions["props.required"]is present with the exact required expression - Sub-fields live inside
fieldArray.fieldGroup, notfieldGroup - Row template has enough fields to justify expansion UX (otherwise prefer
customrepeater) -
uniqueRepeaterValuesinpropsandvalidators.validationreference the same key, with a matchingvalidation.messages.uniqueRepeaterValues -
validation.messages.requiredis present whendefaultRequired: trueorminItemsis set
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.
customfileupload
Standard citizen document upload. The file is encoded as base64 and sent inline as part of the application payload on submission.