Using Kendo Templates to Embed Cascading Dropdowns in a Kendo Grid
Mar 7 |
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.
Table of Contents
- Realtime Example
- Using Kendo Templates to Embed Cascading Dropdowns in a Kendo Grid
- Server-Side Queries to Obtain the Data Used in the Dropdowns
- Changing Style Properties of the Kendo Grid
- Empty DIV container to Hold the Kendo Grid
- Kendo Grid Custom Header
- Custom Search JavaScript Methods used by the Grid
- The Logic for the First Cascading Category Domain Kendo Dropdown
- The Logic for the Last Child Cascading Category Kendo Dropdown
- Wrap the Kendo DataSource and Grid Initialization with a Document Ready Block
- Create the Kendo DataSource
- Initialize the Kendo Grid
- Further Reading
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.
- For the columns.template argument, we are invoking the getCategoryDropdown method to populate the initial read-only values when the grid is loaded.
- The columns.editor argument is invoking the categoryDomainEditor JavaScript function that we created to initially populate the control once the edit button on the far right of the grid has been clicked.
- The columns.filter argument is invoking the categoryDomainDropdownFilter function. We are also modifying the filtering capability of the domain dropdown to "equal" or "not equal to".
Handling the Child Category Dropdown
- The columns.template argument, we are invoking the getCategoryDropdown method to populate the initial read-only values when the grid is loaded.
- The columns.editor argument is invoking the categoryDropDownEditor JavaScript function that we created to initially populate the control once the edit button on the far right of the grid has been clicked.
- The columns.filter argument is invoking the categoryDropdownFilter function. We are also modifying the filtering capability of the domain dropdown to "equal" or "not equal to".
$("#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
Related Entries
Tags
ColdFusion and Kendo UIThis entry was posted on March 7, 2023 at 6:19 PM and has received 1747 views.