Ce tutoriel est destiné à appréhender le framework Grails de manière ludique, en construisant pas à pas une application simplifiée qui aura pour objectif de gérer une cave à vin personnelle.

Les étapes sont progressives et permettent à chacun d'avancer à son rythme

Au fur et à mesure des étapes, des exercices plus ou moins simples sont proposés, et des liens vers la documentation Grails vous permettent de vous aider à les résoudre.

En tout état de cause, une solution est proposée pour chaque exercice, vous êtes libre de la regarder ou pas lors de la résolution de l'exercice.


Bienvenue sur le Framework Grails !

Ce chapitre décrit comment initialiser l'environnement avec Eclipse GGTS.

Vous pouvez récupérer la dernière version à jour ici.

Pour ceux qui préfèrent utiliser un autre IDE, vous pouvez passer directement à l'étape 2.

Une fois Eclipse installé :

  • Démarrer Eclipse
  • Créer un nouveau projet : File / New / Grails Project et lui donner le nom dojo-cave-vin :
  • Démarrer le serveur d’application :

Grails utilise un mécanisme de scripts pour générer automatiquement du code ou pour exécuter des commandes du framework (build, test, démarrage du serveur, ...)
Sous Eclipse, il est possible d'appeler directement les commandes Grails depuis une console intégrée. Pour afficher la console, utiliser le raccourci Ctrl + Alt + Shift + G :

La commande help affiche la liste des commandes disponibles, le raccourci Ctrl + Espace permet d'auto-completer les commandes disponibles.


Premier Contrôleur

Exercice
    • Créer un premier contôleur à partir de la commande create-controller dojo.grails.Home
    • Afficher à l'écran le texte "Hello Cave à vin"
    • Tester le rechargement à chaud.
HomeControlleur.groovy
									package dojo.grails
									
									class HomeController {
									
									    def index() { 
									        render "Hello Cave à vin !"
									    }
									}									
								


Première Vue

Par convention, les vues sont placées dans le répertoire views, et prennent pour extension gsp (Grails Server Page).

Exercice
    • Créer une vue index.gsp dans le répertoire views/home
    • Appliquer le layout main à cette vue
    • Afficher dans la vue la chaine "Hello Cave à vin !" en utilisant une variable dynamique value retournée par le contrôleur HomeController.groovy et qui contiendra la chaine "Cave à vin".

Grails utilise la notation Grails EL (Expression Language) pour mapper les variables de la vue avec les informations transmises par les actions du contrôleur.

HomeControlleur.groovy
									package dojo.grails
									
									class HomeController {
									
									    def index() { 
									       def value = “Cave à vin“
									       [value:value]
									    }
									
									}			
								

views/home/index.gsp

Gestion des plugins

L'une des forces du framework Grails tient dans le nombre importants de plugins mis à disposition par la communauté ou directement par SpringSource.
Pour en être persuadé, jetez un oeil à la page Plugins qui propose plus de 900 plugins. Bien souvent, lorsque l'on recherche une fonctionnalité particulière, l'un des premiers réflexes est d'aller vérifier si un plugin existe déjà pour le besoin identifié et si ce dernier répond au besoin.
En particulier, on y trouve le plugin spring-security-core qui va prendre en charge dans le cadre de notre application la gestion de la sécurité, des utilisateurs, des rôles et bien plus encore.

Arrêter le serveur d'application avant d'installer un plugin.

Pour installer un plugin, rien de plus simple, il suffit d'ouvrir le fichier BuildConfig.groovy du répertoire conf et d'ajouter une entrée dans la section plugins.
Pour Spring Security, ajouter la ligne suivante : compile ":spring-security-core:1.2.7.3"

Exécuter la commande compile afin que Grails récupère depuis Internet les dépendances nécessaires :

Intégration des composants de sécurité

Comme nous pouvons le voir avec ce plugin, le mécanisme de commandes Grails est extensible.
En particulier, nous voyons que Spring Security a enrichi le jeu de commandes avec le script s2-quickstart
Ce script permet de générer le code des objets User, Role et UserRole, ainsi que les contrôleurs et vues qui permettent de tirer profit des mécanismes de sécurité fournis par Spring Security.
Exécutons ce script en précisant le package cible s2-quickstart dojo.grails User Role :


Nous allons maintenant ouvrir le fichier Config.groovy et modifier la configuration Spring Security de sorte à :

  • Permettre aux personnes anonymes d'accéder au formulaire d'authentification ;
  • N'autoriser l'accès au reste de l'application que pour les personnes ayant le rôle ROLE_USER.

Plus d'infos sur le plugin Spring Security ici.

Config.groovy
								...
								
								// Filtrage de l'acces à l'application sur le role USER
								grails.plugins.springsecurity.securityConfigType = "InterceptUrlMap"
								grails.plugins.springsecurity.interceptUrlMap = [
									 '/login/*': ['ROLE_ANONYMOUS'],
									 '/**': ['ROLE_USER']
								]								
								

Modifions les messages affichés à l'écran sur la page de connexion :


messages.properties
								...
								
								# Messages Authentification
								springSecurity.login.header=Connexion Cave à Vin
								springSecurity.login.username.label=Login
								springSecurity.login.password.label=Mot de passe
								springSecurity.login.remember.me.label=Se souvenir de moi
								springSecurity.login.button=Ok			
								springSecurity.errors.login.fail=Impossible de se connecter	
								

Lorsqu'on redémarre l'application, on obtient bien :



A présent, les accès sont bien protégés mais aucune données n'a été créée en base pour authentifier un utilisateur !
Ajoutons le jeu de données à la prochaine étape.

Gestion des environnements

De manière native, Grails offre une gestion multi-environnement qui permet d'adapter la configuration de l'application de manière spécifique à chaque environnement cible :

  • development : environnement par défault (phase de développement)
  • test : utilisé pour l'exécution des tests unitaires
  • production : environnement configuré pour la production
  • ... tout autre environnement spécifique
La configuration propre à chaque environnement se retrouve essentiellement dans les fichiers Config.groovy et DataSource.groovy
Exemple dans DataSource.groovy :
									environments {
									    development {
									        dataSource {
									            dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
									            url = "jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
									        }
									    }
									    test {
									        dataSource {
									            dbCreate = "update"
									            url = "jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
									        }
									    }
									    production {
									        dataSource {
									            dbCreate = "update"
									            ...
									        }
									    }
									}
							

Chargement de données

Grails propose de manière native un mécanisme qui permet d'initialiser un jeu de données au démarrage de l'application.
Pour cela, Grails fournit le fichier BootStrap.groovy exécuté lors de chaque démarrage.
Notre application étant démarrée en environnement de type development, les données sont stockées dans une base mémoire H2.
Nous allons créer les données qui permettent d'alimenter la base de données avec un utilisateur de test :

BootStrap.groovy
							    import grails.util.Environment
								import dojo.grails.Role
								import dojo.grails.User
								import dojo.grails.UserRole
							    
							    class BootStrap {
							    
								    def init = { servletContext ->
										if (Environment.current == Environment.DEVELOPMENT) {
											def roleUser = new Role(authority: 'ROLE_USER').save()
											def user = new User(username: 'dojo', password: 'dojo', enabled: true).save()
											UserRole.create user, roleUser, true
										}
								    }
								    
								    ...
							    }								
							

On peut désormais redémarrer et se connecter avec le compte dojo/dojo.

Templates Grails

Afin de factoriser les fragments de pages, Grails offre le mécanisme de templates : il s'agit d'un fichier gsp dont le nom commence par le caractère "_".
On peut ensuite faire appel au template à partir du tag g:render

Factorisons le menu de l'application en créant un template dans le répertoire grails-app/views/menu :

_nav.gsp

Maintenant, il suffit d'inclure le tag <g:render template="/menu/nav" /> directement dans le layout global grails-app/views/layouts/main.gsp pour appliquer le menu à toute l’application.


Look and Feel

Afin d'harmoniser l'ensemble des pages, le layout et les CSS de notre application de démo ont été retravaillés.
Pour éviter de surcharger ce tutoriel, vous pouvez recopier directement les ressources suivantes dans votre projet :

Taglibs

La création d'interfaces HTML plus ou moins riches est simplifiée par l'utilisation de Taglibs.
Grails propose un mécanisme de namespaces pour permettre aux plugins de proposer leur propre jeu de tags.
Le namespace par défault de Grails est "g", celui de Spring Security est sec.

Menu Dynamique

Exercice

Utiliser les différentes taglibs Grails et Spring Security pour arriver au résultat ci-dessous :


    • Gérer le lien de déconnexion (appel du controller logout sur l'action index)
    • Mettre un lien vers la page d'accueil (utilisation du tag g:link)
    • Protéger l'affichage du menu avec le tag Spring Security sec:ifLoggedIn
_nav.gsp

Modèle Métier

Grails intègre une surcouche à Hibernate nommée GORM (Grails ORM).
Cette couche est destinée à masquer les complexité d'Hibernate et permet de :

  • Modéliser les objets de domaine,
  • Configurer le mapping avec la base de données,
  • Décrire les relations entre entités,
  • Modéliser les contraintes sur les champs,
  • Apporter tout l'outillage pour le requêtage : finder dynamique, recherche par Criteria, requêtes HQL

Plus d'informations sur la documentation GORM ici

Dans le cadre de notre application, Spring Security s'étant occupé de générer les objets de domaine User et Role, il reste à modéliser les entités Cave et Bouteille.


  • Pour cela :
    • Arrêter le serveur d'application
    • Créer la première entité Cave en utilisant le script Grails create-domain-class dojo.grails.Cave
    • Ce script créé automatiquement deux fichiers : Cave.groovy et CaveTests.groovy
    • Nous reviendrons plus tard sur la classe de test
Exercice
  • Modéliser la relation (simplifiée) uni-directionnelle 0-1 entre User et Cave :
    • Modifier l'entité User pour ajouter une référence à Cave et indiquer que cet attribut est nullable.
    • Modifier l'entité Cave pour :
      • Ajouter l'attribut name afin qu'il respecte les contraintes suivantes : unique, obligatoire, n'autorise pas de chaine vide, avec une longueur maximale de 15 caractères.
      • Déclarer que l'entité Cave dépend de User
User.groovy
							...							
							Cave cave
							...		
							
							static constraints = {
								...
								cave nullable: true
							}
							

Cave.groovy
							class Cave {
							
							    String name
								
							    static belongsTo = [user: User]
								
							    static constraints = {
									name blank: false, unique: true, maxSize: 15
							    }
							}
							

Test unitaires

  • Grails distingue principalement deux types de tests :
    • Unitaires : permettent de tester un comportement, ou une classe isolée, sans charger l'application complète ;
    • Intégration : exécutés en démarrant l'application complète, c'est à dire en chargeant toutes les dépendances du conteneur IOC, en exécutant le mapping ORM, en chargeant le jeu de données contenu dans Bootstrap.groovy dans une base de données réelle, ...

Comme nous l'avons vu lors de la génération de la classe de domaine Cave, le script create-domain-class a également généré le squelette de la classe de test CaveTests.groovy.
Il est temps à présent de compléter cette classe de tests.

Exercice

Compléter le test unitaire du fichier CaveTests.groovy de sorte à tester les différentes contraintes, et faire en sorte que la méthode validate fournie par Grails sur les objets de domaine soit rendue passante.

Vous pouvez exécuter les tests soit à partir de l'IDE : clic droit sur la classe de test puis Run As / Grails command (test-app), soit via le script test-app -unit CaveTests.

CaveTests.groovy
								@TestFor(Cave)
								class CaveTests {
								
								    void testCaveConstraints() {
								       def cave = new Cave(name: "cave")
								
									   // Permet de mocker la methode validate
									   mockForConstraintsTests(Cave, [cave])
									   
									   // Test sans user attache a l'instance de cave
									   assertFalse cave.validate()
									   
									   // Test nullable
									   def testCave = new Cave()
									   assertFalse testCave.validate()
								       assertEquals "nullable", testCave.errors["name"]
								
									   // Test blank
									   testCave = new Cave(name: "")
									   assertFalse testCave.validate()
									   assertEquals "blank", testCave.errors["name"]
									   
									   // Test maxSize
									   testCave = new Cave(name: "Mon nom de cave beaucoup trop long")
									   assertFalse testCave.validate()
									   assertEquals "maxSize", testCave.errors["name"]
									   
									   // Test unique
									   testCave = new Cave(name: "cave")
									   assertFalse testCave.validate()
									   assertEquals "unique", testCave.errors["name"]
									   
									   // Test ok
									   User fakeUser = new User(username: 'fake', password: 'fake')
									   testCave = new Cave(name: "Ma cave", user: fakeUser)
									   assertTrue testCave.validate()
									   assertEquals 'fake', testCave?.user?.username
								    }
								}
							

Modèle métier - Partie 2

Ajoutons à présent le relation OneToMany entre Cave et Bouteille.

Exercice
    • Créer la classe de domaine Bouteille
    • Ajouter les attributs :
      • label : chaine unique non vide
      • millesime : entier compris entre 1900 et 2013
      • photo : attribut de type binaire, non obligatoire, avec une taille max de 65Ko (maxSize: 65536)
    • Configurer la relation OneToMany entre Cave et Bouteille et configurer le mapping de sorte à supprimer les instances Bouteille orphelines en cascade lors de la suppression d'une Cave.
Bouteille.groovy
		
							class Bouteille {
							
								String label
								Integer millesime
								byte[] photo
								
								static belongsTo = [cave: Cave]
								
							    static constraints = {
									label blank: false, unique: true
									millesime min: 1900, max: 2013
									photo nullable: true, maxSize: 65536
							    }
							}							
							

Cave.groovy
							class Cave {
								...
								
								static hasMany = [bouteilles: Bouteille]
									
							    static mapping = {
							        bouteilles cascade: 'all-delete-orphan'
							    }
							    
								...	
							}
								
							

Tests d'intégration

Afin d'éprouver notre modèle complet en condition réelle, écrivons notre premier test d'intégration.

Pour créer le squelette de la classe de test, exécutons le script create-integration-test dojo.grails.CaveIntegration

Exercice

Compléter la classe de test en créant une instance Cave, contenant deux bouteilles et en faisant des tests sur le résultat après enregistrement en base de données.
Vous pourrez utiliser la methode .save(failOnError: true, flush: true) pour la sauvegarde, la méthode dynamique addTo pour ajouter une bouteille à la collection de bouteilles de la cave, ainsi que le finder dynamique findByUsername pour retrouver l'utilisateur que vous aurez créer au préalable dans la méthode setUp() de la classe de test.

CaveIntegrationTests.groovy
						class CaveIntegrationTests {
						
						    @Before
						    void setUp() {
						        // Setup logic here
								new User(username: 'test', password: 'test').save(flush: true)
						    }
						
						    @After
						    void tearDown() {
						        // Tear down logic here
						    }
						
						    @Test
						    void "Creation nominale d'une Cave avec liste de bouteilles"() {
								// On recupere le user de test
								def user = User.findByUsername('test')
								assertNotNull user
								
						        def cave = new Cave(name: "Ma cave", user: user)
								def bouteille1 = new Bouteille(label: "Ma bouteille 1", millesime: 2012)
								def bouteille2 = new Bouteille(label: "Ma bouteille 2", millesime: 2012)
								cave.addToBouteilles(bouteille1);
								cave.addToBouteilles(bouteille2);
								cave.save(failOnError: true, flush: true)
								
								assertNotNull cave
								assertNotNull cave.name
								def found = Cave.findByName("Ma cave")
								assertEquals 'Ma cave', found.name
								assertEquals found.bouteilles*.millesime, [2012, 2012]
						    }
						}							
						

Scaffolding

Le mécanisme de Scaffolding apporté par Grails permet de générer automatiquement les vues et les actions de contrôleur permettant de traiter automatiquement les aspects CRUD d'un objet de domaine, en tenant compte des contraintes, et du mapping d'un objet métier.


Prérequis

Pour simplifier le mécanisme dans l'application de démo "Cave à Vin", nous ne génèrerons que le Scaffolding de l'objet Bouteille ; pour cela, modifions le fichier BootStrap.groovy pour associer directement une instance de Cave à notre utilisateur ogil :


BootStrap.groovy
							...
							
							// Simplification du jeu de données
							Cave cave = new Cave(id: 1L, name: "Ma Cave perso", user: user).save(flush: true)
							
							...								
							

Modifions le mapping d'URL de la page d'accueil par défaut pour utiliser notre contrôleur Home déclaré au début du tutoriel :

UrlMappings.groovy
							static mappings = {
								"/$controller/$action?/$id?"{
									constraints {
										// apply constraints here
									}
								}
						
								"/"(controller:"home", action:"/index")
								"500"(view:'/error')
							}			
							

Modifions notre contrôleur Home pour récupérer notre cave à vin :

HomeControlleur.groovy
							    def index() { 
									Cave cave = Cave.get(1L)
									[cave: cave]
								}							
							

Modifions la vue views/home/index.gsp pour afficher le nom de notre cave :

views/home/index.gsp

Démarrer le serveur et vérifier le résultat :

Scaffolding Dynamique

Faisons un premier essai en testant le Scaffolding dynamique sur l'entité Bouteille :

create-controller dojo.grails.Bouteille

Modifier le contrôleur :

BouteilleController.groovy
							class BouteilleController {
							
								static scaffold = true
							}
							

Ajoutons une entrée dans le menu gauche pour accéder à la page de gestion des bouteilles :

views/menu/_nav.gsp

Redémarrer et essayer de créer une bouteille avec des données erronées, on obtient :


Créer une bouteille avec des données correctes : les différentes fonctions CRUD sont opérationnelles :

Scaffolding statique

Sans avoir écrit une seule ligne de code, nous avons déjà obtenu résultat interessant avec le Scaffolding statique.
Toutefois, on se rend bien compte que ce n'est pas adapté à une utilisation directe en production.
Voyons à présent comment Grails peut nous aider à améliorer notre productivité, tout en continuant à nous faire profiter de la puissance du Scaffolding, mais en utilisant cette fois le Scaffolding statique.


Utilisons cette fois la commande suivante :


generate-all dojo.grails.Bouteille

Accepter l'overwrite des fichiers au prompt dans la console.


Cette fois, le contrôleur BouteilleController.groovy, la classe de test BouteilleControllerTests.groovy ainsi que l'ensemble des vues views/bouteille/* ont été générés.
Cela nous permet d'avoir un contrôle complet sur l'ensemble des éléments générés par le Scaffolding.

Exercice

Retoucher les différentes vues pour améliorer le rendu des différentes pages. A minima, retoucher les vues list, show ainsi que le contrôleur pour faire en sorte d'afficher les photos des bouteilles.

Vous pouvez utiliser les images de bouteilles mis à disposition dans le dossier image/bouteilles du tutoriel.

  • Aide :
    • Pour afficher une image binaire, vous pouvez utiliser l'action suivante dans le contrôleur BouteilleController :
      BouteilleController.groovy
      											class BouteilleController {
      												...
      												
      												def displayPhoto() {
      													if (!params.id) {
      														response.sendError(404)
      														return
      													}
      											
      													def bouteille = Bouteille.get(params.id)
      													if (bouteille.photo) {
      														byte[] image = bouteille.photo
      														response.setHeader('Cache-Control', 'no-cache')
      														response.outputStream << image
      														response.outputStream.flush()
      													}
      												}												
      											}
      											
    • Tag createLink
views/bouteille/list.gst

Pour en savoir plus sur le scaffolding : ici.
En particulier, vous verrez qu'il est possible de customiser entièrement les templates utilisés par Grails pour la génération du code du contrôleur, vues, et classes de test : http://grails.org/doc/latest/ref/Command%20Line/install-templates.html

Services Métiers

Bien que les services métiers ne soient pas directement intégrés dans la génération du Scaffolding, Grails recommande fortement de déplacer la logique métier dans des méthodes de services, qui permettent de mieux réutiliser le code, et qui prennent aussi en charge les aspects transactionnels.
Dans le cadre de notre application de démo, crééons notre premier et unique service CaveVinService, à partir de la commande Grails create-service dojo.grails.CaveVinService.


Exercice

Ajoutez une méthode de service qui permettra de récupérer les 5 dernières bouteilles créées par l'utilisateur connecté.
Amusez-vous à écrire la requête de récupération en HQL, Criteria et via le mécanisme de Finders dynamiques.
Tester les trois méthodes avec un test d'intégration généré à partir de la commande : create-integration-test dojo.grails.CaveVinServiceIntegration

  • Aide :
    • L'injection de dépendances est réalisée par nom. Par exemple, pour injecter le service SpringSecurityService qui permet de récupérer l'utilisateur courant, on déclarera un attribut : SpringSecurityService springSecurityService
    • L'utilisateur courant peut être récupéré en appelant springSecurityService.getCurrentUser()
    • Services Grails
    • Finders dynamiques
    • Requêtes HQL
    • Requêtes avec Criteria

CaveVinService.groovy
							class CaveVinService {
							
								static transactional = false
								
								// Injection du service Spring Security
								SpringSecurityService springSecurityService
								
								final static int LIMIT = 5
								
							    def findBouteillesRecentesByDynamicFinder() {
									def current = springSecurityService.getCurrentUser()
									return Bouteille.findAllByCave(current.cave, [max: LIMIT, sort: "id", order: "desc"])
								}
								
								def findBouteillesRecentesByHQL() {
									def current = springSecurityService.getCurrentUser()
									return Bouteille.executeQuery("FROM Bouteille b where b.cave = :cave order by b.id desc",
							                     [cave: current.cave, max: LIMIT])
								}						
								
							    def findBouteillesRecentesByCriteria() {
									def current = springSecurityService.getCurrentUser()
									def criteria = Bouteille.createCriteria()
									criteria.list {
										cave {
											eq('id', current.cave?.id)
										}
										maxResults LIMIT
										order 'id','desc'
									}
								}
							}														
							

CaveVinServiceIntegrationTests.groovy
							class CaveVinServiceIntegrationTests {
								// Injection des services
								def springSecurityService
								CaveVinService caveVinService
								
								// Donnees de test attendues
								static final expectedList =  ['bouteille6', 'bouteille5', 'bouteille4', 'bouteille3', 'bouteille2']
								static final expectedNumber = 5
								
							    @Before
							    void setUp() {
							        
									def user = new User(username: 'test', password: 'test').save(flush: true)
							
									// Force l'authentification du user 'test'
									springSecurityService.reauthenticate 'test'
									
									def cave = new Cave(name: "Ma cave", user: user)
									int index = 1
									def bouteille
									
									6.times {
										bouteille = new Bouteille(label: 'bouteille' + index, millesime: 1950)
										cave.addToBouteilles(bouteille)
										// On force la sauvegarde à chaque iteration pour garantir l'ordre de creation pour notre test
										cave.save(flush: true)
										index++
									}
									
							    }
							
							    @Test
							    void "Recherche des bouteilles recentes par dynamic finders"() {
							        def bouteilles = caveVinService.findBouteillesRecentesByDynamicFinder()
									assertEquals bouteilles.size, expectedNumber
									assertEquals bouteilles*.label, expectedList
							    }
								
								@Test
								void "Recherche des bouteilles recentes par HQL"() {
									def bouteilles = caveVinService.findBouteillesRecentesByHQL()
									assertEquals bouteilles.size, expectedNumber
									assertEquals bouteilles*.label, expectedList
								}								
								
								@Test
								void "Recherche des bouteilles recentes par Criteria"() {
									def bouteilles = caveVinService.findBouteillesRecentesByCriteria()
									assertEquals bouteilles.size, expectedNumber
									assertEquals bouteilles*.label, expectedList
								}
								
								@After
								void tearDown() {
									// Tear down logic here
								}
							}													
							

Grails et Ajax

Terminons notre application de démo en intégrant une fonctionnalité Ajax, de sorte à voir comment Grails addresse cette problématique récurrente.

Exercice

Pour faire simple, nous allons afficher en Ajax sur la page d'accueil, la liste des 5 dernières bouteilles ajoutées, en faisant appel à notre service précédenment créé.

  • Aide :
    • Ajouter dans le contrôleur HomeController une action bouteillesRecentes qui fera appel à notre service CaveVinService pour retourner la liste des bouteilles récentes.
    • Créer une page bouteillesRecentes.gsp dans views/home/ qui permettra d'afficher la liste des dernières bouteilles
    • Modifier la page views/home/index.gsp pour afficher au chargement de la page la liste des 5 dernières bouteilles en utilisant un appel Ajax.
    • Pour effectuer l'appel Ajax, vous pouvez soit utiliser une fonction standard JQuery (intégré par défault avec Grails), soit utiliser le tag Grails g:remoteFunction
    • Les appels Javascript (JQuery ou remoteFunction) doivent être effectués a l'intérieur d'un tag g:javascript
    • Tag g:remoteFunction
    • Tag g:javascript

HomeController.groovy
							class HomeController {
							
								CaveVinService caveVinService
								
								...
								
								def bouteillesRecentes() {
									[bouteilles: caveVinService.findBouteillesRecentesByCriteria()]
								}
							}
							

views/home/bouteillesRecentes.gsp
views/home/index.gsp

Les fichiers sources de l'application Cave à vin se trouvent ici.