In this example, we will demonstrate how to create cascading menus' with Kendo MultiSelects. We have covered cascading menus with the dropdown lists, but there are fundamental differences.

Telerik has an example of how to create cascading dropdowns with MultiSelects, however, in my opinion, it is too complex and will suggest a simpler straightforward event-based approach. We will also briefly cover how to inspect the data coming from the parent dropdown data source and use the JavaScript push method to deselect MultiSelect values chosen by the user as well as using the Kendo DataSource to group city data in the menu.

In this example, I will try my best to replicate the UI of James Moberg's 'North America Search Demo' found at https://www.sunstarmedia.com/demo/countrystate/. James is a frequent contributor to the ColdFusion community and his elegant demo is used in production environments. Jame's demo uses a different library, Select2 UI, but it uses the same concepts that we want to show here.

Like the rest of the posts in this series, we will be using ColdFusion on the server side, however, it should be easily understood if you use a different server-side language, and you should be able to follow along.



Cascading MultiSelects Demonstration

 

Cascading MultiSelects Approach

We have already introduced this approach in our previous Cascading Country, State and City Kendo UI Dropdowns article and will briefly cover the major details.

Gregory's Approach to handle cascading dropdowns:

  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.

Differences in this approach when using the MultiSelect

  • For the most part, the cascading MultiSelect dropdowns are nearly identical to the cascading dropDownLists that we have already covered.
  • For the dependent multi-select dropdown, we are using different server-side logic in order to query the database allowing multiple choices for cities and countries. 
  • The major differences on the client side are that we are using multi-selects and passing comma-separated values to the server, and we are not disabling the multi-selects when there is a change in the state dropdown.
  • We are not using the Kendo DropDownList's select argument to set the default value of the Kendo MultiSelect when changes are made to the parent dropdowns. Instead, when a state is deselected; we need to dynamically recreate the values in the MultiSelect.

First Country Dropdown

The first dropdown is a Kendo DropDownList widget; I could have made it a multi-select, but the city list is already quite long even when a unique state is selected. I did not want to have a scenario where cities are shown for multiple states and multiple countries. This would negatively impact the performance of the UI and potentially crash the browser.

This dropdown is identical to the state dropdown in our Cascading Country, State, and City Kendo UI Dropdowns article. This dropdown list contains the flags of each country, if you are interested in how to accomplish this please see Adding the Country Flag to the DropDownList

Since we have already covered this dropdown, I will not share the code or elaborate on the details but will include the links to the original article below:

  1. Create the first dropdown
  2. Create The Country Dropdown Kendo Datasource
  3. Adding the Country Flag to the DropDownList
  4. Initialize the Country DropDownList
  5. The Country DropDownList onChange Event

Second State MultiSelect Dropdown

The state multi-select dropdown is dependent on the country dropdown and contains all of the states for the selected country and is a required field.

The user must select either one or more states. The state onChange event is used to fire another function to populate the cities into the next multi-select dropdown. The state multi-select dropdown is quite similar to the state dropdown-list, but has a few changes that I will note.


Server-Side Logic with ColdFusion

We are going to use the same method from the World.cfc that we discussed in our Using ColdFusion to Populate Kendo UI Widgets article. In a nutshell, this method queries the World, Countries City database and converts the ColdFusion query into a JSON string using our CfJson ColdFusion component. In this example, we are passing in the countryId from the country dropdown. We don't need to pass multiple values as the parent country dropdown is a Kendo DropDownList and not a MultiSelect.

Here is the server-side code:

<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, latitude, longitude 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="false">	
	</cfinvoke>

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

</cffunction>

The State Kendo DataSource

In this example, even though we are using a MultiSelect here instead of a DropDownList, the Kendo data source of the state dropdown is identical to the DropDownList state data source in our previous article. The only difference here is that in this example, the data-sources change method is used on rare occasions to enable and populate the cities dropdown when there is no state.

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

// First state 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("kendoMultiSelect");
		// If there are no states for this country...
		if (!data.length){
			// Now enable the city dropdown list.
			cityDropdown.enable();
			// Refresh the city dropdown
			cityDropdown.dataSource.read();
			// Note: we are not disabling the city MultiSelect in an else block if there is no data. Doing so would disable the city dropdown if you have made a new city selection.
		}//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...

// Note: in the cascading dropdown example, we are using the select event for the state dropdown to set the option to the default 'Select...' placeholder (stateDropdown.select(0)). However, this is not necessary here, and the select method does not exist for the multiSelect

Initializing the State MultiSelect

This is nearly identical to the state drop-down list example in our previous article. There are a few differences- we are substituting the string kendoMultiSelect for kendoDropdownList, using placeHolder instead of optionLabel for the hint. We are also calling functions via two MultiSelect events- the change event, which we have already covered, and a new deselect event that is specific to the Kendo MultiSelect.

Introducing Kendo MultiSelect Deselect Event

As we previously discussed in our prior article, every widget supports a certain set of events. Often, an event may be unique to a particular widget, such as the deselect event here with the Kendo MultiSelect.

All of the widgets supported events are documented on the Kendo UI site- search for the widget documentation and click on the API button.

Here, we need to use the deselect event to determine if any cities should be removed if the user deselects a given state. This event is fired every time a user removes one of the multi-select elements that they have previously chosen. We will use this event to remove any chosen cities when the parent state is deselected by the user. In this example, once a user deselects a state we will call the onStateDeselect function which will be covered later in this article.

// Create the state dropdown list
var stateDropdown = $("#stateDropdown").kendoMultiSelect({
	optionLabel: "Select State...",
	dataValueField: "id",
	dataTextField: "name",
	autoBind: false,
	dataSource: stateDs,
	filter: "contains",
	change: onStateChange,
	// Deselect event to remove cities if the parent state was removed by the user
	deselect: onStateDeselect,
}).data("kendoMultiSelect");//var stateDropdown...

State onChange Method

Just like the onChange method for the Kendo drop-down list, this method saves the selected values into the cityDropdown hidden form field and enables and refreshes the data in the next city multi-select.

/ ------------ onChange Event ------------
// Function to enable the last city dropdown menu
function onStateChange(e){
	// Get the next dropdown list.
	var cityDropdown = $("#cityDropdown").data("kendoMultiSelect");
	// 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 Optional State onStateDeselect Function

This function is used to perform clean-up operations when a state is deselected from the state multi-select.

In our last cascading dropdown article, we reverted all of the child dropdowns to the initial value when a parent dropdown was changed (see the 'stateDropdown.select(0)' line at the very end of the code example of our previous Initializing the second state dropdown article).

Here, we need to do something similar when a state is deselected. For example, let's say a user selects the state of New York and California- and selects the city of New York and Los Angeles. If the user then deselects the state of New York, we will want to also remove the city of New York. However, we will leave the city of Los Angeles as the state of California was not deselected. 

The best way to understand what is going on is to interact with the demonstration at the top of this article. Select at least two states and at least one city from each state. After making the selections, deselect one of the states and the associated cities that you selected will also automatically be deselected as well.

To perform this cleanup, we are going to use JavaScript to inspect the data coming back from the server and dynamically re-populate the Kendo MultiSelect. Don't worry about the details of this code yet- we are going to cover this in detail in our next article- just note what the purpose of this code is.

This step is completely optional. If you don't want the complexity of this code you can simply remove the cities whenever something is deselected by keeping the cities in the list or by setting the values of the multi-select to null. See the code below.


// ------------ onDeselect Event ------------
function onStateDeselect(e) {
	/* 
	Inspect the transformed JSON JavaScript object from the datasource to determine if we should eliminate certain cities that belonged to the deselected state
	*/

	// Determine what was just deselected
	var dataItem = e.dataItem;

	// Note: the dataValueField and dataTextField are available in this struct 
	var deselectedStateId = dataItem.id;
	var deselectedState = dataItem.name;

	// Get any cities that were selected
	var selectedCities = $("#selectedCity").val();

	// Get a reference to the city dropdown. We will use this in the loop below to set its items.
	var cityDropdown = $("#cityDropdown").data("kendoMultiSelect");

	// Now get a reference to the city dropdown datasource. Here we are setting the the datasource using dropDownVarName.datasource. Note: if this code is not inside of a document ready block, you can use the var that was created when creating the datasource. In this case it would be cityDs. However, if this is inside of a ready block, you will get a 'Cannot read properties of undefined' error. This is always a factor when using code inside of a document ready block.
	var cityDs = cityDropdown.dataSource;
	// Get the underlying datasource data. Note, using the cityDs variable that we created above won't work- we need to use the full chained name here. 
	var cityData = cityDropdown.dataSource.data(); 
	// Clear the previous values in the multiSelect
	cityDropdown.value([]);

	// Fetch the data from the cityDs Kendo datasource. We can also use the read method
	cityDs.fetch(function(){
		// Create an array in order to populate multiple values 
		var cityIdList = [];
		// Loop through the data to create an array to send to the city multi-select
		for (var i = 0; i < cityData.length; i++) {
			// Get the cityId
			var cityStateId = cityData[i].state_id;
			var cityId = cityData[i].id;
			// Check to see if the stateId matches the deselected state when the selected city is found in the datasource (listFind Javascript function: listFind(list, value, delimiter))
			if (listFind(selectedCities,cityId) > 0){
				// If the city does not reside in the state that was just deselected, add it to cityIdList array using the JS push method 
				if (cityStateId != deselectedStateId){
					// Use the Javascript push method to push the city into our cityIdList array that we will use to repopulate the MultiSelect.
					cityIdList.push(cityId);
				}
			}
		}//..for (var i = 0; i < cityIdList.length; i++) 
		// Repopulate the multiselect
		cityDropdown.value(cityIdList);
	});//..cityDs.fetch(function(){
}

Alternative Deselect Function to Remove All of the Selected MultiSelect Options on Deselect

To remove all of the selected options in the Kendo MultiSelect, simply use the following code:

// ------------ onDeselect Event ------------
function onStateDeselect(e) {
	/* This function will remove all of the cities selected by the user by setting the MultiSelect value to null */
	// Get a reference to the city dropdown. 
	var cityDropdown = $("#cityDropdown").data("kendoMultiSelect");
	// Set the value to null (with an empty array)
	cityDropdown.value([]);
}

The Dependent City MultiSelect Dropdown

This multi-select is dependent upon the chosen values in the country dropdown and the state multi-select. This example has some very minor differences from the first state multi-select. One of the main differences is that since the user can select multiple states with the multi-select instead of just one when using the drop-down list, the ColdFusion logic on the server side must change to allow for multiple states to be passed in. We are also going to introduce Datasource grouping.

Introduction to Grouping Using the Kendo DataSource

When using a widget that allows the user to select multiple values, like the MultiSelect, it is nice to be able to group the data of the next dependent dropdown by the selected values. Nearly every Kendo Widget, including the MultiSelect, has the ability to group data. You are not limited to grouping the data by the dropdown value or the label- you can use any field in the JSON that is returned by the server. 

To group data, simply add the field to the JSON from the server and add that field to the Kendo UI group argument in the Kendo data source. You don't need to use grouping in your SQL- the only thing that you need is the JSON string to return the field that you want to group by, add the field that you want to group by, and the Kendo data source will perform the grouping for you.

Here, we will add grouping to group all of the cities by the chosen states.


Grouping the City MultiSelect Dropdown by the Selected State

If you want to group the city MultiSelect by the states, we need to:

  1. Add the state name column to the ColdFusion query that generates the JSON
  2. Add the new field to the group argument in the Kendo DataSource.

Server Side Logic using ColdFusion

Adding the State Name Column

To group by the state, the state field must be returned in the JSON. We are going to change the query in the getCities method in the World component on the server and join the country table to add the state name.

For Kendo MultSelects, the Query Clause Must Also Support Multiple Values

The state MultiSelect widget will either pass one or more numeric values, separated by commas when a selection is made. This selection is saved into the selectedCity hidden form and passed along using the Kendo DataSource.

This ColdFusion function takes these comma-separated values and validates the list using the justNumericList custom function to ensure that the values are numeric and do not have any empty values. This validation step is necessary as the database query will fail if there are empty or non-numeric values.

There are many ways to accomplish this, but in this example, I chose to use the justNumericList function as it was modified by James Moberg, and had initially assumed that he modified this function for his own cascading dropdowns that we are replicating. However, after creating the logic for this article, I found out that he is using this function for different purposes. 

Once the comma-separated stateIdList string has been validated, we will query the database with these values using the SQL IN keyword. Once the query has been made, the ColdFusion Query Object will be transformed into JSON using the convertCfQuery2JsonStruct method found in the CFJson component.

<cffunction name="getCitiesByStateIdListGroupByState" access="remote" returnformat="json" output="true"
		hint="Gets the world cities using a stateId list. This is a separate function to keep it isolated from the getCities function">
	<cfargument name="stateIdList" type="string" required="true" default="" />

	<cfset validatedNumericList = justNumericList(arguments.stateIdList)>

	<cfquery name="Data" datasource="cityDb">
		SELECT states.id as state_id, states.name as state, cities.id, cities.name, cities.latitude, cities.longitude 
		FROM cities
		INNER JOIN states 
		ON states.id = cities.state_id
		WHERE 0=0
		AND cities.state_id IN (<cfqueryparam value="#validatedNumericList#" cfsqltype="integer" list="yes">)
		ORDER BY cities.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 City Kendo DataSource

The Kendo DataSource takes the values from the hidden selectedState form field and passes the comma-separated list to the getCitiesByStateIdList method. We are also grouping the data by the state name using group: { field: "state" }. This will group the data by the state in the dropdown.

This grouping is more effective when the data is not as extensive. When there are scores of cities for each state, like in this example, you will have to scroll quite a bit if the state is not the first entry in the list. 

// 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=getCitiesByStateIdListGroupByState&stateIdList=" + $('#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"
		}//read..
	},//transport..,
	// Group by the state
	group: { field: "state" }
});//var cityDs..

Initializing the City MultiSelect 

This function is fundamentally the same as city dropDownList that we have previously covered, however, the string kendoDropDownList is replaced with kendoMultiSelect, and optionLabel is replaced with placeHolder. Also, we will not use the select argument and the end of the initialization to default the multi-select to the first value in the list (the multi-select does not have an initial value and thus does not support the select argument).

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

// Note: the select method does not exist for the multiSelect

The City onChange Method

The city onChange method simply saves the selected cities into the selectedCity hidden form and enables the button at the bottom of the UI once a selection has been made.

// ------------ 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

The client-side HTML is straightforward. Here we have 3 hidden form fields: selectedCountry, selectedState, and the selectedCity. All of these hidden fields are populated by the widget's respective onChange events. The countryDropdown, stateDropdown, and cityDropdown fields contain the Kendo UI widgets, and the 'View City Details' button at the end of the UI in this example is for visual purposes.

<table width="100%" class="k-content">
  <input type="hidden" name="selectedCountry" id="selectedCountry" value=""/>
  <input type="text" name="selectedState" id="selectedState" value=""/>
  <input type="hidden" name="selectedCity" id="selectedCity" value=""/>
  <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/Province:</label>
	</td>
	<td>
		<!-- Create the state dropdown -->
		<select id="stateDropdown" name="stateDropdown" 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="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>

Further Reading