Components

customcascadingdropdowns

A multi-level cascading dropdown for hierarchical selection. Each level filters the options available in the next level based on the parent selection.

A multi-level cascading dropdown for hierarchical selection. Each level filters the options available in the next level based on the parent selection.

Read Form Rules before using this component — keys, labels, required fields, expressions, visibility, and validation all follow shared conventions.

When to use

  • Rwanda administrative location selection (Province → District → Sector → Cell → Village)
  • Any hierarchical selection where each level depends on the parent

When NOT to use

Instead of customcascadingdropdowns, use…For…
customdropdownA flat, non-hierarchical list

Props

PropTypeRequired?Description
labelstringrequiredVisible label for the overall field group.
requiredbooleanrequiredAlways false. The expression controls the runtime value.
defaultRequiredbooleanrequiredtrue for required fields, false for optional ones.
configsarrayrequiredOrdered array of level definitions, top to bottom. See below.
locationFirstLevelstringrequiredTop-most level key (e.g. "PROVINCE").
locationLastLevelstringrequiredBottom-most level key (e.g. "VILLAGE").
hideLabelbooleanoptionalSet true to hide the component-level label (individual level labels are still shown).
placeholderstringoptionalGhost text for the first level dropdown.
hideFieldbooleanoptionalHides the entire component. Toggle via expressions["props.hideField"].

configs array — per-level definition

One entry per level, in order from top to bottom.

FieldTypeDescription
keystringModel key where this level's selection is stored (e.g. "PROVINCE").
labelstringLabel shown above this level's dropdown.
parentstring | nullkey of the parent level, or null for the root level.
parentBindKeystringThe field on the parent's selected object used to filter this level's options. Always "id" for location.
requiredbooleanWhether this level is required.
bindlabelstringKey in each option object to display. Always "label" for location.
bindvaluestringKey in each option object to store. "" stores the whole object (used for location so child levels can filter by id).
placeholderstringGhost text for this level's dropdown.
datasetobjectData source configuration. See below.
excludedItemsstring[]Optional. List of option values to remove from this level's fetched response. See Excluding values.
excludedItemsDetailsobject[]Optional. The full { label, value } objects for each excluded item. Companion to excludedItems.
excludedItemKeystringOptional. The key on each fetched option compared against excludedItems to decide removal. Use "value" for location.

dataset object

FieldTypeDescription
urlstringAPI path to fetch options. Province uses /by-dataset-code/PROVINCE; all sub-levels use /by-details.
dataFieldstringKey in the API response containing the options array. Always "data" for location.
useBaseUrlbooleanAlways true — prepends the API gateway base URL.

Excluding values from a level

The dataset fetch returns every option the API has for a level (e.g. all 30 districts). To narrow what the user can pick — for example, a service that only operates in a handful of districts — add an exclusion list to that level's configs entry. The exclusion is applied to the fetched response on the client; the API call itself is unchanged.

Three fields work together on the level you want to filter:

FieldPurpose
excludedItemKeyThe key on each fetched option to compare. For location levels this is "value".
excludedItemsThe list of values to drop. Any option whose excludedItemKey matches one of these is removed.
excludedItemsDetailsThe matching { label, value } objects, kept alongside for readability and tooling.

When the form is rendered, the component fetches the level's options and removes every option whose excludedItemKey value appears in excludedItems. Everything not listed remains selectable.

Include vs. exclude

There is no separate "include only these" prop — an allow-list is expressed as exclusion. To keep only a small set of districts, compute the exclusion as all options minus the ones you want and put the result in excludedItems. Both an explicit exclude list and an allow-list resolve to the same excludedItems array in the final config.

Example — District level limited to three districts

This level fetches all districts, then excludes Burera (404), Gakenke (402), and Gasabo (102) so they cannot be selected:

{
  "key": "FIELD_FACILITY",
  "type": "customcascadingdropdowns",
  "props": {
    "label": "District",
    "required": false,
    "defaultRequired": true,
    "locationFirstLevel": "DISTRICT",
    "locationLastLevel": "DISTRICT",
    "configs": [
      {
        "key": "FIELD_FACILITY_DISTRICT",
        "label": "District",
        "parent": null,
        "required": true,
        "bindlabel": "label",
        "bindvalue": "",
        "placeholder": "Select district",
        "dataset": {
          "url": "/admin/v1/dataset-items/by-dataset-code/DISTRICT",
          "dataField": "data",
          "useBaseUrl": true
        },
        "excludedItems": ["404", "402", "102"],
        "excludedItemsDetails": [
          { "label": "Burera", "value": "404" },
          { "label": "Gakenke", "value": "402" },
          { "label": "Gasabo", "value": "102" }
        ],
        "excludedItemKey": "value"
      }
    ]
  },
  "expressions": {
    "props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
  },
  "validation": {
    "messages": {
      "required": "This field is required."
    }
  }
}

Exclusion filters the fetched response by value, so it only works on a level that pulls a flat dataset (the root level, e.g. /by-dataset-code/DISTRICT). Child levels fetched via /by-details are already scoped by the parent selection.

Example — full Province → Village location picker

{
  "key": "FIELD_LOCATION",
  "type": "customcascadingdropdowns",
  "props": {
    "label": "Location",
    "required": false,
    "defaultRequired": true,
    "locationFirstLevel": "PROVINCE",
    "locationLastLevel": "VILLAGE",
    "hideLabel": true,
    "configs": [
      {
        "key": "PROVINCE",
        "label": "Province",
        "parent": null,
        "required": true,
        "bindlabel": "label",
        "bindvalue": "",
        "placeholder": "Select Province",
        "dataset": {
          "url": "/admin/v1/dataset-items/by-dataset-code/PROVINCE",
          "dataField": "data",
          "useBaseUrl": true
        }
      },
      {
        "key": "DISTRICT",
        "label": "District",
        "parent": "PROVINCE",
        "parentBindKey": "id",
        "required": true,
        "bindlabel": "label",
        "bindvalue": "",
        "placeholder": "Select District",
        "dataset": {
          "url": "/admin/v1/dataset-items/by-details",
          "dataField": "data",
          "useBaseUrl": true
        }
      },
      {
        "key": "SECTOR",
        "label": "Sector",
        "parent": "DISTRICT",
        "parentBindKey": "id",
        "required": true,
        "bindlabel": "label",
        "bindvalue": "",
        "placeholder": "Select Sector",
        "dataset": {
          "url": "/admin/v1/dataset-items/by-details",
          "dataField": "data",
          "useBaseUrl": true
        }
      },
      {
        "key": "CELL",
        "label": "Cell",
        "parent": "SECTOR",
        "parentBindKey": "id",
        "required": true,
        "bindlabel": "label",
        "bindvalue": "",
        "placeholder": "Select Cell",
        "dataset": {
          "url": "/admin/v1/dataset-items/by-details",
          "dataField": "data",
          "useBaseUrl": true
        }
      },
      {
        "key": "VILLAGE",
        "label": "Village",
        "parent": "CELL",
        "parentBindKey": "id",
        "required": true,
        "bindlabel": "label",
        "bindvalue": "",
        "placeholder": "Select Village",
        "dataset": {
          "url": "/admin/v1/dataset-items/by-details",
          "dataField": "data",
          "useBaseUrl": true
        }
      }
    ]
  },
  "expressions": {
    "props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
  },
  "validation": {
    "messages": {
      "required": "This field is required."
    }
  }
}

The Province level fetches from /by-dataset-code/PROVINCE. Every sub-level (District through Village) fetches from /by-details, passing the parent's selected object's id via parentBindKey: "id". bindvalue: "" stores the whole option object so the child level can read id from it.

Checklist

  • key is UPPER_SNAKE_CASE and unique across the entire form
  • props.label is present
  • Field is nested at the correct depth: sections > formly-group > block > customcascadingdropdowns
  • required: false is set (never true)
  • defaultRequired is set
  • expressions["props.required"] is present with the exact required expression
  • locationFirstLevel and locationLastLevel match the first and last key in configs
  • configs has one entry per level, in top-to-bottom order
  • Root level has parent: null; all subsequent levels have parent set to the previous level's key
  • Sub-levels (District through Village) have parentBindKey: "id"
  • bindvalue: "" on all levels (stores the whole object so child levels can filter by id)
  • Province uses /by-dataset-code/PROVINCE; all other levels use /by-details
  • If a level restricts options, excludedItems, excludedItemsDetails, and excludedItemKey are all set on the level that fetches the flat dataset (the root level)
  • validation.messages.required is present

On this page