Skip to Content
DocsCascading Dropdowns

Cascading Dropdowns

Cascading dropdowns define relationships where selecting an option in one select element dynamically loads the choices in a child select element (e.g. selecting a “Country” loads its “Cities”).

Formly provides a declarative cascade configuration that automatically watches dependencies, runs async data loaders, and exposes option states with type safety.


Defining Cascading Options

To configure cascading relations, pass the cascade option config to useForm:

import { useForm } from "@explita/formly"; type Option = { value: string; label: string }; const form = useForm({ defaultValues: { country: "", city: "", }, cascade: { // Watch 'country' and load 'cities' reactively cities: { watch: ["country"], fn: async ([country]): Promise<Option[]> => { if (!country) return []; // Fetch from API const res = await fetch(`/api/countries/${country}/cities`); return await res.json(); }, onLoad: (cities) => { console.log("Cities loaded successfully:", cities); }, }, }, });

Accessing Options State

Formly exposes option sets, loaders, and errors under form.cascade:

const { data, isLoading, error } = form.cascade.cities; // `data` is automatically typed as Option[]!

Complete Example

import React from "react"; import { Form, useForm } from "@explita/formly"; const countries = [ { value: "us", label: "United States" }, { value: "ca", label: "Canada" }, ]; export default function BookingForm() { const form = useForm({ defaultValues: { country: "", city: "" }, cascade: { cities: { watch: ["country"], fn: async ([country]) => { if (!country) return []; // Mimic API delay await new Promise((r) => setTimeout(r, 600)); if (country === "us") { return [ { value: "nyc", label: "New York City" }, { value: "la", label: "Los Angeles" }, ]; } else if (country === "ca") { return [ { value: "tor", label: "Toronto" }, { value: "van", label: "Vancouver" }, ]; } return []; }, }, }, onSubmit: (values) => console.log("Booking submission:", values), }); // Extract option array and loading flag const { data: cityOptions = [], isLoading: loadingCities } = form.cascade.cities; return ( <Form use={form}> {/* Country Selector */} <form.Field name="country" label="Select Country" render={(props) => ( <select {...props}> <option value="">-- Choose Country --</option> {countries.map((c) => ( <option key={c.value} value={c.value}> {c.label} </option> ))} </select> )} /> {/* City Selector (Cascades from Country selection) */} <form.Field name="city" label="Select City" render={(props) => ( <div style={{ marginTop: "1rem" }}> <select {...props} disabled={!form.values.country || loadingCities}> <option value=""> {loadingCities ? "Loading cities..." : "-- Choose City --"} </option> {cityOptions.map((city) => ( <option key={city.value} value={city.value}> {city.label} </option> ))} </select> {loadingCities && <span style={{ marginLeft: "10px" }}>⏳</span>} </div> )} /> <button type="submit" style={{ marginTop: "1.5rem" }} disabled={loadingCities}> Submit Booking </button> </Form> ); }

Automatic Reset Behavior

When a watched dependency changes (e.g. the user changes Country from United States to Canada), Formly automatically resets the dependent field value (city) back to its empty default values. This prevents users from accidentally submitting invalid nested combinations (like Canadian cities with the United States country selected).

Last updated on