Using datatables.net with Grails Seamlessly
Join the DZone community and get the full member experience.
Join For FreeRecently I was tasked with showing large tabular data on a website and came across Datatables.net . It is an awesome javascript library that can be easily extended and customised. It is very powerful and has a lot of functionalities like server side pagination, in memory search and sorting. It can be easily used once you are familiar with its code and options.
As with every powerful library, there is a learning curve to it. I being a grails guy, don't appreciate when I have to write a lot of code. So ended up creating a taglib and some conventions that allows one to easily embed a datatable with server side pagination and search in a page. Hope that it saves someone some time.
To use this, this is what should go into the gsp page :
<adminConsole:dataTable id="customDatatable" serverURL="${createLink(controller: "someController",action: "someAction")}" ></adminConsole:dataTable>
What this does, is embeds the datatable with all necessary initialization into the html page. It will take up 100% of the space that is available in the parent container.
And this how your controller should look :
def someAction(){ def offset = params.iDisplayStart ? Integer.parseInt(params.iDisplayStart) : 0 def max = params.iDisplayLength ? Integer.parseInt(params.iDisplayLength) : 10 def sortOrder = params.sSortDir_0 ? params.sSortDir_0 : "desc" def sortBy = new DataTableMapper(config: grailsApplication.config).getPropertyNameByIndex(params.iSortCol_0,"customDatatable") def searchString = params.sSearch def returnList = adminConsoleService.inviteSuccessUserList(sortBy,sortOrder,offset,max,searchString) def returnMap = new DataTableMapper(config: grailsApplication.config).createResponseForTable(returnList,"customDatatable",params.sEcho) render returnMap as JSON }
Thats it. You are ready to go. Your table is rendered with all server side pagination and search.
Behind the scenes this is what happens.
There are three main parts of this.
The first is the taglib itself.
package com.wowlabz.mara.datatables class AdminConsoleDataTableTagLib { def springSecurityService static namespace = "adminConsole" def grailsApplication def dataTable = { attrs, body -> def dataTableHeaderListConfig = grailsApplication.config."${attrs.id}".table.headerList def removeSorting = false def serverURL = attrs.serverURL def fixedClass=attrs.fixedTableClass?:'noClass' println "fixedClass=="+fixedClass out << """ <table id="${attrs.id}" cellpadding="0" cellspacing="0" border="0" class="table table-striped table-bordered ${fixedClass}"> <thead> <tr>""" dataTableHeaderListConfig.each { out << """ <th style="cursor: pointer;" sortPropertyName="${it.sortPropertyName}" sortable="${it.sortable}" """ out << """>""" out << g.message(code: it.messageBundleKey, default: it.name) out << """</th> """ } out << """</tr> </thead> <tbody> </tbody> </table> <script type="text/javascript"> var dataTableDefaultSorting = []; var hideSorting = []; """ dataTableHeaderListConfig.eachWithIndex {obj, i -> if (obj.defaultSorting) { out << """ dataTableDefaultSorting[dataTableDefaultSorting.length]=[${i},'${obj.defaultSortOrder}']; """ } if (obj.disableSorting == "true") { removeSorting = true out << """ hideSorting[hideSorting.length] = ${i}; """ } } out << """ if(dataTableDefaultSorting.length==0){ dataTableDefaultSorting = [[0,"asc"]]; } """ out << """ jQuery.extend( jQuery.fn.dataTableExt.oStdClasses, { "sWrapper": "dataTables_wrapper form-inline" } ); jQuery(document).ready( function() { var ${attrs.id}oTableCurrentData; var ${attrs.id}oTable = jQuery('#${attrs.id}').dataTable({ "aaSorting":dataTableDefaultSorting, "bProcessing": true, "bServerSide": true,""" if (attrs.serverParamsFunction) { out << """ "fnServerParams": function(aoData){ var hideSearch='${attrs.hideSearch}' if(hideSearch!='null'){ jQuery("#${attrs.id}_filter").hide(); } ${attrs.id}ServerParamsFunction(aoData) }, """ } //drawLabelElementId => This variable is set from the dashboard where we need to set count after the table is full loaded,we can implement this in other details page as well. println("Removing Sort Status : ${removeSorting}") if (removeSorting) { out << """ "aoColumnDefs": [{ "bSortable": false, "aTargets": hideSorting }], """ } out << """ "fnDrawCallback": function(oSettings) { var drawLabel='${attrs.drawLabelElementId}'; if(drawLabel!='null'){ jQuery('${attrs.drawLabelElementId}').html("["+oSettings._iRecordsTotal+"]"); } var callBackFunction='${attrs.callBackFunction}'; if(callBackFunction!='null'){ ${attrs.id}CallBackFunction(oSettings._iRecordsTotal); } }, """ out << """ "sAjaxSource": "${serverURL}", "sDom": "<'row'<'col-md-6'l><'col-md-6'f>r>t<'row'<'col-md-6'i><'col-md-6'p>>", "fnCreatedRow":function( nRow, aData, iDataIndex ) { jQuery(nRow).attr("mphrxRowIndex",iDataIndex); jQuery(nRow).attr("mphrxRowID",aData[0]); """ if (attrs.contextMenuTarget) { out << """ jQuery(nRow).contextmenu({ target:'#${attrs.contextMenuTarget}', before: function(e, element) { ${attrs.id}oTableCurrentData = ${attrs.id}oTable.fnGetData( jQuery(element).attr("mphrxRowIndex") ); return true; }, onItem: function(e, element) { if(${attrs.id}ContextMenuHandler){ ${attrs.id}ContextMenuHandler(e,element); } } }) """ } out << """ }, "oTableTools": { "aButtons": [ "copy", "print", { "sExtends": "collection", "sButtonText": 'Save <span class="caret" />', "aButtons": [ "csv", "xls", "pdf" ] } ] } }); jQuery('#${attrs.id}_filter input').unbind(); jQuery('#${attrs.id}_filter input').bind('keyup', function(e) { if(e.keyCode == 13) { ${attrs.id}oTable.fnFilter(this.value); } }); """ dataTableHeaderListConfig.eachWithIndex { obj, i -> if (obj.hidden) { out << """ ${attrs.id}oTable.fnSetColumnVis(${i}, false); """ } } out << """ }); </script> """ } }
Now the server side :
I added a Config class to help customising the columns easy. It is something like this.
package com.wowlabs.mara.datatables.config import com.wowlabs.mara.AppUserInviteList inviteCount = { appUserInviteList -> return (appUserInviteList as AppUserInviteList).inviteList.size() } customDatatable { table { headerList = [ [name: "ID", messageBundleKey: "id", returnValuePropertyOrCode: "id", sortPropertyName: "id", hidden: true], [name: "PhoneNumber", messageBundleKey: "com.wowlabz.adminconsole.phoneNumber", returnValuePropertyOrCode: "ownerPhoneNumber", sortPropertyName: "ownerPhoneNumber"], [name: "Count", messageBundleKey: "com.wowlabz.adminconsole.count", returnValuePropertyOrCode: "count",sortPropertyName: "count"] ] } }
Keep adding columns to this config, and the columns get added to the UI magically. Well, not entirely magically. You know how that works. So you can just change this config, even at runtime and control entirely how the table renders.
This config is read by an Utility class that reads this config to generate the data and populate it accordingly.
package com.wowlabz.mara.datatables import com.mongodb.BasicDBObject import com.mongodb.Cursor import com.mongodb.QueryResultIterator /** * Created by siddharthbanerjee on 2/8/14. */ class DataTableMapper { def config def setConfig(configObject){ config = configObject } def createResponseForTable = { returnList, id, sEcho -> def returnMap = [:] try { returnMap.iTotalRecords = returnList.totalCount returnMap.iTotalDisplayRecords = returnList.totalCount }catch(exp){ returnMap.iTotalRecords = 10000 returnMap.iTotalDisplayRecords = 10000 } returnMap.sEcho = sEcho def dataReturnMap = [] if(returnList instanceof Cursor){ while(returnList.hasNext()){ def eachData = returnList.next() def eachDataArr = [] config."${id}".table.headerList.each { eachConfig -> if (eachConfig.returnValuePropertyOrCode instanceof String) { eachDataArr << evaluateExpressionOnBean(eachData, "${eachConfig.returnValuePropertyOrCode}") } else if (eachConfig.returnValuePropertyOrCode instanceof Closure) { eachDataArr << eachConfig.returnValuePropertyOrCode(eachData) } } dataReturnMap << eachDataArr } }else { returnList.each { eachData -> def eachDataArr = [] config."${id}".table.headerList.each { eachConfig -> if (eachConfig.returnValuePropertyOrCode instanceof String) { eachDataArr << evaluateExpressionOnBean(eachData, "${eachConfig.returnValuePropertyOrCode}") } else if (eachConfig.returnValuePropertyOrCode instanceof Closure) { eachDataArr << eachConfig.returnValuePropertyOrCode(eachData) } } dataReturnMap << eachDataArr } } returnMap.aaData = dataReturnMap return returnMap } def evaluateExpressionOnBean(beanValue, expression) { def cellValue if (expression.contains(".")) { expression.split("\\.").each { if (cellValue) { if (cellValue?.metaClass?.hasProperty(cellValue, it)) cellValue = cellValue."$it" } else { if (beanValue?.metaClass?.hasProperty(beanValue, it)) cellValue = beanValue."$it" } } } else { if(beanValue instanceof BasicDBObject){ try { cellValue = beanValue?."$expression" }catch(exp){ cellValue = null } } if (beanValue?.metaClass?.hasProperty(beanValue, expression)) cellValue = beanValue?."$expression" } return cellValue } def getPropertyNameByIndex(index, tableId) { return config."${tableId}".table.headerList[index.toString().toInteger()].sortPropertyName } }
That is all it takes to get this going. There are a lot of customisations that can be done on top of this. This is a work in progress but can be run as is. Hope I saved someone's time.
Opinions expressed by DZone contributors are their own.
Trending
-
Design Patterns for Microservices: Ambassador, Anti-Corruption Layer, and Backends for Frontends
-
Azure Virtual Machines
-
Step Into Serverless Computing
-
Building the World's Most Resilient To-Do List Application With Node.js, K8s, and Distributed SQL
Comments