Grails – DataTable z filtrem i pagerem
Kategoria Programowanie dn. 01 lut, 2009
Mojej przygody z Grails ciąg dalszy. Zauważyłem, że framework ten posiada bardzo wiele fajnych wtyczek. Jedną z nich jest grails-ui, który korzystając z dobrodziejstw Yahoo! User Interface Library pozwala na wstawianie ciekawych komponentów na stronę za pomocą zaledwie jednego znacznika GSP. Jednym z takich komponentów jest DataTable, bazujący na yui:dataTable, pozwalający np. na pobieranie danych za pomocą żądań asynchronicznych AJAX w formacie JSON (lub XML, wedle życzenia), dzielenie wyników na podstrony (pager), mechanizm skórek itd. Znacznik GSP gui:dataTable posiada wiele opcjonalnych atrybutów, którymi można skonfigurować naszą tabelę z danymi (ilość maksymalna wierszy na jednej podstronie, format danych, kolumny itd.). Mam jednak wrażenie, że autor wtyczki nie pomyślał o możliwości dołączenia do naszej tabeli filtrowania wyników. Owszem, w Sieci można znaleźć przykłady filtrowania dla ‘gołego’ yui:dataTable, ale większość z nich, albo nie wspiera cięcia na podstrony, albo filtruje wyniki dopiero po stronie klienta (a z serwera pobiera pełną listę danych). Dla mnie to niestety za mało.
Oto przykład na to, jak korzystając ze znacznika GSP gui:dataTable zaimplementować dataTable, który daje się połączyć z prostym filtrem wyników.
Do przedstawienia przykładu wykorzystam dane dostępne na stronie z przykładami użycia dataTable w YUI. Załóżmy więc, że mam bazę danych restauracji (pizzerii).

Użyję więc klasy dziedzinowej (ang. domain class) Restaurant o postaci:
class Restaurant { String title String address String city String state }
Listę tę chcę mieć dostępną pod adresem http://localhost:8080/PagedDatatable/restaurant/list, czyli potrzebny mi będzie kontroler RestaurantController, a w nim metoda list. W metodzie tej, kontroler nie ma zbyt wiele roboty – po prostu serwuje statyczną stronę, toteż jej postać jest taka:
def list = { }
Niezbyt skomplikowane, prawda? :) Pusta metoda sprawi, że kontroler od razu zabierze się za renderowanie wyniku, strony GSP, która wygląda tak:
<g:javascript library="yui" /> <gui:resources components="['dataTable']"/> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <meta name="layout" content="main" /> <title>Restaurant List</title> </head> <body> <div class="yui-skin-sam" style="width: 600px;"> <label for="fltr[title]">Title:</label><input type="text" id="fltr[title]" value=""> <label for="fltr[city]">City:</label><input type="text" id="fltr[city]" value=""> <button id="filterButton">Filter</button><br/> <gui:dataTable id="myDataTable" controller="restaurant" action="listAsJSON" rowsPerPage="15" sortedBy="title" columnDefs="[ [key:'title', label:'Title'], [key:'address', label: 'Address'], [key:'city', label: 'City'], [key:'state', label: 'State'] ]" /> </div> <script> YAHOO.util.Event.onDOMReady(function () { updateFilter = function () { GRAILSUI.myDataTable.customQueryString = "title="+YAHOO.util.Dom.get("fltr[title]").value+"&"+ "city="+YAHOO.util.Dom.get("fltr[city]").value; var state = GRAILSUI.myDataTable.getState(); // gui:dataTable odwołuje się do własności sorting obiektu state, // podczas gdy yui oferuje jedynie własność sortedBy (czyżby bug?) // kopiujemy własność, by uniknąć błędu state.sorting = state.sortedBy; // reset pagera, zawsze po filtrowaniu wracamy do pierwszej strony state.pagination.recordOffset = 0; query = GRAILSUI.myDataTable.buildQueryString(state); GRAILSUI.myDataTable.getDataSource().sendRequest(query,{ success : GRAILSUI.myDataTable.onDataReturnReplaceRows, failure : GRAILSUI.myDataTable.onDataReturnReplaceRows, scope : GRAILSUI.myDataTable, argument: state }); }; YAHOO.util.Event.on('filterButton','click',function (e) { updateFilter(); }); }); </script> </body> </html>
Na początku strony, deklarujemy użycie wtyczki YUI! oraz komponentu dataTable – w ten sposób Grails zadba o to, by nasza strona została wyposażona w linki do odpowiednich skryptów i arkuszy stylów. W znaczniku używam opcjonalnego atrybutu id, jest to spowodowane faktem, że będę się odwoływał do naszej tabeli i muszę wiedzieć jak. Gdybym nie użył id, dostałbym tabelę z unikalnym, losowym identyfikatorem. Nasza tabela wywołuje asynchronicznie metodę listAsJSON kontrolera RestaurantController (o niej za chwilę). Ważne jest, żeby w zdarzeniu onDOMReady powiązać kliknięcie w przycisk Filter z funkcją updateFilter(), w której konstruujemy parametry żądania i wstawiamy je do własności customQueryString naszej tabeli. Następnie obchodzimy jeden bug(?) gui:dataTable, resetujemy pager (na wypadek, gdybyśmy załadowali wynik z mniejszą ilością podstron niż obecna) i przeładowujemy naszą tabelę.
Gotowe :)
Na koniec tylko metoda listAsJSON naszego kontrolera:
def listAsJSON = { def pagingConfig = [ max: params.max ?: 15, offset: params.offset ?: 0, sort: params.sort ?: 'title', order: params.order ?: 'asc' ] def resultFilter = { or { if(params.title && params.title != ''){ like("title", "${params.title}%") } if(params.city && params.city != ''){ like("city", "${params.city}%") } } } def c = Restaurant.createCriteria() def list = c.list(pagingConfig, resultFilter) def totalRec = list.totalCount response.setHeader("Cache-Control", "no-store") def data = [ totalRecords: totalRec, results: list ] render data as JSON }
Korzystamy tu z dobrodziejstw dwóch ciekawych możliwości oferowanych przez Grails. Pierwszą z nich są Criteria, pozwalające na budowanie filtrów naszego zapytania (za pomocą domknięć mechanizm ten staje się jeszcze atrakcyjniejszy niż jego pierwowzór pochodzący z Hibernate). Każda klasa dziedzinowa posiada możliwość stworzenia obiektu klasy Criteria za pomocą metody createCriteria(). Drugą ciekawostką jest konwerter JSON, ładowany za pomocą importu:
import grails.converters.JSON
Konwerter ten, w locie, zamieni naszą mapę wyników na format JSON, zrozumiały dla naszej tabeli.

Gotowe. Miłego filtrowania :)
13 lutego, 2009 o 14:54
Great example! I can’t read Polish, but your code is very clear and concise. I’m going to refer to this when I work on the JIRA to get this functionality into Grails-UI. Good work!
13 lutego, 2009 o 15:09
Thanks Matt! And thank you for this great grails plugin! :)
I posted a new message on nabble grails-user list, because my snippet lacks this little feature – loading indicator.
13 lipca, 2010 o 14:16
Hi -
Thanks for the example. I was wondering, would I be able to update a „div”. For some reason, I can’t seem to get the button activated. The difference in my code is – I have the button in a „tabView”. It looks like the „button” action is not getting run. Would appreciate any pointers. Again, great example.
15 lipca, 2010 o 14:49
Hi. I’m happy that you like my example. Unfortunatelly I don’t use Grails for now, so I can’t remember the tricks and I don’t know how to help you with this. You may take a look to the nabble grails-user list – maybe those guys will know the answer.