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.
Instead of customcascadingdropdowns, use… |
For… |
|---|---|
customdropdown |
A flat, non-hierarchical list |
| 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 definitionOne 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. |
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.
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.
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.
{
"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.
key is UPPER_SNAKE_CASE and unique across the entire formprops.label is presentsections > formly-group > block > customcascadingdropdownsrequired: false is set (never true)defaultRequired is setexpressions["props.required"] is present with the exact required expressionlocationFirstLevel and locationLastLevel match the first and last key in configsconfigs has one entry per level, in top-to-bottom orderparent: null; all subsequent levels have parent set to the previous level's keyparentBindKey: "id"bindvalue: "" on all levels (stores the whole object so child levels can filter by id)/by-dataset-code/PROVINCE; all other levels use /by-detailsexcludedItems, excludedItemsDetails, and excludedItemKey are all set on the level that fetches the flat dataset (the root level)validation.messages.required is present