Components

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…
customexpansionrepeaterLong row templates that should collapse into expandable panels
customlabelvaluerepeaterSimple label/value pairs where each row is essentially a single labelled value
customdynamicrepeaterRows that come from an API and the user only fills in extra fields per item
customcascadingdropdowns or a single fieldA single value — a repeater is overkill when there's only ever one item

Props

PropTypeRequired?Description
labelstringrequiredSection label (Title Case, unique across form).
requiredbooleanrequiredAlways false. The expression controls the runtime value.
defaultRequiredbooleanrequiredtrue to require at least one row when the repeater is visible, false for optional.
addTextstringoptionalLabel on the "Add row" button — e.g. "Add New Shareholder". Defaults to a generic add label if omitted.
minItemsnumberoptionalMinimum number of rows the applicant must enter; the form blocks submission until the count is met.
maxItemsnumberoptionalMaximum number of rows; the "Add" button is disabled once this count is reached. Can also be set dynamically via expressions.props.maxItems.
hideLabelbooleanoptionalWhen true, hides the section label — use when the surrounding block already provides context.
disabledbooleanoptionalWhen true, the repeater is read-only; all rows and the "Add/Remove" buttons are locked.
uniqueRepeaterValuesarrayoptionalArray of sub-field keys whose values must be unique across all rows — pair with the uniqueRepeaterValues validator.
hideFieldbooleanoptionalHides 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 fields

fieldArray 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 fieldGroup at the repeater level instead of inside fieldArray.fieldGroup — the rows do not render.
  • Using fieldArray as an array ([]) instead of an object ({}) — fieldArray is a single field config object, not a list.
  • Setting uniqueRepeaterValues in props without also adding the matching validator in validators.validation — the prop alone does not enforce uniqueness; both must be present.
  • Setting validators.validation[].options.propKeys to a different key than the one in props.uniqueRepeaterValues — the validator runs but evaluates the wrong field.
  • Omitting validation.messages.uniqueRepeaterValues when the uniqueness validator is active — the validator fires but the user sees no error text.
  • Setting required: true directly instead of using the defaultRequired + expression pattern — a hidden repeater with required: true blocks form submission.
  • Setting minItems without validation.messages.required — the form prevents submission but shows no message explaining why.
  • Nesting a customrepeater inside another customrepeater — deeply nested repeaters cause model-path resolution issues. Use a flat structure with block wrappers inside a single repeater instead.

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 > customrepeater
  • required: false is set (never true)
  • defaultRequired is set
  • expressions["props.required"] is present with the exact required expression
  • Sub-fields live inside fieldArray.fieldGroup, not fieldGroup
  • fieldArray is an object, not an array
  • uniqueRepeaterValues in props and validators.validation reference the same sub-field key, with a matching validation.messages.uniqueRepeaterValues
  • No nested customrepeater inside another customrepeater
  • validation.messages.required is present when defaultRequired: true or minItems is set

On this page