Components

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…
customrepeaterShort row templates (1–3 fields) where always-open rows are easier to scan
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

Props

The prop list mirrors customrepeater. Only differences are noted in the Description column.

PropTypeRequired?Description
labelstringrequiredSection label, also used as the fallback panel header (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 Director". 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 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 fields

Examples

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 customexpansionrepeater when the row template has 1–3 fields — prefer customrepeater so users don't have to click to see content.
  • Placing sub-fields in fieldGroup at the repeater level instead of inside fieldArray.fieldGroup.
  • Using fieldArray as an array ([]) instead of an object ({}).
  • Setting uniqueRepeaterValues in props without the matching validator in validators.validation.
  • Setting required: true directly instead of using the defaultRequired + expression pattern.
  • Nesting a customexpansionrepeater inside another repeater — keep the structure flat.

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 > customexpansionrepeater
  • 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
  • Row template has enough fields to justify expansion UX (otherwise prefer customrepeater)
  • uniqueRepeaterValues in props and validators.validation reference the same key, with a matching validation.messages.uniqueRepeaterValues
  • validation.messages.required is present when defaultRequired: true or minItems is set

On this page