In this article, I will show you how to create sophisticated cascading Kendo Dropdown menus based on the selection of a dependent dropdown, and provide real-time examples.

This article will be more comprehensive than the generic examples provided on the Kendo UI website- I will share how to use multiple input parameters and provide some tips and tricks that I have learned over the years to make your code more reliable and accessible. Unlike the Kendo UI examples on the Telerik site, this article will take you through the whole process including building the remote end-point. 

In this example, we will create and demonstrate a Country, State, and City cascading dropdown and demonstrate the ease of use when using the search widget to quickly select a value from a long list of items. These comprehensive dropdowns will contain all of the countries, states, and major cities in the world.

We will also be going over various Kendo events and will change the UI based on how many records are being returned in the JSON object.

I will also provide the database files and all of the code on GitHub for others to use. The Kendo UI-related code will be compatible with the open-source version of Kendo in order to use this in your own projects and potentially distribute your code.

Like the rest of this series of Kendo articles, we will be using ColdFusion as our service end-point, however, if you use another server-side language, you should be able to follow along.



Cascading Country State and City Cascading Dropdowns Example Application

We are going to demonstrate the Kendo Dropdowns with a country, state, and city cascading dropdown. The first dropdown will display all of the countries of the world, the second dropdown will only display the states relevant to the selected country, and the third dropdown will show the cities within the chosen state. When there are no states for a given country, we will disable the state dropdown and query the database for all cities for the selected country instead of the state using an extra parameter that we will send to the server.

Click on the button below to see the live example.


Our Cascading Kendo Dropdowns Methodology

Telerik suggests using the cascadeFrom and other cascade-specific arguments when creating cascading dropdowns, however, we are going to use a different approach.

Telerik's cascading implementation works fine, however, I have personally had difficulties with it. It is more complex and problematic than just saving the required elements needed for the dependent dropdowns in a hidden field. I have also had issues with the variables not being defined when I need them as the widgets are typically found in a document-ready scope and the variables are unavailable outside of this scope. Saving the selected values in a hidden form field allows us to use this selection for other logic outside of the document-ready scope. It is also more difficult to pass in multiple arguments to the Kendo data source that we are doing here, and you can't use multiple Kendo cascadeFrom arguments. Using this approach allows you to send the selections of multiple dropdowns to the controller instead of just using one. I also don't like that all of the widgets need to be tightly bound together in a single scope and prefer an approach that allows for better separation of logic.

Gregory's approach:

  1. Initialize the dropdowns as you normally would- but add an onChange method.
  2. Use the drop-down widgets onChange method to:
    1. Capture and store the user's selection into a hidden form field.
    2. Refresh any dependent dropdowns using the widgets refresh method.
  3. The data sources belonging to the dependent dropdowns will pass the necessary values in the hidden form fields to the remote end-point.
  4. If necessary, use the DataSource's onChange method to:
    1.  dynamically populate the values of a dependent MultiSelect
    2. or validate the data and perform any additional logic.

We will cover step 4 in a future article.

I would like to add an additional rule to keep the Kendo dropdown and data source separate for better separation of logic and code reuse, but this is not strictly necessary.

Using this purely state-driven approach to connecting the menus is reliable and I have used this approach in production environments for the last ten years.


Introducing Kendo UI Events

Every widget has a number of events. There are traditional events that are similar to typical DOM events, such as the onChange event, and additional events that occur when the dropdowns acquire data such as the dataBound event. Additionally, Kendo's abstractions, such as the Kendo dataSource, also have their own events. To see a list of the events for a particular widget, go to the widget example on the Telerik site, and click on the API button at the bottom of the page. 

In this article, we will use the change event for the dropDownList to save the values that the user selected into hidden form fields and to trigger the dependent dropdowns. We are also going to use the Kendo dataSource to make changes in the UI.


Creating the Countries - States - Cities Database

We will be using the open-source Countries- States and Cities GitHub repository created by Darshan Gada. This repository is updated several times a year and contains all of the necessary files to create and maintain the database.

If you want to follow along and create your own MySql World database, go to the repository and locate the country SQL file in the SQL folder. We need to open and download the world.sql file. The URL to this file as of 2022 is https://github.com/dr5hn/countries-states-cities-database/blob/master/sql/world.sql

Do not download any other file- we only need the world.sql file to create the database.

Once the world.sql file is downloaded, copy and paste the code into your MySQL query, or import the SQL file into your MySql database. This file can also be used to update the database. If you need assistance please refer to your MySql documentation.

Once you're done, create a ColdFusion data source.


Creating the back-end ColdFusion Service Endpoint

We are using a ColdFusion component, WorldCountries.cfc, to serve as the service endpoint for the Kendo Datasource that will populate the Kendo dropdowns. This component will prepare and send the data from the world database back to each cascading dropdown as JSON.

The getCountries WoldCountries.cfc method

This method populates the country dropdown list in the interface and should be self-explanatory. Since the dropdowns can be quite long (there are thousands of potential cities for example), we only want to send back what is necessary here, namely the country id and name.

This method does not take any arguments but simply queries our World MySql database and converts the ColdFusion query object using our CfJson object as a JSON object.
See https://www.gregoryalexander.com/blog/2022/7/13/Using-ColdFusion-to-Populate-Kendo-UI-Widgets for more information on how we are converting the ColdFusion query object to JSON.

<cffunction name="getCountries" access="remote" returnformat="json" output="true"
		hint="Gets the world countries">

	<cfquery name="Data" datasource="cityDb">
		SELECT id, name FROM countries
		ORDER BY name
	</cfquery>

	<!--- Convert the query object into JSON using the convertCfQuery2JsonStruct method --->
	<cfinvoke component="#application.cfJsonComponentPath#" method="convertCfQuery2JsonStruct" returnvariable="jsonString">
		<cfinvokeargument name="queryObj" value="#Data#">
		<cfinvokeargument name="includeTotal" value="true">	
	</cfinvoke>

	<!--- Return it. --->
	<cfreturn jsonString>

</cffunction>

The getStates method

This function is similar to the getCountries method above, however, it takes the country id as an argument and filters the results based on the selected country. It will only retrieve states that belong to the country that was selected by the user.

<cffunction name="getStates" access="remote" returnformat="json" output="true"
		hint="Gets the world states by a variety of optional arguments">
	<cfargument name="countryId" type="string" required="false" default="" />

	<cfquery name="Data" datasource="cityDb">
		SELECT id, name FROM states
		WHERE 0=0
	<cfif len(arguments.countryId)>
		AND country_id = <cfqueryparam value="#arguments.countryId#" cfsqltype="integer">
	</cfif>
		ORDER BY name
	</cfquery>

	<!--- Convert the query object into JSON using the convertCfQuery2JsonStruct method --->
	<cfinvoke component="#application.cfJsonComponentPath#" method="convertCfQuery2JsonStruct" returnvariable="jsonString">
		<cfinvokeargument name="queryObj" value="#Data#">
		<cfinvokeargument name="includeTotal" value="true">	
	</cfinvoke>

	<!--- Return it. --->
	<cfreturn jsonString>

</cffunction>

The getCities Method

This method is a little different than the getStates method as it accepts two optional arguments- the country and state id. A country might have cities, but not states. If this is the case, we need to return the cities that belong to a country, yet don't reside in their own state. We will cover this scenario further later in this article. 

<cffunction name="getCities" access="remote" returnformat="json" output="true"
		hint="Gets the world cities by a variety of optional arguments">
	<cfargument name="countryId" type="string" required="false" default="" />
	<cfargument name="stateId" type="string" required="false" default="" />

	<cfquery name="Data" datasource="cityDb">
		SELECT id, name FROM cities
		WHERE 0=0
	<cfif len(arguments.countryId)>
		AND country_id = <cfqueryparam value="#arguments.countryId#" cfsqltype="integer">
	</cfif>
	<cfif len(arguments.stateId)>
		AND state_id = <cfqueryparam value="#arguments.stateId#" cfsqltype="integer">
	</cfif>
		ORDER BY name
	</cfquery>

	<!--- Convert the query object into JSON using the convertCfQuery2JsonStruct method --->
	<cfinvoke component="#application.cfJsonComponentPath#" method="convertCfQuery2JsonStruct" returnvariable="jsonString">
		<cfinvokeargument name="queryObj" value="#Data#">
		<cfinvokeargument name="includeTotal" value="true">			
	</cfinvoke>

	<!--- Return it. --->
	<cfreturn jsonString>

</cffunction>

The First Country Dropdown

The first dropdown, the country drop-down list is the easiest to understand.

This dropdown will have a detached Kendo data source (you may use inline if you want) that grabs all of the countries from the ColdFusion remote endpoint. There is nothing inherently different than the previous Introducing the various forms of Kendo Dropdowns in the data source or widget initialization.

Where this differs is that after we initialize the widget we will use an onChange event on the DropDown list. We will explain further below.


Create The Country Dropdown Kendo Datasource

This data source simply invokes the WorldCountries.cfc getCountries method which returns a JSON object with the countryId and country of all of the countries in the world. There is nothing out of the norm here that we did not already cover in previous articles.

// ---------------------------- Top level country dropdown. ----------------------------
			
// ---------- Country Datasource. ----------
var countryDs = new kendo.data.DataSource({
	transport: {
		read: {
			cache: false,
			// Note: since this template is in a different directory, we can't specify the cfc template without the full path name.
			url: "<cfoutput>#application.baseUrl#</cfoutput>/demo/WorldCountries.cfc?method=getCountries", // The cfc component which processes the query and returns a json string.
			dataType: "json",
			contentType: "application/json; charset=utf-8", // Note: when posting json via the request body to a coldfusion page, we must use this content type or we will get a 'IllegalArgumentException' on the ColdFusion processing page.
			type: "POST"
		}
	},
});//var countryDs...

Adding the Country Flag to the DropDownList

It is relatively trivial to add the country flag to the dropdown list using a Kendo Template. First, you must download a country flag library. There are hundreds of libraries, I used the country-flag library found on GitHub at https://github.com/hampusborgos/country-flags. One of the reasons that I chose this library is that it has the name of the flags that is identical to the ISO2 name found in the ISO2 column in the country table in the country-states-cities database.

Adding the flag was simple, I just added the URL to the .png flag inside of a Kendo Template. Here I used 20px14px which matches the 10x7 ratio of the png files that are 100x70. The country-flag library also has the .svg flag files that scale automatically, however, they are much larger than the png files and the SVG files took too long for the dropdowns to properly render. 

Here is the code for the Kendo Template:

<!-- Kendo Template to display the flag next to the country name -->
<script type="text/x-kendo-template" id="country-flag">
	<div class="country-flag">
		<img src="/common/assets/flags/png250px/#: data.iso2 #.png" width="20" height="14"/> #: data.name #
	</div>
</script>

Initialize the Country DropDownList

Here we are simply extracting data from the countryDs Kendo data source, using the countryId as the value of the dropdown, and using the country name as the label. We are also using the template to add the flag to the country name.

There is nothing unusual that has not been covered in our previous article other than having an onChange event that calls the onCountryChange function which will be discussed below. 

// ----------- Country Dropdown. -----------
var countryDropdown = $("#countryDropdown").kendoDropDownList({
	optionLabel: "Select Country...",
	dataValueField: "id",
	dataTextField: "name",
	// Add the country flag
	template: kendo.template($("#country-flag").html()),
	// Create the onChange method that will create the next dropdown list
	change: onCountryChange,
	filter: "contains",
	dataSource: countryDs
}).data("kendoDropDownList");//var countryDropdown...

The Country DropDownList onChange Event

The change event is fired off when the user selects a new country. Here, we are going to simply save the selected countryId into a hidden form field. 

// ------------ onChange Event ------------
// On change function that will enable the next dropdown menu if a value is selected.
function onCountryChange(e){
	// Get the selected value
	var id = this.value();
	// Save the selected value in a hidden form
	$("#selectedCountry").val(this.value());
	// If the user selected a value, enable and refresh the next dropdown list
	if (id != '') {
		// Enable the state dropdown
		stateDropdown.enable();
		// Refresh the state dropdown
		stateDropdown.dataSource.read();
	}//..if (id != '') {

	// Disable the city button
	// Get the Kendo button at the end of the interface
	var cityButton = $("#cityButton").data("kendoButton");
	// Disable the the button
	cityButton.enable(true);

}//function onCountryChange(e)...

The Second State Dropdown

The second dropdown, the State DropDownList, will show all of the states for the selected country. It differs slightly from the first country dropdown as it also has an onChange event on the data source as well as an onChange event for the dropdown widget. 

The onChange event on this widgets data source validates that a state is returned for the selected country. If the state exists, this will fire off an event to enable and populate the cities dropdown that belong to the selected state. However, if a state does not exist, it will deactivate the state dropdown and populate the cities dropdown with the cities that belong to the country as some countries have cities, but not states (i.e. Vatican City). We will cover this dataSource event below in more detail.


The States Kendo Datasource

Other than the onChange method, this data source is similiar to the other data sources that we have already covered. We are simply passing the value of the selected countryId that was stored into the hidden form to the back end ColdFusion remote endpoint and calling the getStates method in the WorldCountries ColdFusion component. 

The onChange method requires more elaboration...

Here, we are validating the data to see if the country does not exist and changing the UI when the state does not exist for the selected country. When the state does not exist, we are enabling the cities menu calling the getCities method, and passing the countryId. This will extract any cities that may exist for the selected country, but not the state. Remember that a country may exist without states. 

If there are states for the selected country, we disable the city dropdown until a state has been selected. We are also disabling the city button (not the dropdown though) at the bottom of the page.

This optional validation step is indicated in step 3 of our methodology.

// -------------------------------- 2nd State Dropdown. --------------------------------

// ----------- State Datasource. -----------

// state populated by the first country dropdown.
var stateDs = new kendo.data.DataSource({
	transport: {
		read: {
			cache: false,
			// The function allows the url to become dynamic to append additional arguements.
			url: function() { return "<cfoutput>#application.baseUrl#</cfoutput>/demo/WorldCountries.cfc?method=getStates&countryId=" + $('#selectedCountry').val(); },
			dataType: "json",
			contentType: "application/json; charset=utf-8", // Note: when posting json via the request body to a coldfusion page, we must use this content type or we will get a 'IllegalArgumentException' on the ColdFusion processing page.
			type: "POST"
		}
	},
	// Function to enable the city dropdown when a state does not exist for a selected country. This will only fire when there is no data in the Kendo datasource and will enable the city dropdown in order for the user to still be able to pick a city. This is fired everytime there is a change to the state datasource
	change: function() {
		// Get the datasource length
		var data = this.data();
		// Set a var for the city dropdown list.
		var cityDropdown = $("#cityDropdown").data("kendoDropDownList");
		// If there are no states for this country...
		if (!data.length){
			// Disable the state dropdown
			stateDropdown.enable(false);
			// Now enable the city dropdown list.
			cityDropdown.enable();
			// Refresh the city dropdown
			cityDropdown.dataSource.read();
		} else {
			// Disable the city dropdown
			cityDropdown.enable(false);
		}//if (! data.length){..

		// Get the Kendo button at the end of the interface
		var cityButton = $("#cityButton").data("kendoButton");
		// Disable the the button
		cityButton.enable(true);

	}//change: function() {..
});//var stateDs...

Initializing the second state dropdown

Here, we are using the Id as the value of the dropdown and the state name as the label. We are also setting the autobind argument to false as we want to control when to fire off this dropdown based upon an event. This is necessary for all of the dependent dropdowns. Our change event is going to call another function that will save the selected value into the hidden form and we will cover that next. Finally, we are setting the state menu to select the first item in the list (select(0)) which will set the dropdown to display our 'Select State...' hint.

// Create the state dropdown list
var stateDropdown = $("#stateDropdown").kendoDropDownList({
	optionLabel: "Select State...",
	dataValueField: "id",
	dataTextField: "name",
	autoBind: false,
	enable: false,
	dataSource: stateDs,
	filter: "contains",
	change: onStateChange,
}).data("kendoDropDownList");//var stateDropdown...

// After the state is initialized, set the value to be the option label.
stateDropdown.select(0);

The States Widget onChange Event

Like all of the dropdowns here, when the user selects a state, we are simply enabling and firing off the next city dropdown menu and saving the selected stateId into a hidden form.

// ------------ onChange Event ------------
// Function to enable the last city dropdown menu
function onStateChange(e){
	// Get the next dropdown list.
	var cityDropdown = $("#cityDropdown").data("kendoDropDownList");
	// Save the id in a hiden form in order to get at it in the next dropdown
	var id = this.value();
	// If the user selected a value, enable and refresh the next dropdown list
	if (id != '') {
		$("#selectedState").val(this.value());
		// Enable the city dropdown list.
		cityDropdown.enable();
		// Refresh the city dropdown
		cityDropdown.dataSource.read();
	}//..if (id != '') 

}//function onStateChange(e)

The Last City Dropdown

The last city dropdown sends multiple arguments to the ColdFusion end-point. This sends both the selected country and the state to the getCities method in the WorldCountries.cfc ColdFusion component. This allows us to extract the cities by country or state. We won't have any duplicates here as both the state and the city will belong to the same country. Finally, we will set the state of the button at the bottom of the interface and enable the button if a city was found.

The City Kendo DataSource

// -------------------------------- Last City Dropdown. --------------------------------

// ----------- City Datasource. -----------

// City populated by the second state dropdown.
var cityDs = new kendo.data.DataSource({
	transport: {
		read: {
			cache: false,
			// The function allows the url to become dynamic to append additional arguements. Here we are sending both the countryId and the stateId. There are some countries that do not have states.
			url: function() { return "<cfoutput>#application.baseUrl#</cfoutput>/demo/WorldCountries.cfc?method=getCities&countryId=" + $('#selectedCountry').val() + "&stateId=" + $('#selectedState').val(); },
			dataType: "json",
			contentType: "application/json; charset=utf-8", // Note: when posting json via the request body to a coldfusion page, we must use this content type or we will get a 'IllegalArgumentException' on the ColdFusion processing page.
			type: "POST"
		}
	},
});//var cityDs...

The City Dropdown Initialization

The initialization approach is identical to the other widgets. Simply put, we are using the cityId as the value of the dropdown, the city name as the label, disabling autobind, and setting the default dropdown option to the optionLabel, 'Select City...'.

// ----------- City Dropdown. -----------
var cityDropdown = $("#cityDropdown").kendoDropDownList({
	optionLabel: "Select City...",
	dataTextField: "name",
	dataValueField: "id",
	autoBind: false,
	enable: false,
	filter: "contains",
	change: onCityChange,
	dataSource: cityDs
});//var cityDropdown...

// After the city is initialized, set the value to be the option label.
cityDropdown.select(0);

The Cities Dropdown onChange Event

Here, when a city is selected, we will save the selection into a hidden form and activate the city button at the bottom of the interface, otherwise, we will disable the button.

// ------------ onChange Event ------------
// Function to enable the button to launch a new window showing the details
function onCityChange(e){
	var id = this.value();
	// If the user selected a value, enable teh button at the end of the interface
	if (id != '') {
		// save the selected value in a hidden form
		$("#selectedCity").val(this.value());
		// Get the Kendo button at the end of the interface
		var cityButton = $("#cityButton").data("kendoButton");
		// Enable the button
		cityButton.enable(true);
	} else {
		// Disable the button at the bottom of the UI
		cityButton.enable(false);
	}//..if (id != '') {
}//function onStateChange(e)

// Create the last button.
var cityButton = $("#cityButton").kendoButton({
	enable: false
}).data("kendoButton");

Client Side HTML

We will wrap this article up with the client-side HTML. Here we have our hidden form elements that store the user's choices and the select elements that will be used for the Kendo DropDowns and disabling the children dropdowns. 

<!-- Hidden inputs to hold selected values. -->
<input type="hidden" id="selectedCountry" name="selectedCountry" />
<input type="hidden" id="selectedState" name="selectedState" />
<input type="hidden" id="selectedCity" name="selectedCity" />

<p>Select a country, state and city. Not all countries have states or cities, take Anguilla for example.<br/>
Note: in this example, the button at the end of the interface is for display purposes only.</p>
<table width="100%" class="k-content">
  <tr>
	<td align="left" valign="top" class="border" colspan="2"></td>
  </tr>
  <tr>
	<td align="right" style="width: 20%">
		<label for="countryDropdown">Country:</label>
	</td>
	<td>
		<!-- Create the country dropdown -->
		<select id="countryDropdown" name="countryDropdown" style="width: 95%"></select>
	</td>
   </tr>
   <tr>
	 <td align="left" valign="top" class="border" colspan="2"></td>
   </tr>
   <tr>
	<td align="right" style="width: 20%">
		<label for="stateDropdown">State:</label>
	</td>
	<td>
		<!-- Create the state dropdown -->
		<select id="stateDropdown" name="stateDropdown" style="width: 95%" disabled></select>
	</td>
  </tr>
  <tr>
	<td align="left" valign="top" class="border" colspan="2"></td>
  </tr>
  <tr>
	<td align="right" style="width: 20%">
		<label for="cityDropdown">City:</label>
	</td>
	<td>
		<!-- Create the state dropdown -->
		<select id="cityDropdown" name="cityDropdown" style="width: 95%" disabled></select>
	</td>
  </tr>
  <tr>
	<td align="left" valign="top" class="border" colspan="2"></td>
  </tr>
  <tr>
	<td></td>
	<td>
		<button id="cityButton" name="cityButton" class="k-button k-primary" type="button" disabled>View City Details</button>
	</td>
  </tr>
</table>