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

d12

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.

d22

Gotowe. Miłego filtrowania :)