Недавно мне было поручено показывать большие табличные данные на веб-сайте, и я наткнулся на 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.