Grails i Ajax – pierwsze starcie
Naukę Grails prowadzę wielowątkowo. Lektura dokumentacji, przeglądanie wybranych rozdziałów książek, eksperymenty z poszczególnymi komponentami na testowym projekcie oraz próba sił w dwóch konkretnych projektach. Tym razem mała relacja z próby użycia Ajax'a z Grails.
W swojej aplikacji chciałem zrobić Ajax'owe zarządzanie kategoriami (lista, dodawanie i usuwanie). Na początek utworzyłem sobie klasę domenową:
class Category { String name }
oraz kontroler z pustą akcją index. Do nagłówka strony dodałem:
<g:javascript library="prototype" />
Prototype i scriptaculous (bazujący na prototype) są domyślnie dołączone do Grails. Podobno korzystając z pluginów łatwo można wykorzystać dowolną inną bibliotekę - jeszcze nie sprawdzałem.
W pliku index.gsp wstawiłem następujący fragment kodu:
<div class="form"> <g:javascript> function clearField(name) { document.getElementById(name).value = ''; } </g:javascript> <g:formRemote name="addCategory" url="[controller:'categories', action:'add']" update="[success:'categories', error:'message']" onSuccess="clearField('name')"> <input type="text" name="name" id="name"><br/> <input type="submit" value="Dodaj kategorię" action="add"/> </g:formRemote> </div> <div class="list" id="categories"> <g:include controller="categories" action="list"/> </div>
Można tu zauważyć trzy elementy. g:javascript, który zawiera prościutki kawałek JS do czyszczenia pola formularza. g:formRemote to specjalny formularz używany do wysyłania żądań korzystając z Ajax'a. Posiada artybuty określające jakie pole zaktualizować po odebraniu odpowiedzi oraz gdzie żądanie powinno być wysłane. g:inlude wstawia nam efekt działania akcji list kontrolera categories (chcemy inicjalnie tą listę wyświetlić).
Kontroler nie należy do zbytnio skomplikowanych:
import Category class CategoriesController { def index = { } def add = { new Category(params).save() redirect(action:"list") } def delete = { Category.get(params.id).delete() redirect(action:"list") } def list = { render(template:"list", model:[categories:Category.list(sort:"name")]) } }
Akcja index po prostu wyświetla nasz widok. Akcja list renderuje nam szablon list z zapodanym modelem (do zmiennej categories przypisujemy listę kategorii posortowaną po nazwie - to znacie z serii postów o GORM). Akcja add tworzy i zapisuje nową kategorię na podstawie parametrów żądania (dzięki Jacek za wskazanie takiego sposobu tworzenia obiektów) i przekierowuje na wyświetlenie listy. Akcja delete usuwa kategorię i też przekierowuje na wyświetlenie aktualnej listy.
Brakuje nam tylko szablonu list (szablony tworzymy jako pliki GSP z nazwą zaczynającą się od podkreślenia, np. _list.gsp). Szablon wygląda tak:
<g:each in="${categories}" var="category"> ${category.name} <g:remoteLink controller="categories" action="delete" id="${category.id}" update="categories">Usuń</g:remoteLink> <br/> </g:each>
Na początku miałem małą zagwozdkę, jak inicjalnie wczytać listę i później ją aktualizować. Miałem akcję list w kontrolerze, a akcja index dodatkowo też wczytywała kategorię. Nie podobało mi się to podwojenie logiki, a tak g:render nie rozwiązywał mojego problemu. Jednak po chwili znalazłem tag g:include.
Pytanie za 100 pkt.: dlaczego w kontrolerze umieściłem linijkę "import Category", skoro i klasa domenowa i kontroler są w tym samym pakiecie? Jako podpowiedź polecam zajrzeć na blogi Jacka i Chlebika (nie napotkali tego problemu, ale pisali, co i dlaczego warto w projektach zawsze stosować).
Jeśli ktoś chce przyjrzeć się źródłom to dostępne są na GitHubie: http://github.com/matixo/wydatki/tree/master