Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Using datatables.net with Grails Seamlessly

DZone's Guide to

Using datatables.net with Grails Seamlessly

· Java Zone
Free Resource

Check out this 8-step guide to see how you can increase your productivity by skipping slow application redeploys and by implementing application profiling, as you code! Brought to you in partnership with ZeroTurnaround.

Recently 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. 



The Java Zone is brought to you in partnership with ZeroTurnaround. Check out this 8-step guide to see how you can increase your productivity by skipping slow application redeploys and by implementing application profiling, as you code!

Topics:

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}