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… |
|---|---|
customdropdown | A flat, non-hierarchical list |
Props
| Prop | Type | Required? | Description |
|---|---|---|---|
label | string | required | Visible label for the overall field group. |
required | boolean | required | Always false. The expression controls the runtime value. |
defaultRequired | boolean | required | true for required fields, false for optional ones. |
configs | array | required | Ordered array of level definitions, top to bottom. See below. |
locationFirstLevel | string | required | Top-most level key (e.g. "PROVINCE"). |
locationLastLevel | string | required | Bottom-most level key (e.g. "VILLAGE"). |
hideLabel | boolean | optional | Set true to hide the component-level label (individual level labels are still shown). |
placeholder | string | optional | Ghost text for the first level dropdown. |
hideField | boolean | optional | Hides the entire component. Toggle via expressions["props.hideField"]. |
configs array — per-level definition
One entry per level, in order from top to bottom.
| Field | Type | Description |
|---|---|---|
key | string | Model key where this level's selection is stored (e.g. "PROVINCE"). |
label | string | Label shown above this level's dropdown. |
parent | string | null | key of the parent level, or null for the root level. |
parentBindKey | string | The field on the parent's selected object used to filter this level's options. Always "id" for location. |
required | boolean | Whether this level is required. |
bindlabel | string | Key in each option object to display. Always "label" for location. |
bindvalue | string | Key in each option object to store. "" stores the whole object (used for location so child levels can filter by id). |
placeholder | string | Ghost text for this level's dropdown. |
dataset | object | Data source configuration. See below. |
excludedItems | string[] | Optional. List of option values to remove from this level's fetched response. See Excluding values. |
excludedItemsDetails | object[] | Optional. The full { label, value } objects for each excluded item. Companion to excludedItems. |
excludedItemKey | string | Optional. The key on each fetched option compared against excludedItems to decide removal. Use "value" for location. |
dataset object
| Field | Type | Description |
|---|---|---|
url | string | API path to fetch options. Province uses /by-dataset-code/PROVINCE; all sub-levels use /by-details. |
dataField | string | Key in the API response containing the options array. Always "data" for location. |
useBaseUrl | boolean | Always 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:
| Field | Purpose |
|---|---|
excludedItemKey | The key on each fetched option to compare. For location levels this is "value". |
excludedItems | The list of values to drop. Any option whose excludedItemKey matches one of these is removed. |
excludedItemsDetails | The 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-detailsare 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
-
keyisUPPER_SNAKE_CASEand unique across the entire form -
props.labelis present - Field is nested at the correct depth:
sections > formly-group > block > customcascadingdropdowns -
required: falseis set (nevertrue) -
defaultRequiredis set -
expressions["props.required"]is present with the exact required expression -
locationFirstLevelandlocationLastLevelmatch the first and lastkeyinconfigs -
configshas one entry per level, in top-to-bottom order - Root level has
parent: null; all subsequent levels haveparentset to the previous level'skey - Sub-levels (District through Village) have
parentBindKey: "id" -
bindvalue: ""on all levels (stores the whole object so child levels can filter byid) - Province uses
/by-dataset-code/PROVINCE; all other levels use/by-details - If a level restricts options,
excludedItems,excludedItemsDetails, andexcludedItemKeyare all set on the level that fetches the flat dataset (the root level) -
validation.messages.requiredis present
customapplicationnumberinput
An input specialised for IremboHub application numbers — validates the format and can look up the referenced application to populate downstream fields.
customconditionaldatafetch
A guarded variant of customgenericdatafetch that skips duplicate calls, chains dependent fetches, and conditionally fires only when a fetchCondition is met.