With virtualized scrolling, Kendo Grids can be used to allow users to visualize and analyze data with extremely large datasets. A Kendo Grid with virtual scrolling minimizes the number of records by only retrieving records to populate the records within the browser window viewport which vastly improves rendering performance. In production, I use these virtualized grids, with a large number of columns, to render up to 75 million records! This article will demonstrate all of the steps necessary to deploy these powerful grids and teach you how to implement server-side paging, sorting, and filtering.
Kendo Virtual Grid Example
This virtual Kendo grid has over 150k records showing all of the major countries, cities, and states. Server-side sorting and paging are supported, and each column in the grid provides filters to allow the user to quickly retrieve the records. Both the live demo and the code are available below.
Kendo Grids with virtualized scrolling provide an enormous benefit allowing users to visualize data with millions of records in an easy-to-use and powerful HTML5 web-based grid. I use these types of Kendo grids often in enterprise finance departments to allow the administrators to analyze Payroll Subledger data. These grids have sorting and powerful search and filtering capabilities allowing users to isolate records from extremely large datasets. However, there are a few limitations to be aware of.
The main drawback to virtual grids is that users can't scroll over a million or so records, however, I don't know of a single user that will voluntarily scroll beyond several thousand records! The user will always seek to search the data instead of having to scroll through a ridiculously large set of records. To overcome this limitation, you can also provide server-side paging and implement column filtering as we have done here.
Differences Between Kendo Grid Pagination and Virtualization
To improve performance, Telerik suggests either using pagination or virtualization, but what is the actual difference here? Both techniques offer significant performance increases by limiting the amount of data required while rendering the Kendo grid. However, using virtual grids is the way to go if you are offering grid filtering or sorting (and with large datasets- you should). The reason is that while the pagination does limit the number of records, the default behavior sorts the data on the client side requiring all of the data to be analyzed before applying the search filters or sort order. On the other hand, since all of the data operations are offloaded to the server when using column virtualization, the client-side rendering is much more performant. Also, the database is generally superior in handling and messaging a large volume of data. As a general rule of thumb, if you are using more than 20k records, you should always consider using a virtual grid or having server-side pagination.
Kendo Virtual Grid Overview
The Kendo virtual grid sends a string of arguments as JSON to the server to determine which records should be displayed. The grid sends new arguments to the server whenever the user scrolls past a certain point or when sorting or filters are made. The server will take this JSON string and query the database to retrieve the records that need to be displayed. In this example, we will be using ColdFusion on the server. I have built many different functions to process the logic on the server. If you are using ColdFusion, this process is pretty much plug-and-play. Using this approach, implementing a virtual Kendo grid should only take 15 minutes or so. If you are using a different server-side language, such as PHP, I will explain the backend logic in a future article so that you develop your own logic to handle server-side logic for the Kendo virtual grid.
Client Side Logic
If you have been following along with our previous Kendo articles, the client-side code should be familiar to you and will explain the highlights of the code below. We are only going to highlight some important parts of the code, you may want to open up the code window and push it to the side when reading this to follow along.
CSS to Ensure that all Rows are Uniform in Size
Since the events are fired based on the user's current scroll position, each row in the grid should be uniform in height. This is accomplished using CSS by either eliminating word wrapping or making the grid wide enough so that no row wrapping exists. In this example, we are setting the minimum grid width to be at least 1190 pixels wide and explicitly setting the height of the row, we are also eliminating white space wrapping.
<style>
/*horizontal Grid scrollbar should appear if the browser window is shrunk too much*/
#WorldDbGrid table
{
min-width: 1190px;
}
.k-virtual-scrollable-wrap td {
font-size: 14px;
white-space:nowrap;
line-height: 13px;
}
#WorldDbGrid .k-virtual-scrollable-wrap tr td {
height: 15px
}
</style>
Using a Kendo Template to Display the Country's Flag
<!-- 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"/>
</div>
</script>
The Kendo DataSource
The virtual grid Kendo data source is nearly identical to the data sources handling the non-virtual grids. The differences are that we are using:
The server arguments, serverPaging, sorting, and filtering, defer all of the processing to the server. As we just mentioned, these settings send a string of arguments in JSON to the server when an action is needed to be performed. Schema.data refers to the data handle in the JSON string that contains the data object. Schema.total determines how many records we need to display. We will delve into the details of these settings later in the article.
Since we are using virtual scrolling (see the notes in the grid initialization below), server paging is optional. However, here we are using both virtual scrolling and serverPaging. You can use the two options in tandem and set the page size to determine how far the user can virtually scroll. In this example, the user can scroll a bit but will come to the end of grid when the user is at the 100th record (set in the pageSize argument). The pageSize setting must be set higher than the number of rows that can visually be seen in the grid without scrolling.
Note that the parameterMap logic is needed when using virtual grids. The rest of the settings have been covered in prior articles and should be self-explanatory.
// Datasource Declaration ---------------------------------------------------------------------------------
WorldDbDs = new kendo.data.DataSource({
type: "json",
serverPaging: true, // when set to true, the grid will automatically post the following json string to the processing page {"take":100,"skip":0,"page":1,"pageSize":100}
serverSorting: true,
allowUnsort: true,
filterable: true,
serverFiltering: true,
pageSize: 100, // If server paging is properly set above, the grid should send the following arguments to the cfc: "{"take":100,"skip":0,"page":1,"pageSize":100}"
allowCopy: true,
reorderable: true,
resizable: true,
columnMenu: true,
transport: {
read: {
// Note: since this template is in a different directory, we can't specify the subledger template without the full path name.
url: "<cfoutput>#cfcUrl#</cfoutput>?method=getWorldDbForKendoGrid", // /cssweb/applications/subLedger/subLedger.cfc?method=getVBarInProgressForGrid 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"
},
parameterMap: function (options) {
return JSON.stringify(options);
}
},//transport
cache: false,
schema: {
total: "total", // Needed on virtual grids. The total and data are being returned by the cfc. Note the total and data vars are wrapped in quotes.
data: "data", // 'Data' is being returned by the Kendo virtual grid helper function.
model: {
id: "CountryId", // 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: {
CountryId: { type: "number", editable: false, nullable: false },
Country: { type: "string", editable: false, nullable: false },
Capital: { type: "string", editable: false, nullable: false },
ISO2: { type: "string", editable: false, nullable: false },
ISO3: { type: "string", editable: false, nullable: false },
StateId: { type: "number", editable: false, nullable: false },
State: { type: "string", editable: false, nullable: false },
CountryId: { type: "number", editable: false, nullable: false },
CityId: { type: "number", editable: false, nullable: false },
City: { type: "string", editable: false, nullable: false },
CityLatitude: { type: "number", editable: false, nullable: false },
CityLongitude: { type: "string", editable: false, nullable: false }
}//fields:
}//model:
}//schema
});//feedsDs = new kendo.data.DataSource
Grid Initialization
Most of these settings have been covered in prior articles, but there are a few things that are required when using virtual grids:
Virtual grids must use scrollable virtual:true. Since serverPaging is set to true in the Kendo data source, the user will have to click on the paging button at the bottom of the grid after scrolling to 100 records.
serverPaging is optional when using virtual scrolling, however, having endless scrolling can be confusing, so we are also using serverPaging here.
Setting the grid height is necessary when using virtual grids, and all visible columns are filterable allowing the users to search the records in the grid.
The flag column uses the Kendo template that we created earlier in this example to display the flag.
Unlike all of the other articles, the server-side logic required for virtual grids is quite extensive. I have developed various ColdFusion-based functions that are used that make implementing a virtual grid with ColdFusion a breeze, however, there is a lot of logic that takes place behind the scenes here. This approach also only works with MS SQL Server.
Create a Flat Table or a View as the Datasource
The approach that I have developed requires a SQL Server flat table or view. Here we are using a view that denormalizes our Country, State City SQL Server database. My virtual grid ColdFusion components only work with MS SQL server, and this MySql database was converted to MS SQL for this example.
Download ColdFusion Templates from GitHub
I have created four different files on GitHub to use to create Kendo virtualized grids.
You also need ou CfJson component if you don't already have it. This ColdFusion component is used for almost every Kendo widget that consumes JSON data using a ColdFusion service endpoint. This ColdFusion component is found at https://github.com/GregoryAlexander77/CfJson.
The first function (getWorldDbForKendoGrid) relies upon the KendoUtils.cfc component containing the core logic to prepare the SQL statements based on the JSON arguments sent by the Kendo grid.
Finally, the ColumnProperty.cfc component is used by the function to determine the column datatype.
Create the ColdFusion Service Endpoint
The service endpoint, called from the Kendo Datasource, needs some minor modification to get it to work in your environment. This function relies upon the files that need to be downloaded above.
The ColdFusion-based server endpoint requires 4 arguments and you must specify the columns that you are selecting.
The tableName should specify the name of the flat table or view.
Use the same table name as the tableAlias. This will create an alias name for the table. I put this in the logic as I wanted to use the same table for multiple grids and wanted to distinguish the queries separately.
The primaryKey argument is used to identify the selected row in the grid and is used if you use the grid for editing or having a master-detail interface. I will explain this concept later.
The defaultOrderByStatement allows you to specify how you want the records ordered. In this example, I am ordering the data by country.
You need to specify all of the columns that you want in the sqlStatement. The column names must be in the proper case, and be sure to leave the 'SELECT * FROM ( SELECT' string at the top of the page. I may recode this with an extra requiredColumns argument to make it easier in the future, but I'll leave the code that I use in production alone for now.
You should not have to touch any of the code underneath the sqlStatement argument.
<!--- Function to populate the grid --->
<cffunction name="getWorldDbForKendoGrid" access="remote" returnformat="json" output="false">
<!--- There are no arguments for this function. --->
<cfsetting enablecfoutputonly="true" />
<!--- Set params --->
<cfparam name="take" default="100" type="string">
<cfparam name="skip" default="0" type="string">
<cfparam name="page" default="1" type="string">
<cfparam name="pageSize" default="100" type="string">
<cfparam name="whereClause" default="" type="string">
<cfparam name="sortStatement" default="" type="string">
<cfparam name="searchFilter" default="false" type="boolean">
<cfparam name="logSql" default="true" type="boolean">
<!--- The name of the view (or a table that is derived from a view. --->
<cfset tableName = 'ViewCountryStateCity'>
<cfset tableNameAlias = 'vCountryStateCity'>
<cfset primaryKey = "CityId">
<cfset defaultOrderByStatement = 'ORDER BY Country'>
<!--- Get the number of records in the entire table, not just the top 100 for display purposes. We will overwrite this later if there are any new filters applied. --->
<cfquery name="getTotal" datasource="#dsn#">
SELECT count(#primaryKey#) as numRecords FROM [dbo].[#tableName#]
</cfquery>
<cfset totalNumRecords = getTotal.numRecords>
<!--- Make the query. Input the select statement *without* the from clause (or any other clauses) here. --->
<cfset sqlStatement = '
SELECT * FROM
( SELECT
CountryId
,Country
,Capital
,Currency
,CurrencyName
,ISO2
,ISO3
,Flag
,Latitude
,Longitude
,StateId
,State
,StateFlag
,Type
,CityId
,City
,CityLatitude
,CityLongitude
,CityFlag
'>
<!--- Note: you should not have to touch the following lines of this code. If you want a custom query name other than 'data', you will have to adjust tthe query name in two places. --->
<!---
Get the HTTP request body content.
The content in the request body should be formatted like so: {"take":100,"skip":9300,"page":94,"pageSize":100,"sort":[{"field":"ref2","dir":"desc"}]}
NOTE: We have to use toString() as an intermediary method
call since the JSON packet comes across as a byte array
(binary data) which needs to be turned back into a string before
ColdFusion can parse it as a JSON value.
--->
<cfset requestBody = toString( getHttpRequestData().content ) />
<!--- Double-check to make sure it's a JSON value. --->
<cfif isJSON( requestBody )>
<!--- Deserialize the json in the request body. --->
<cfset incomingJson = deserializeJSON( requestBody )>
<!--- Invoke the createSqlForVirtualGrid method in the kendoUtils.cfc component that will send back sql clauses. --->
<cfinvoke component="#KendoUtilsObj#" method="createSqlForVirtualGrid" returnvariable="sqlStruct">
<cfinvokeargument name="jsonString" value="#requestBody#">
<cfinvokeargument name="dsn" value="#dsn#">
<cfinvokeargument name="tableName" value="#tableName#">
</cfinvoke>
<cfif structFind(sqlStruct, "take") neq ''>
<cfset take = structFind(sqlStruct, "take")>
</cfif>
<cfif structFind(sqlStruct, "skip") neq ''>
<cfset skip = structFind(sqlStruct, "skip")>
</cfif>
<cfif structFind(sqlStruct, "page") neq ''>
<cfset page = structFind(sqlStruct, "page")>
</cfif>
<cfif structFind(sqlStruct, "pageSize") neq ''>
<cfset pageSize = structFind(sqlStruct, "pageSize")>
</cfif>
<cfif structFind(sqlStruct, "whereClause") neq ''>
<cfset whereClause = structFind(sqlStruct, "whereClause")>
</cfif>
<cfif structFind(sqlStruct, "sortStatement") neq ''>
<cfset sortStatement = structFind(sqlStruct, "sortStatement")>
</cfif>
<cfif structFind(sqlStruct, "searchFilter") neq ''>
<cfset searchFilter = structFind(sqlStruct, "searchFilter")>
</cfif>
</cfif><!--- <cfif isJSON( requestBody )> --->
<!--- Build the over order by statement. Make sure that a closing bracket ')' is at the end of the string. --->
<cfset overOrderStatement = ',ROW_NUMBER() OVER ('>
<cfif sortStatement neq ''>
<cfset overOrderStatement = overOrderStatement & sortStatement & ")">
<cfelse>
<!--- Default order by. --->
<cfset overOrderStatement = overOrderStatement & defaultOrderByStatement & ")">
</cfif>
<!--- Append it to the sqlStatement --->
<cfset sqlStatement = sqlStatement & " " & overOrderStatement>
<!--- Build the alias for the rownumber(). I am defaulting to 'as rowNumber' --->
<cfset sqlStatement = sqlStatement & " AS RowNumber">
<!--- Append the real and alias table name --->
<cfset sqlStatement = sqlStatement & " FROM [dbo].[" & tableName & "]) " & tableNameAlias>
<!--- Append the additional WHERE clause statement to it if necessary --->
<cfif whereClause neq ''>
<cfset sqlStatement = sqlStatement & " " & preserveSingleQuotes(whereClause)>
</cfif>
<!--- Log the sql when the logSql is set to true (on top of function) --->
<!--- <cfif logSql>
<cfset filePath = subledgerPath & 'logs'>
<cffile action="append" addnewline="yes" file="#filePath#/virtualGridSql.txt" output="#Chr(13)##Chr(10)#'#myTrim(sqlStatement)#'#Chr(13)##Chr(10)#" fixnewline="yes">
</cfif> --->
<!--- Testing carriage. If this is not commented out, the grids will not populate. --->
<cfoutput>#preserveSingleQuotes(whereClause)#</cfoutput>
<cfquery name="data" datasource="#dsn#">
#preserveSingleQuotes(sqlStatement)#
</cfquery>
<!--- Write the sql to the console log for debugging. Note: if you write this out- it will break the grid, so only do so in development.
<cfoutput>
<script>
if ( window.console && window.console.log ) {
// console is available
console.log ('#preserveSingleQuotes(sqlStatement)#');
}
</script>
</cfoutput>
--->
<!--- Using my jsonArray.cfc --->
<cfinvoke component="#application.cfJsonComponentPath#" method="convertCfQuery2JsonStructForVirtualGrid" returnvariable="jsonString" >
<cfinvokeargument name="queryObj" value="#data#">
<cfinvokeargument name="includeTotal" value="true">
<!--- When we use server side paging, we need to override the total and specify a new total which is the sum of the entire query. --->
<cfinvokeargument name="overRideTotal" value="true">
<!--- We set the totalNumRecords near the top of the function, however, if the filters were applied, the new total will be the number of records in the data query object. --->
<cfif searchFilter>
<cfset totalRecords = data.recordcount>
<cfelse>
<cfset totalRecords = totalNumRecords>
</cfif>
<cfinvokeargument name="newTotal" value="#totalRecords#">
<!--- The includeDataHandle is used when the format is json (or jsonp), however, the data handle is not included when you want to make a javascript object embedded in the page. --->
<cfinvokeargument name="includeDataHandle" value="true">
<!--- If the data handle is not used, this can be left blank. If you are going to use a service on the cfc, typically, the value would be 'data'--->
<cfinvokeargument name="dataHandleName" value="data">
<!--- Keep the case of the columns the same as the database --->
<cfinvokeargument name="convertColumnNamesToLowerCase" value="false">
</cfinvoke>
<cfreturn jsonString>
</cffunction>
In a future article, I will analyze the code on the server that prepares the data for this function. The goal of this article is to allow developers to fully implement a Kendo virtual grid without detailed elaboration on the backend.
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.
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
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.
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.
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".
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({
There are plenty of posts on the web showing you how to make a typical square button with Kendo UI, but I have not seen a post describing how to make a round Kendo button. Since there are no other posts that cover this, I had to learn this through trial and error. If you're using Kendo, it is a good idea to try to use as many native Kendo widgets as possible as these widgets will be incorporated into the chosen Kendo theme. The reasons for this are simple, native Kendo widgets will perform and look the same and inherit the properties of the selected theme.
Border Radius 50% can make nearly every element round, including a normal Kendo button.
Using the border-radius: 50%CSS can make nearly any HTML into a circle. Indeed, it can also make a normal Kendo UI button into a circle. However, on the desktop, Kendo UI's button also has an outline around its buttons, and the bottom part of the outline is larger than the upper part making the button look weird. Let's take a look:
#primaryTextButton {
height: 35px;
width: 35px;
border-radius: 50%;
}
<button id="primaryTextButton" class="k-primary">i</button> looks OK with mobile- but not so good on the desktop.
This button's outline looks weird. It only gets worse when the button becomes smaller.
A different approach
The code below is one method to make a round Kendo UI button widget using inline code. The element can be anything. Here I am using the button to expand the comments for Galaxie Blog, so I am using the id of comment Control. The collapse class is also not important here. What is important is the k-i-sort-desc-sm class, and k-primary. The k-i-sort-desc-sm is the Kendo UI icon that is being displayed, and the k-primary class indicates that the color of the button must be the primary color of the selected theme. The width and height of the button are set by the width and height property, and the border-radius: 50%argument takes the width and height properties to make a circular button image.
While coding logic for a Kendo tooltip, I had to send both the anchor's title and other information that the Kendo tooltip would display. I wanted to display the location where the image was taken, and a description of the image like this: "Grand Prismatic Spring, Yellowstone National Park. The vibrant colors of this spring is best captured from over-head. Wouldn't it be cool to fly a drone over this and take a few pictures?" I wanted both elements to be separated with a horizontal rule, and I needed to isolate the location and the description. However, the anchor tag only has a 'title' and an alt tag to store this information. If you want to store additional information in an element does not support, you can easily use the 'data-' + name prefix like so:
<span title="Grand Prismatic Spring, Yellowstone National Park." data-desc="The vibrant colors of this spring is best captured from over-head. Wouldn't it be cool to fly a drone over this and take a few pictures?">
</span>
To get the information that the data element contains, in this case, a Kendo template, use the data- prefix. You can name the prefix anything you want, and within the javascript template, don't need to specify the actual data tag- just leave it blank but name the variable after the 'data-' element (see '#=target.data('desc')#: below).
One of the reasons that there are very few posts concerning server-side validation with the Kendo validator is that it is not really built to do this. Unlike the majority of the other Kendo widgets which allow for customization, the validator was meant for simple validation. The built-in validation is quite useful for simple client-side validation, but it is not an extensive validation library and anytime that you need to extend it you will find yourself wanting more. I felt like I was trying to hammer a square peg into a circle while coding this. However, since one of the main goals of this blog is to share how ColdFusion can use the Kendo UI, I felt the need to dig into the kendo validator. I have a captcha form on this blog that is used to verify that the user is an actual user, and it encrypts a token and passes it off to the server-side for validation. You can see this in action by making a comment on this post below. The meat and potatoes of this function, like most of the other Kendo widgets, lie in Javascript. This script is heavily commented on.
$(document).ready(function() {
// Validation.
// Preset our sessionStorage var. This is set to '' initially to indicate that server side validation has not yet occurred.
sessionStorage.setItem("captchaValidated", "");
// Set the initial value of the captchaValidatedValue form element. We need to store this in order to know when to hit the server with a new validation request. We don't want to hit the server 3 times a second unless the text value has actually changed.
sessionStorage.setItem("captchaValidatedValue", "");
// Since the kendo validator occurs so quickly, it may send an erroneous value to the server the a few times before it picks up the new value that was entered. We need to allow several attempts to occur when we hit the server. This is a numeric value that will be incremented.
sessionStorage.setItem("captchaValidatedAttempts", "0");
// Invoked when the submit button is clicked. Instead of using '$("form").submit(function(event) {' and 'event.preventDefault();', We are using direct binding here to speed up the event.
var addCommentSubmit = $('#addCommentSubmit');
addCommentSubmit.on('click', function(e){
// Prevent any other action.
e.preventDefault();
// Set the attempts var to 0
sessionStorage.setItem("captchaValidatedAttempts", 0);
// Note: when using server side logic, this function may not post the data to the server due to the time required to return the validation from the server.
// If the form has been successfully validated.
if (addCommentFormValidator.validate()) {
// Submit the form. We need to have a quick timeout function as the captcha resonse does not come back for 150 milliseconds.
setTimeout(function () {
// Note: when testing the ui validator, comment out the post line below. It will only validate and not actually do anything when you post.
postCommentSubscribe(<cfoutput>'#URL.Id#'</cfoutput>, <cfoutput>'#URL.uiElement#'</cfoutput>);
}, 300);//..setTimeout(function () {
}//..if (addCommentFormValidator.validate()) {
});//..addCommentSubmit.on('click', function(e){
// !!! Note on the validators, all forms need a name attribute, otherwise the positioning of the messages will not work. Also data attributes that are dash separated become camel cased when retrieved using jQuery. --->
addCommentFormValidator = $("#addCommentSubscribe").kendoValidator({
// Set up custom validation rules
rules: {
// Name of custom rule.
// This can be any name, but I typically put the name of the field and a verb to indicate what I am enforcing ('nameIsRequired'). Note: if you just want to check to see if something was entered you can specify 'required' in the form element.
// This rule is quite different as it relies upon server side processing. I used https://www.telerik.com/blogs/extending-the-kendo-ui-validator-with-custom-rules as an example to build this.
captcha:
function(input) {
if (input.is("[id='captchaText']")){
// The captchaValidated value is set in storage session and set in the function below. Note, until the form loses focus, this function is constantly being validated until validation passes. Be careful not to go into an endless loop without exits.
var captchaValidated = getCapthchaValidated();
// If the captcha has not been validated on the server...
if (captchaValidated == ''){
// Check the captcha
captchaText.check(input);
// And stop...
return false;
}
// If the server validation failed, try again...
if (captchaValidated == 'no'){
// Check the captcha
captchaText.check(input);
// And stop...
return false;
}
if (captchaValidated == 'yes'){
// The captha text was succuessfully validated. Exit this function.
return true;
}
}//..if (input.is("[id='captchaText']")){
// This rule does not apply to the captha text input.
return true;
}//..function(input) {
}
//..captcha:
}).data("kendoValidator");
// Create a variable for this function as we will use the properties in the captch validation function above when it returns results.
var captchaText = {
check: function(element) {
// Note: the validator will fire off a new request 3 times a second, and we need to make sure that we are not hitting the server with stale data every time. We are going to see if the value has changed before firing off a new request to the server.
// Compare the input value to the value that was stored in sessionStorage. If the data has changed, and there has been fewer than 5 validation attempts that have failed, hit the server.
if (element.val() != getCapthchaValidatedValue() || getCaptchaValidatedAttempts() <= 5){
// Post to the server side method that will validate the captcha text.
$.ajax({
url: "<cfoutput>#application.proxyController#</cfoutput>?method=validateCaptcha",
dataType: 'json', // Use json for same domain posts. Use jsonp for crossdomain.
data: {
// Send in the arguments.
captchaText: element.val(),
captchaHash: $( "#captchaHash" ).val()
},
success: function(data) { // The `data` object is a boolean value that is returned from the server.
var captchaValidated = getCapthchaValidated();
if (data){
// debugging alert('Yes!');
// Set the value on the cache object so that it can be referenced in the next validation run. Note: sessionStorage can only store strings.
sessionStorage.setItem("captchaValidated", "yes");
// At the tail end of the validation process, when the validated data is complete, post the data. Since we have passed validation, we don't need to hit the 'captcha' custom rule above again.
if (addCommentFormValidator.validate()) {
// Hide the custom window message
kendo.ui.ExtAlertDialog.hide;
// submit the form. We need to have a quick timeout function as the captcha resonse does not come back for 150 milliseconds.
setTimeout(function () {
// Note: when testing the ui validator, comment out the post line below. It will only validate and not actually do anything when you post.
postCommentSubscribe(<cfoutput>'#URL.Id#'</cfoutput>, <cfoutput>'#URL.uiElement#'</cfoutput>);
}, 300);//..setTimeout(function () {
}
} else {
// Get the number of validation attempts.
var captchaValidatedAttempts = getCaptchaValidatedAttempts();
// Increment the validation attempt.
var currentCaptchaValidatedAttempt = (captchaValidatedAttempts + 1);
// Store the number of validation attempts in sessionStorage.
sessionStorage.setItem("captchaValidatedAttempts", currentCaptchaValidatedAttempt);
// After the 5th bad attempt, set the validation var and use a quick set timeout in order for the data to come back and be validated on the server before launching our custom error popup. Otherwise, if there was a previous captch error from the server, this custom error will pop up as the new data has not had a chance to be returned from the server yet.
if (currentCaptchaValidatedAttempt == 6){
// Store that we tried to validate, but it was not correct.
sessionStorage.setItem("captchaValidated", "no");
// Load a new captcha image (this is my own custom requirement and it has no bearing to the validator logic).
reloadCaptcha();
// Popup an error message.
setTimeout(function() {
if (getCapthchaValidated() == 'no'){
// Note: this is a custom library that I am using. The ExtAlertDialog is not a part of Kendo but an extension.
$.when(kendo.ui.ExtAlertDialog.show({ title: "The text did not match", message: "We have reloaded a new captcha image. If you're having issues with the captcha text, click on the 'new captcha' button to and enter the new text.", icon: "k-ext-warning", width: "<cfoutput>#application.kendoExtendedUiWindowWidth#</cfoutput>", height: "215px" }) // or k-ext-error, k-ext-question
).done(function () {
// Do nothing
});//..$.when(kendo.ui.ExtAlertDialog.show...
}//..if (addCommentFormValidator.validate()) {
}, 500);// A half of a second should allow the server to validate the captcha and return the result.
}
}
// Store the validated value. We will use this to determine when to hit the server for validation again if the value was not correctly typed in.
sessionStorage.setItem("captchaValidatedValue", element.val());
// Trigger the validation routine again. We need to validate each time, even if the value is validated on the server as we need to eliminate the error message raised in the validation script and will be popped up when the form loses focus on the onBlue event.
setTimeout(function() {
addCommentFormValidator.validate();
}, 2000);// Wait 2 seconds to hit the server again.
}//..success: function(data) {
// Notes: success() only gets called if your webserver responds with a 200 OK HTTP header - basically when everything is fine. However, complete() will always get called no matter if the ajax call was successful or not. It's worth mentioning that .complete() will get called after .success() gets called - if it matters to you.
});//..$.ajax({
}//..if (element.val() != getCapthchaValidatedValue()){
}//..check: function(element, settings) {
};//..var captchaText = {
});//...document.ready
// Validation helper functions. These must be oustide of the document ready block in order to work.
// Note: due to the latency of the data coming back from the server, we need to have two points to post a completely validated form to the server for processing. The first point is when the user clicks the submit form button, and the second point is at the tail end of the processing when the server has validated data.
// I am using sessionStorage to store the value from the server in order to effect the captach widget that I developed. I don't want to have to ask the user to go thru the captha validation process multiple times within the same session and don't want to have to write out the logic every time.
function getCapthchaValidated(){
return sessionStorage.getItem("captchaValidated");
}
// Prior to validation, what did the user enter?
function getCapthchaValidatedValue(){
// Since sessionStorage only stores strings reliably, this will be either: '', 'no', or 'yes'.
return sessionStorage.getItem("captchaValidatedValue");
}
// Returns the number of attempts that the server tried to validate the data. This only gets incremented when the server comes back with a false (not validated).
function getCaptchaValidatedAttempts(){
var attemps = sessionStorage.getItem("captchaValidatedAttempts");
return(parseInt(attemps));
}
Server-side ColdFusion:
5) The server-side logic determines if the text that the user entered matches the text that is shown in the captcha image.
5a) Does the text match the captcha image? Will return a boolean value (true/false).
5b) We need to eliminate any chance that a positive result is not overwritten. The client is firing off server-side ajax requests 3 times a second, and we need to be careful not to allow a subsequent ajax request to overwrite our value. We are using a server-side cookie to ensure that this does not happen.
<!--- 5) Helper functions for interfaces (addComments, addSub, etc.). Important note on function tags- they must have a returnFormat="json". Otherwise, ColdFusion will return the value wraped in a wddx tag.--->
<cffunction name="validateCaptcha" access="remote" returnType="boolean" returnFormat="json" output="false" hint="Remote method accessed via ajax. Returns a boolean value to determine if the users entered value matches the captcha image.">
<cfargument name="captchaText" required="yes" hint="What did the user enter into the form?" />
<cfargument name="captchaHash" required="yes" hint="The hashed value of the proper answer. This must match the captcha text in order to pass true." />
<cfargument name="debugging" required="no" type="boolean" default="false" hint="For testing purposes, we may need to not use the session.captchValidated value to prevent a true value from being incorreclty reset." />
<!---5a) Does the text that the user entered match the hashed value?--->
<cfif application.captcha.validateCaptcha(arguments.captchaHash,arguments.captchaText)>
<cfset captchaPass = true />
<!--- Set the captcha validated cookie to true. It will expire in one minute. --->
<cfcookie name="captchaValidated" expires="#dateAdd('n', 1, now())#" value="true">
<cfelse>
<!--- 5b) Note: the captcha will only be validated true one time as the encryption tokens get changed on true. However, the kendo validator validates quickly on blur, so there many be a true value overwritten by a false a millisecond later. We don't want to ever change a true value to false and will use session vars to prevent this behavior. You can override this behavior by setting debugging to true. --->
<cfif not debugging and isDefined("cookie.captchaValidated")>
<cfset captchaPass = true />
<cfelse>
<cfset captchaPass = false />
</cfif>
</cfif>
<!---Return it.--->
<cfreturn captchaPass />
</cffunction>
The HTML is rather simple, the key here are the custom messages are displayed in the 'data-required-msg="Captcha text is required."' and 'data-captcha-msg="The text does not match."'. These tags will pop up the required message when the captcha text has not been filled out, and when the text that the user has entered does not match the text in the captcha image. I am not dealing with any other custom messages here. The rest of the code does not apply, but I am including it for reference.
HTML: 6) The captcha HTML input.
<!-- Captcha -->
<tr height="35px" class="k-alt">
<td>
<!--- Captcha logic in its own table. This is a Kendo Mvvm template. --->
<div id="captchaImage" class="container k-alt">
<table align="left" class="k-alt" width="100%" cellpadding="0" cellspacing="0">
<!--- The source refers to the javascript code that will be used to populate the control, the template is the UI and it is not associated with the javascript code. --->
<tbody data-bind="source: captchaTextObj" data-template="captchaTemplate" data-visible="true"></tbody>
</table>
<!--- Create a Kendo template. We will use this to refresh the captcha hash and image on the page.--->
<InvalidTag type="text/x-kendo-template" id="captchaTemplate">
<tr class='k-alt'>
<td><label for="captchaText">Enter image text:</label></td>
</tr>
<tr class='k-alt'>
<td>
<input type="hidden" id="captchaHash" name="captchaHash" value="#: captchaHashReference #" />
<!--- 6) Create the captcha input with the custom messages. --->
<input type="text" name="captchaText" id="captchaText" size="6" class="k-textbox" style="width: 250px"
placeholder="Enter Captcha Text" required
data-required-msg="Captcha text is required."
data-captcha-msg="The text does not match." />
</td>
</tr>
<tr class='k-alt'>
<td>
<img src="#: captchaImageUrl #" alt="Captcha" align="left" vspace="5" border="1" />
</td>
</tr>
<tr class='k-alt'>
<td>
<button type="button" class="k-button" onClick="reloadCaptcha()">
<i class="fas fa-redo" style="alignment-baseline:middle;"></i> New Captcha
</button>
</td>
</tr>
</script>
</div><!---<div id="captchaImage" class="container">--->
</td>
</tr>
This entry was posted on March 1, 2019 at 7:34 PM and has received 1888 views.