Cascading Country, State and City Kendo UI Dropdowns
Sep 16 |
In this article, I will show you how to create sophisticated cascading Kendo Dropdown menus based on selecting 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 I have learned 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 the ease of use when using the search widget to select a value from a long list of items quickly. These comprehensive dropdowns will contain all the countries, states, and major cities worldwide.
We will also review various Kendo events and change the UI based on the number of records returned in the JSON object.
I will also provide the database files and all the code on GitHub for others to use. The Kendo UI-related code will be compatible with the open-source version of Kendo, so you can use it in your own projects and potentially distribute your code.
Like the rest of this series of Kendo articles, we will use ColdFusion as our service endpoint. However, if you use another server-side language, you should be able to follow along.
Table of Contents
- Cascading Country State and City Cascading Dropdowns Example Application
- Our Cascading Kendo Dropdowns Methodology
- Introducing Kendo UI Events
- Creating the Countries - States - Cities Database
- Creating the back-end ColdFusion Service Endpoint
- The First Country Dropdown
- The Second State Dropdown
- The Last City Dropdown
- Client Side HTML
Cascading Country State and City Cascading Dropdowns Example Application
We will demonstrate the Kendo Dropdowns with a cascading country, state, and city dropdown. The first dropdown will display all of the world's countries, 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 had difficulties with it. It is more complex and problematic than saving the required elements 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 the document-ready scope. It is also more difficult to pass in multiple arguments to the Kendo data source 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 I prefer an approach that allows for better separation of logic.
Gregory's approach:
- Initialize the dropdowns as you usually would- but add an onChange method.
- Use the drop-down widgets onChange method to:
- Capture and store the user's selection into a hidden form field.
- Refresh any dependent dropdowns using the widgets refresh method.
- The data sources belonging to the dependent dropdowns will pass the necessary values in the hidden form fields to the remote end-point.
- If necessary, use the DataSource's onChange method to:
- dynamically populate the values of a dependent MultiSelect
- or validate the data and perform any additional logic.
We will cover step 4 in a future article.
I want to add a rule to keep the Kendo dropdown and data source separate for better separation of logic and code reuse, but this is not strictly necessary.
This purely state-driven approach to connecting the menus is reliable, and I have used it in production environments for the last ten years.
Introducing Kendo UI Events
Every widget has several events. There are traditional events 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. Kendo's abstractions, such as the Kendo dataSource, also have their 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 user's selected values into hidden form fields and trigger the dependent dropdowns. We will also use the Kendo dataSource to make changes in the UI.
Creating the Countries - States - Cities Database
We will use the open-source Countries- States and Cities GitHub repository created by Darshan Gada. This repository is updated several times yearly and contains all the necessary files to build 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, 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 pretty 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 takes no arguments but 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 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 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 countries from the ColdFusion remote endpoint. Nothing is inherently different than the previous Introducing the various forms of Kendo Dropdowns in the data source or widget initialization.
This differs because 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 invokes the WorldCountries.cfc getCountries method which returns a JSON object with the countryId and country of all the countries in the world. There is nothing out of the norm 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
Adding the country flag to the dropdown list using a Kendo Template is relatively trivial. 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 the flags' names are identical to the ISO2 names 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 a Kendo Template. Here, I used 20px14px, which matches the 10x7 ratio of the .png files, which 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 render correctly.
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 dropdown's value and 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 called 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 will 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 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 and an onChange event for the dropdown widget.
The onChange event on this widget 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 city dropdown that belongs 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
This data source is similar to the others we have already covered, except for the onChange method. We are simply passing the value of the selected country stored in 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 validate the data to see if the country does not exist and change the UI when the state does not exist for the selected country. When the state does not exist, we enable the cities menu by calling the getCities method and passing the country. This will extract any cities 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 also disable 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 auto bind argument to false as we want to control when to fire off this dropdown based on an event. This is necessary for all of the dependent dropdowns. Our change event will 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 simply enable and fire off the next city dropdown menu and save 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 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 duplicates here as 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 is 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
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's 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>
Related Entries
Tags
ColdFusion and Kendo UIThis entry was posted on September 16, 2022 at 10:08 PM and has received 2581 views.