customrepeater
A repeating group of fields where the applicant adds one or more rows of structured data using a shared row template.
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.
When to use
- Shareholders, board members, directors, partners
- Dependants, children, household members
- Bank accounts, properties, or any other list of structured items the applicant types in
- The row count is driven by the applicant, optionally bounded by
minItems/maxItems
When NOT to use
Instead 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 |
Props
| 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"]. |
The required mechanism
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-field placement
Sub-fields must live inside fieldArray.fieldGroup, not inside fieldGroup at the repeater level.
customrepeater
└── fieldArray
└── fieldGroup ← sub-fields live here
└── leaf fieldsfieldArray is a single field-config object, not an array.
Examples
Minimal — child details, up to 4 rows
{
"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"
}
}
]
}
}Full example — shareholders with uniqueness and conditional visibility
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"
}
}
]
}
}Common mistakes
- Placing sub-fields in
fieldGroupat the repeater level instead of insidefieldArray.fieldGroup— the rows do not render. - Using
fieldArrayas an array ([]) instead of an object ({}) —fieldArrayis a single field config object, not a list. - Setting
uniqueRepeaterValuesinpropswithout also adding the matching validator invalidators.validation— the prop alone does not enforce uniqueness; both must be present. - Setting
validators.validation[].options.propKeysto a different key than the one inprops.uniqueRepeaterValues— the validator runs but evaluates the wrong field. - Omitting
validation.messages.uniqueRepeaterValueswhen the uniqueness validator is active — the validator fires but the user sees no error text. - Setting
required: truedirectly instead of using thedefaultRequired+ expression pattern — a hidden repeater withrequired: trueblocks form submission. - Setting
minItemswithoutvalidation.messages.required— the form prevents submission but shows no message explaining why. - Nesting a
customrepeaterinside anothercustomrepeater— deeply nested repeaters cause model-path resolution issues. Use a flat structure withblockwrappers inside a single repeater instead.
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 > customrepeater -
required: falseis set (nevertrue) -
defaultRequiredis set -
expressions["props.required"]is present with the exact required expression - Sub-fields live inside
fieldArray.fieldGroup, notfieldGroup -
fieldArrayis an object, not an array -
uniqueRepeaterValuesinpropsandvalidators.validationreference the same sub-field key, with a matchingvalidation.messages.uniqueRepeaterValues - No nested
customrepeaterinside anothercustomrepeater -
validation.messages.requiredis present whendefaultRequired: trueorminItemsis set