In this article, we will cover advanced grid functionality and show you how to create an editable grid with cascading dropdowns with a custom header template. Kendo Templates allow us to essentially place widgets inside of a grid widget. Simply put- using these Kendo UI templates allows us to use the grid as a container to hold other Kendo UI widgets.

Like our other articles, we will be using ColdFusion as the backend- however, as we have stated before, Kendo UI is server agnostic and you can use the same techniques learned here with other server-side technologies. 

Please see the example below to see a live demonstration of this code. The code is exhaustive, and we will do our best to cover all essential logical elements here.


Realtime Example

This example is a proof of concept intended to be used by the administrator to determine what categories are used for the CfBlogs Blog Aggregator found at cfblogs.org. The domain dropdown specifies a broader category type, here I am using Lucee and ColdFusion. The category domain is part of the RSS 2 specification and it is an optional category element.  This optional element is rarely used, however, it will allow the Cfblogs logic to potentially determine which subcategory to be used when aggregating the posts.

If the category domain is 'ColdFusion' in this example, the administrator is allowed to select any category that exists in the CfBlogs database. I am grabbing all of the RSS categories when aggregating ColdFusion-related posts; there are thousands of them. If the user selects 'Lucee', I am defaulting to the subcategories found in the Lucee documentation. Once the domain and subcategories have been selected, the user can save the data to the database (however, this functionality is turned off in this example).

To help the users find the blog, there is a custom search engine provided at the top of the page.

Please click on the button below to see the demonstration. This article is quite extensive, you may want to click on the code button and put the window aside to follow along.

Note: this demonstration does not update the database. It is used for demonstration purposes only.




Using Kendo Templates to Embed Cascading Dropdowns in a Kendo Grid


Introducing Kendo Templates

As we mentioned in previous articles, most of the Kendo Widgets support using templates to extend the widget functionality. The Kendo template is similar to other JavaScript template engines, such as Angular, and is often used to bind data from the server. A template can include logic to display logic or use other JavaScript objects. For more information regarding the Kendo Templates see https://docs.telerik.com/kendo-ui/framework/templates/overview.

You can use a template for nearly every display property in a Kendo Grid. Some of the more popular temples in the Kendo Grid are the row and detail templates. In this example, we will apply a custom toolbar template at the top of the grid instead to apply custom search functionality along with buttons to save and export the data as well as adding a Kend Grid columns.template to embed the URL to the blog name found in the blog column.


Server-Side Queries to Obtain the Data Used in the Dropdowns

The following ColdFusion queries are used to prepare the data for the cascading dropdowns. The getCategoryDomain merely captures two records with the ID and the Category Domain, which are 1 and 3, and 'ColdFusion' and 'Lucee' in this case. The getCategory query captures the categories associated with the Domain Category Id (for example 'AJAX'). 


<cfquery name="getCategoryDomain" datasource="#dsn#">
	SELECT 
		category_domain_id,
		category_domain
	FROM category_domain
	WHERE category_domain <> 'Kendo UI'
	ORDER BY category_domain
</cfquery>

<cfquery name="getCategory" datasource="#dsn#">
	SELECT 
		category_id,
		category_domain_ref as category_domain_id,
		category
	FROM category
	ORDER BY category
</cfquery>

Changing Style Properties of the Kendo Grid

Use the k-grid class to change the font and the height of the rows. The k-grid class is quite extensive and can also be used for other properties as well. Be aware if you use the k-grid class, it will affect the display of all of the grids on the page.


<style>
	.k-grid {
		font-size: 12px;
	}
	.k-grid td {
		line-height: 2em;
	}
</style>

Empty DIV container to Hold the Kendo Grid

Nearly all of the Kendo Widgets need to have a DIV element to contain the elements of the widget. You can create a static DIV like we have done here, or create the DIV dynamically as we have done when creating a dynamic Kendo Window.


<!--- Empty iv container for the grid. ---> 
<div id="feedsGrid"></div>

Kendo Grid Custom Header


Our feedGridToolbar custom header has buttons to export the grid data to Excel and PDF and has a custom search interface to allow the user to quickly find records. 

External Kendo Templates

In this example, we will use a Kendo external template. These templates are embedded in JavaScript with the type of 'text/x-kendo-template'. 


Custom Export PDF and Excel Buttons

It should be noted that the "k-button k-button-icontext k-grid-pdf" and "k-button k-button-icontext k-grid-excel" classes are used to create custom buttons to invoke Kendo's native saveAsPdf and saveAsExcel methods. Clicking on these buttons will either save the contents of the grid to PDF or Excel. Other helpful custom classes that you can apply to custom buttons are:

  • k-grid-add creates a button with a plus and will invoke the addRow method
  • k-grid-edit fires the edit method to edit the rows within the grid
  • k-grid-cancel cancels any edits in progress

Note: typically, if you don't want a custom toolbar, you can simply add the following code in the Kendo grid initialization to embed export to PDF and Excel capabilities along with a save button and search functionality.

toolbar: ["pdf", "excel", "save", "search"],

This will replicate the functionality that we have provided using the custom toolbar templates.

Note: the default search input functionality only works with 2019 R3 2019.3.917 release and greater.


Implementing a Custom Kendo Grid Search Interface 

We are using custom JavaScript logic to handle the search interface. It should be noted that built-in search functionality is already baked into the grid that filters records on the client, however, there are advantages when using a customized search interface that returns search results as we are doing here.

When the search button is clicked, the onFeedsGridSearch() method will be invoked which will query the database on the server using the entered search string to retrieve the relevant records. The circular refresh button calls the refreshFeeds() method which will refresh the grid. We will cover these two methods below.


<!--- Grid toolbar template --->
<script type="text/x-kendo-template" id="feedsGridToolbar">
	<div class="toolbar" style="margin: auto; float: left;">
		<!--- Default Kendo UI buttons for PDF and Excel export. Note: when using ColdFusion, we need to escape any pound symbols in the template with a backslash --->
		<a class="k-button k-button-icontext k-grid-pdf" style="margin: auto;" href="#"><span class="k-icon k-i-pdf"></span>Export to PDF</a>
		<a class="k-button k-button-icontext k-grid-excel" id="feedGridExcelExport" href="#"><span class="k-icon k-i-excel"></span>Export to Excel</a>
	</div>

	<span class="toolbar" style="margin: auto; float:right; <cfif not session.isMobile>padding-right:10px;</cfif>">

		<!--- Search ---> 
		<label class="category-label" for="feedsGridSearchField">Search:</label>
		<input type="text" id="feedsGridSearchField" class="k-textbox" style="width: <cfif session.isMobile>200<cfelse>400</cfif>px; padding :5px;"/>
		<a href="javascript:onFeedsGridSearch();" aria-label="Search" class="k-link k-menu-link"><span class="fa fa-search" style="font-size:1em; padding :5px;"></span></a>

		<cfif not session.isMobile>
		<!--- Refresh --->
		<a href="#" class="k-pager-refresh k-link k-button k-button-icon" title="Refresh" onClick="refreshFeedsGrid();" style="padding :5px;"><span class="k-icon k-i-reload" onClick="refreshBlogCategoryGrid();"></span></a>
		</cfif>
	</span>
</script>

Custom Search JavaScript Methods used by the Grid


The onFeedsGridSearch Method

This method simply takes the string that was entered by the user and passes it to the createSearchFilter method. These two methods can be consolidated, however, I separated them into two distinct methods as I may want to use the createSearchFilter method in other client-side interfaces to modify the grid.


The createSearchFilter method

This method takes a search term string and applies the filters to send it to the Kendo data source. The Kendo filters accept an array of items, and we will populate the new filter array using jQuery's push method. After the filter array is populated, we will apply it to the Kendo grid data source that is used to repopulate the Kendo grid.


function onFeedsGridSearch(){
	// Extract the search term
	var searchTerm = $("#feedsGridSearchField").val();
	// Invoke the createSearchFilter function
	createSearchFilter(searchTerm);
}//function

// Grid filters
function createSearchFilter(searchTerm){
	// Get a reference to the grid.
	var grid = $("#feedsGrid").data("kendoGrid");
	// Instantiate the filter object as an array
	$filter = new Array();
	// Build the filters with the search term
	if(searchTerm){
		// Populate the array of filters
		$filter.push({ field: "name", operator: "contains", value: searchTerm });
		$filter.push({ field: "description", operator: "contains", value: searchTerm });
		$filter.push({ field: "url", operator: "contains", value: searchTerm });
		// Refresh the data with the new filters.  
		grid.dataSource.filter({logic: "or", filters: $filter}); 
	}//if
}//function

Manually Refreshing the Kendo Grid

The circular button on the right of the custom header allows the user to manually refresh the grid. Here we are clearing the search input using jQuery, removing any previously applied filters, refreshing the data source using the datasource read method, and refreshing the Kendo grid.


function refreshFeedsGrid(){
	// Clear any prevous search term in the search input
	$("#feedsGridSearchField").val('');
	// Remove the filters
	$("#feedsGrid").data("kendoGrid").dataSource.filter({});
	// Refresh the datasource
	$("#feedsGrid").data("kendoGrid").dataSource.read();
}

The Logic for the First Cascading Category Domain Kendo Dropdown


Create the Local JavaScript JSON Variable for the Parent Dropdown

For performance reasons, all cascading dropdowns within a grid should use local binding. You can use server-side binding, however, since the grid can contain thousands, or potentially millions of records, the performance would be very slow and the interface would appear to be buggy.

For the parent category domain dropdown, we are creating a local JavaScript JSON variable to hold the dropdown values. Currently, this is either ColdFusion or Lucee. The output of this JSON is "var categoryDomainDataArray = [{ "category_domain_id":1,"category_domain":"ColdFusion"},{ "category_domain_id":2,"category_domain":"Lucee"}];"


/*  Static data arrays are needed for the dropdowns within a grid. Putting the dropdown data in a datasource declaration is not sufficient as it loads the data from the server on every click making the dropdowns within a grid very slow. */
var categoryDomainDataArray = [<cfoutput query="getCategoryDomain">{ "category_domain_id":#category_domain_id#,"category_domain":"#category_domain#"}<cfif getCategoryDomain.currentRow lt getCategoryDomain.recordcount>,</cfif></cfoutput>];
//Note: this local js variable is not declared as a separate datasource for efficiency. When the grid is refreshed via the read method, having this in it's own datasource is problematic with large datasets.

Create a JavaScript Function to Initialize the Parent Kendo Dropdown

If you have been following our previous Kendo UI articles, you should recognize that the code to initialize the cascading dropdowns is quite similar. The main difference is that the initialization here is wrapped inside a JavaScript function. You should note that the serverFiltering argument is set to true which typically means that the filtering takes place on the server- however, this is not the case as our dataSource is using the local JavaScript JSON array that we just created. We are also using an onClose and onChange to invoke the onDomainChange method which is used to populate our child category dropdown.


/* This function is used by the template within the domain column in the grid */
function domainCategoryDropDownEditor (container, options) {
	$('<input required data-text-field="category_domain" data-value-field="category_domain_id" data-bind="value:' + options.field + '"/>')
	.appendTo(container)
	.kendoComboBox({
		serverFiltering: true,
		placeholder: "Select domain...",
		dataTextField: "category_domain",
		dataValueField: "category_domain_id",
		dataSource: categoryDomainDataArray,
		// Invoke the onDomain change methd to filter the category when the domain has been selected.
		close: onDomainChange,
		change: onDomainChange
	});//...kendoComboBox
}//...function

Create a Function to Display the Initial Values in the Parent Domain Category Dropdown

This function will populate the initial category domain values in the grid (which will be ColdFusion or Lucee). The getCategoryDomain function simply loops through the categoryDomainArray JSON that we created above and compares the domainId to the category_domain_ref returned from the server by the Kendo DataSource. When the two values match, it returns the value back. 


// Function to display the initial domain in the grid.
// !! Notes: 1) the categoryDomainRef is coming from the query on the server that is populating the grid, 2) the dropdown functions and it's datasource must be outside of the document.ready scope.
function getCategoryDomainDropdown(category_domain_ref) {
	// Set the default var.
	var domain = '';
	// Loop thru the local js variable.
	for (var i = 0, length = categoryDomainDataArray.length; i < length; i++) {
		if (categoryDomainDataArray[i].category_domain_id == category_domain_ref) {
			// Set the global labelValue var in order to return it properly to the outer function. This should either be null or the proper value.
			domain=categoryDomainDataArray[i].category_domain;
		}//if (categoryDomainDataArray[i].category_id == category_id)...
	}//for...
	return domain;
}//...function

Create the Parent Domain Category Filter

The following function is used when filtering the data by clicking on the domain category column in the grid. This function indicates that the filter should search our categoryDomainArray and should return the category_domain for the dropdown label and set the category_domain_id for the value of the field. Note: this step is only needed when you explicitly set the grid's filterable argument to true.


// Filter for the dropdown. Note: the filterMenuInit event is raised when the filter menu is initialized.
function categoryDomainDropdownFilter(element) {
	element.kendoComboBox({
		serverFiltering: true,
		dataSource: categoryDomainDataArray,
		filter: "contains",
		dataTextField: "category_domain",
		dataValueField: "category_domain_id"
	})//...element.kendoComboBox;
}//...function categoryDomainDropdownFilter(element)

Create the Parent onChange Function

The onDomainChange JavaScript function will get a reference to the category child dropdown and filter the category by the categoryDomain that was selected by the user. Here we are using the contains operator, which works in this case as every domainId is unique. If the IDs were not unique, we would change the operator to "equals".


// On change function that will filter the category dropdown list if a domain was selected.
function onDomainChange(e){
	// Create a reference to the next dropdown list.
	var category = $("#category").data("kendoComboBox");
	// Filter the category datasource
	category.dataSource.filter( {
	  field: "category_domain_id", //  
	  value: this.value(),
	  operator: "contains"
	});
}//function onDomainChange(e)...

The Logic for the Last Child Cascading Category Kendo Dropdown

For the child cascading menu, we are going to repeat all of the steps used to create the first category domain dropdown that we just performed, however, we can omit the last onChange step unless there is another dependent child dropdown, which is not the case here. Much of our logic is identical, but I will identify the main differences.


Create the Local JavaScript JSON Variable for the Child Dropdown

The logic here is nearly identical to the domainCategoryArray that we created. The main difference is that we are also including the category_domain_id, along with the category_id and the category. For any child dropdowns, you must include a value that is found in the parent dropdown in order to associate the dropdowns. Here, the category_domain_id will be the association that we will use to filter this child dropdown.


var categoryArray = [<cfoutput query="getCategory">{ "category_domain_id": #category_domain_id#, "category_id": #category_id#, "category":"#category#"}<cfif getCategory.currentRow lt getCategory.recordcount>,</cfif></cfoutput>];

Create a JavaScript Function to Initialize the Kendo Child Dropdown

The ComboBox initialization is nearly identical to its parent, here we are assigning the category as the dropdown label and the category_id as the dropdown value. We are also omitting the onChange logic as this is the last dependent dropdown. If there are other child dropdowns dependent upon the selection of this value, the onChange logic must be included.


// This function is invoked inside of the grid template to populate the initial values for the first category dropdown. This dropdown is independent of the 2nd category dropdown
function categoryDropDownEditor(container, options) {
	$('<input id="category" required data-text-field="category" data-value-field="category_id" data-bind="value:' + options.field + '"/>')
	.appendTo(container)
	.kendoComboBox({
		placeholder: "Select category...",
		dataTextField: "category",
		dataValueField: "category_id",
		dataSource: categoryArray,
	});//...kendoComboBox
}//...function

Create a Function to Display the Initial Values in the Category Child Dropdown

Again, this logic is identical to the parent other than we are using the category_id and category instead of the domain category information. 


function getCategoryDropdown(category_id) {
	// Set the default var.
	var category = '';
	// Loop thru the local js variable.
	for (var i = 0, length = categoryArray.length; i < length; i++) {
		if (categoryArray[i].category_id == category_id) {
			// Set the global labelValue var in order to return it properly to the outer function. This should either be null or the proper value.
			category=categoryArray[i].category;
		}//if (categoryArray[i].category_id == category_id)
	}//for...
	return category;
}//...function

Create the Child Category Filter

This is identical to the parent menu other than using the category_id and category that will be used when the user filters the data. This code is not necessary if the filterable argument was not explicitly set to true in the grid declaration.


function categoryDropdownFilter(element) {
	element.kendoComboBox({
		dataSource: categoryArray,
		filter: "contains",
		dataTextField: "category",
		dataValueField: "category_id",
		cascadeFrom: "category"
	})//...element.kendoComboBox;
}//...function categoryDropdownFilter(element)

Wrap the Kendo DataSource and Grid Initialization with a Document Ready Block

Most of the Kendo Widgets, especially the Kendo Grid, should be placed in jQuery's document-ready scope in order for the widgets to function. However, you must also place any universal JavaScripts that need to be invoked outside of the document-ready scope, otherwise, your JavaScripts will not be available outside of the ready block. Typically I wrap the ready block around the Kendo DataSource and the widget initialization code and keep all other scripts outside of the ready block. If your Kendo Grids are not initializing, this is one of the first things that I check and I will always try to place a comment at the end of the ready block.


$(document).ready(function(){
... code goes here
});//document ready

Create the Kendo DataSource

The Kendo DataSource here is nearly identical to the data source that we created in our last article showing you how to create the data source for an editable grid. The only difference is that here we are wanting to return information to create the category domain and category. There is nothing unique that we need here in order to use cascading dropdowns. Please see https://gregoryalexander.com/blog/#mcetoc_1gkr406c31 for more information.


// Create the datasource for the grid
feedsDs = new kendo.data.DataSource({
	// Determines which method and cfc to get and set data.
	transport: {
	   read:  {
			url: "/blog/demo/Demo.cfc?method=getDemoFeeds", // the cfc component which processes the query and returns a json string. 
			dataType: "json", // Use json if the template is on the current server. If not, use jsonp for cross domain reads.
			method: "post" // Note: when the method is set to "get", the query will be cached by default. This is not ideal. 
		},
		// The create method passes the json like so: models: [{"id":"","name":"","description":"sadfas","url":"","blogsoftware":"","demo_notes":"asdfa","demo_active":false}]
		create: {
			url: "/blog/demo/Demo.cfc?method=saveBlogReqestViaGrid&action=insert",
			dataType: "json",
			method: "post"
		},
		// The update function passes all of the information in the grid in a models JSON string like so: [{"total":147,"demo_notes":"","description":"Jochem's tech exploits due to fork","blogsoftware":"http://wordpress.org/?v=2.7","recentposts":0,"id":48,"rssurl":"http://jochem.vandieten.net/feed
","demo_description":"Jochem's tech exploits","demo_active":false,"url":"http://jochem.vandieten.net
","name":""it could be bunnies""}]
		update: {
			url: "/blog/demo/Demo.cfc?method=saveBlogReqestViaGrid&action=update",
			dataType: "json",
			method: "post"
		},	
		destroy: {
			url: "/blog/demo/Demo.cfc?method=saveBlogReqestViaGrid&action=delete",
			dataType: "json",
			method: "post"
		},	
		// The paramterMap basically strips all of the extra information out of the datasource for the grid display. YOu must use this when you are using an editible Kendo grid otherwise strange behavior could occur.
		parameterMap: function(options, operation) {
			if (operation !== "read" && options.models) {
				return {models: kendo.stringify(options.models)};
			}
		}
	},
	cache: false,
	batch: true, // determines if changes will be send to the server individually or as batch. Note: the batch arg must be in the datasource declaration, and not in the grid. Otherwise, a post to the cfc will not be made. 
	pageSize: 10, // The number of rows within a grid.
	schema: {
		model: {
			id: "id", // Note: in editable grids- the id MUST be put in here, otherwise you will get a cryptic error 'Unable to get value of the property 'data': object is null or undefined'
			fields: {
				// We are using simple validation to require the blog name, desc, url and rss url. The other fields are not required. More elaborate validation examples will be provided in future blog articles. It is somewhat counter intuitive IMO that these validation rules are placed in the Kendo DataSource. 
				name: { type: "string", editable: true, nullable: false, validation: { required: true } },
				description: { type: "string", editable: true, nullable: false, validation: { required: true } },
				url: { type: "string", editable: true, nullable: false, validation: { required: true } },
				rss_url: { type: "string", editable: true, nullable: false, validation: { required: true } },
				site_image: { type: "string", editable: true, nullable: false, validation: { required: false } },
				// Note: the following two cascading menu fields need to have a default value 
				domain_category: { type: "string", editable: true, nullable: false, validation: { required: false }, defaultValue: 0 },
				// The category is dependent upon the domain category. Each category has a domain category id
				category: { type: "string", editable: true, nullable: false, validation: { required: false }, defaultValue: 0 },
				request_approved: { type: "boolean", editable: false, nullable: false, validation: { required: false } },
			}//fields:
		}//model:
	}//schema
});//feedsDs = new kendo.data.DataSource

Initialize the Kendo Grid


Like the Kendo DataSource, the grid initialization is quite similar to the grid initialization in our previous article, However, this script calls the CfBlogs Eernal Kendo Template that we created at the beginning of this article for the header. The category_domain and category columns in the grid have extra logic required to make our cascading dropdowns.

Handling the Parent category_domain Dropdown

For our first domain_category dropdown, there are three settings that are unique to the dropdown.


Handling the Child Category Dropdown

$("#feedsGrid").kendoGrid({
	dataSource: feedsDs,
	// Edit arguments
	editable: "inline", // use inline mode so both dropdownlists are visible (required for this type of cascading dropdown)
	// Header 
	headerTemplate: 'CfBlogs',
	// Toolbars. You can customize each button like the excel button below. The importExcel button is a custom button, and we need to wire it up to a custom handler below.
	toolbar: kendo.template($("#feedsGridToolbar").html()),
	excel: {
		allPages: true
	},
	// General grid elements.
	height: 660,// Percentages will not work here.
	filterable: true,
	columnMenu: true,
	groupable: true,
	sortable: {
		mode: "multiple",
		allowUnsort: true,
		showIndexes: true
	},
	allowCopy: true,
	reorderable: true,
	resizable: true,
	pageable: {
		pageSizes: [15,30,50,100],
		refresh: true,
		numeric: true
	},
	columns: [{
		// Columns
		field:"id",
		title: "I.D.",
		hidden: true,
		filterable: false
	}, {
		field:"name",
		title: "Blog",
		filterable: true,
		width: "15%",
		template: '<a href="#= url #">#= name #</a>'
	}, {
		field:"description",
		title: "Description",
		filterable: true,
		width: "20%"
	}, {
		field:"url",
		title: "Blog URL",
		filterable: true,
		width: "15%"
	}, {
		field:"domain_category",
		width: "10%",
		editor: domainCategoryDropDownEditor,
		// The template should be a function that matches the id's and returns the title.
		template: "#=getCategoryDomainDropdown(category_domain_ref)#",//The method that gets the id by the name that was selected.
		title: "Domain",
		filterable: {
			extra: false,// Don't show the full filter menu
			ui:categoryDomainDropdownFilter,
			operators: {
				string: {
					eq: "is equal to",
					neq: "is not equal to"
				}//..string
			}//...operators
		}//...filterable
	}, {
		field:"category",
		width: "12%",
		editor: categoryDropDownEditor,
		// The template should be a function that matches the id's and returns the title.
		template: "#=getCategoryDropdown(category_id)#",//The method that gets the id by the name that was selected.
		title: "Category",
		filterable: {
			extra: false,// Don't show the full filter menu
			ui:categoryDropdownFilter,
			operators: {
				string: {
					eq: "is equal to",
					neq: "is not equal to"
				}//..string
			}//...operators
		}//...filterable
	}, { 
		command: [
			// Opens the editable columns 
			{ name: "edit", text: "Edit" },
			// Cancels the operation
			{ name: "destroy", text: "Cancel" }
		],
		title: " ",
		width: "12%" 
	}
	]// columns:

});// $("#feedsGrid").kendoGrid({

Further Reading