Статьи

Использование datatables.net с Grails без проблем

Недавно мне было поручено показывать большие табличные данные на веб-сайте, и я наткнулся на  Datatables.net  . Это потрясающая библиотека JavaScript, которая может быть легко расширена и настроена. Он очень мощный и обладает множеством функций, таких как разбиение на страницы на стороне сервера, поиск и сортировка в памяти. Его можно легко использовать, когда вы ознакомитесь с его кодом и опциями. 

Как и в случае с любой мощной библиотекой, в ней есть кривая обучения. Я парень Грейлс, не ценю, когда мне приходится писать много кода. В итоге мы создали taglib и некоторые соглашения, которые позволяют легко встраивать данные в нумерацию страниц на стороне сервера и выполнять поиск на странице. Надеюсь, что это сэкономит кому-то время. 

Чтобы использовать это, вот что должно перейти на страницу gsp: 

<adminConsole:dataTable  id="customDatatable" serverURL="${createLink(controller: "someController",action: "someAction")}" ></adminConsole:dataTable>

Это встраивает таблицу данных со всей необходимой инициализацией в html-страницу. Это займет 100% пространства, доступного в родительском контейнере. 

И вот как должен выглядеть ваш контроллер:

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 
}

Это оно. Вы готовы к работе. Ваша таблица отображается со всей страницей и поиском на стороне сервера. 


За кадром это то, что происходит.

Есть три основные части этого. 

Первый — это сам taglib. 

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>
            """
    }
}

Теперь на стороне сервера: 

Я добавил класс Config, чтобы упростить настройку столбцов. Это что-то вроде этого. 

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"]
        ]
    }
}

Продолжайте добавлять столбцы в эту конфигурацию, и столбцы волшебным образом добавляются в пользовательский интерфейс. Ну, не совсем волшебно. Вы знаете, как это работает. Таким образом, вы можете просто изменить эту конфигурацию даже во время выполнения и полностью контролировать отображение таблицы. 

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.