Introduction
Voici le premier article de mon blog, je le dédie à ma nouvelle passion, Groovy & Grails. Le choix du sujet de l’article s’est porté sur la transformation d’une simple application Grails, générée en scaffold statique, vers une application AJAX. Cette transformation se base sur les composants AJAX de Grails et sur le framework javascript jQuery (le choix de jQuery est tout simplement basé sur mes préférences, je pouvais aussi bien utiliser les autres Frameworks Javascript comme Prototype, scriptaculous).
Cadre de cet article
C’est un article destiné aux lecteurs ayant un minimum de connaissances autour de Grails. L’objectif n’étant pas d’introduire et d’expliquer comment marche Grails, mais de montrer une simple technique de transformation.
L’idéal serait que Grails permette de faire du scaffold AJAX, peut être que cet article introduirait cette idée si elle est jugée intéressante.
Etape 1 : Création des classes de domaines
Pour cette application, j’ai utilisé la version 1.2.0 de Grails et Spring Tools Suite 2.3.0.
L’exemple traité dans cet article est une application de gestion de produits organisés par catégorie.
Commençons donc par créer l’application gProducts en utilisant soit :
Les commandes Grails
grails create-app gProducts
L’environnement de développement STS
A partir de cette étape, les commandes Grails seront exécutées en utilisant la console Grails de l’environnement de développement STS comme suit :
Ensuite ajoutons deux classes de domaine Categorie et Produit
grails create-domain-class fr.exemples.gproducts.Categorie grails create-domain-class fr.exemples.gproducts.Produit
Modifions maintenant les propriétés des deux classes de domaines comme suit:
package fr.exemples.gproducts class Categorie { String nom String description String image String ordre static hasMany = [produits: Produit] static constraints = { nom(blank:false) description(blank:false) image(nullable:true) ordre (default:0) } }
et
package fr.exemples.gproducts class Produit { String nom String description String image float prix static belongsTo = [categorie : Categorie] static constraints = { nom(blank:false) description(blank:false) image(nullable:true) prix() } }
Listing 1 : code des classes de domain
Etape 2 : Création des contrôleurs et scaffold statique
Une fois que nos classes persistantes sont créées, il suffit de générer les contrôleurs Grails et les vues correspondantes en scaffold statique. Pour ce faire, il suffit d’exécuter les commandes de génération suivantes :
grails generate-all fr.exemples.gproducts.Categorie grails generate-all fr.exemples.gproducts.Produit
Ajoutons ensuite quelques données dans la base pour avoir un jeu de test. Pour cela, il faut modifier le fichier grails-app/conf/BootStrap.groovy comme suit :
import fr.exemples.gproducts.Produit class BootStrap { def init = { servletContext -> def ordinateurs = new Categorie(nom : "Ordinateurs de bureau", description : "Catégorie des ordinateurs de bureau.", ordre:1, image:"").save(); ordinateurs.save(); def portables = new Categorie(nom : "Ordinateurs portables", description : "Catégorie des ordinateurs portables.", ordre:2, image:""); portables.save(); def accessoires = new Categorie(nom : "Accessoires", description : "Catégorie des accessoires pour ordinateur.", ordre:3, image:""); accessoires.save(); new Produit (nom :"PACKARD BELL iMedia A4365 FR", description:"PACKARD BELL iMedia A4365 FR", image:"PACKARD_BELL_iMedia_A4365_FR.jpg", prix:447, categorie:ordinateurs).save(); new Produit (nom :"HP TouchSmart 300-1025fr", description:"HP TouchSmart 300-1025fr", image:"HP_TouchSmart_300-1025fr.jpg", prix:750, categorie:ordinateurs).save(); new Produit (nom :"ASUS EeeTop PC ET2002T-B0037 tactile", description:"ASUS EeeTop PC ET2002T-B0037 tactile", image:"", prix:561, categorie:ordinateurs).save(); new Produit (nom :"DELL Inspiron 1470-8471", description:"DELL Inspiron 1470-8471", image:"DELL_Inspiron_1470-8471.jpg", prix:569, categorie:portables).save(); new Produit (nom :"ASUS X70AC-TY033V", description:"ASUS X70AC-TY033V", image:"ASUS_X70AC-TY033V.jpg", prix:499, categorie:portables).save(); new Produit (nom :"Sony Netbook VAIO VPC-W11S1E/T", description:"Sony Netbook VAIO VPC-W11S1E/T", image:"Son_Netbook_VAIO_VPC-W11S1E_T.jpg", prix:365, categorie:portables).save(); } def destroy = { } }
Listing 2 : Code de la class BootStrap
Nous pouvons par la suite tester l’application générée en exécutant la commande
grails run-app
et en ouvrant l’url http://localhost:8080/gProducts
Observons l’application résultante
Etape 3 : modification du layout et ajout des plugins nécessaires
Cette étape consiste à modifier le layout de la page pour avoir un menu à gauche permettant d’accéder aux interfaces de gestion des catégories et produits. Ce menu affichera aussi une liste de catégories permettant d’accéder rapidement aux produits correspondants.
Installation du plugin jQuery
Comme introduit au début de cet article, le framework Javascript qui sera utilisé est jQuery. Or ce dernier n’est pas supporté en natif dans Grails donc il faut passer par un plugin dédié : jQuery Plugin
Commençons par ajouter ce plugin en exécutant la commande Grails suivante :
grails install-plugin jquery
Ce plugin va permettre de transformer le code généré par les composants Grails de type AJAX (g:remoteLink, g:formRemote, g:submitToRemote) en utilisant le framework jQuery.
Au jour d’aujourd’hui, ce plugin est à la version 1.3.2.4
Modification du layout
Nous allons introduire dans le layout par défaut du scaffold, un menu à gauche de la page. Ce menu sera donc matérialisé par une page GSP partielle, nommée « _leftbar.gsp » dans le dossier « views/common ». L’appel à cette page se fait dans le layout principal, c’est-à-dire dans le fichier « views/layout/main.gsp », en utilisant la balise « g:render ».
Voici le contenu du fichier « main.gsp » après les modifications effectuées :
<html> <head> <title><g:layoutTitle default="Grails" /></title> <link rel="stylesheet" href="${resource(dir:'css',file:'main.css')}" /> <link rel="shortcut icon" href="${resource(dir:'images',file:'favicon.ico')}" type="image/x-icon" /> <g:layoutHead /> <g:javascript library="jquery" /> <script type="text/javascript" src="${resource(dir:'js',file:'gproducts.js')}"></script> </head> <body> <div id="container"> <div id="header"> <div id="spinner"> <div id="ajax-load-notification">Loading...</div> </div> <div id="grailsLogo" class="logo"> <a href="http://grails.org"> <img src="${resource(dir:'images',file:'grails_logo.png')}" alt="Grails" border="0" /> </a> </div> </div> <div id="wrapper"> <div id="body"> <g:layoutBody /> </div> </div> <div id="leftbar"> <g:render template="/common/leftbar" /> </div> <div id="footer"> <span class="footer">nabiladouani.fr copyright 2010</span> </div> </div> </body> </html>
Listing 3 : Code du fichier main.gsp
La ligne 7 du listing 3 montre l’appel au plugin jQuery. Cette balise permet d’indiquer à Grails d’utiliser le framework jQuery pour la génération du code Javascript pour effectuer les appels asynchrones.
La ligne 8 du listing 3 permet d’importer le fichier « gproducts.js » qui contient un ensemble de méthodes Javascript spécifiques à notre application.
Le contenu de ce fichier est le suivant et contient une méthode d’initialisation permettant de gérer l’affichage d’une indication, avant et après chaque appel AJAX. Cette méthode permet aussi de gérer le cas d’erreur dans les réponses AJAX:
$(document).ready(function(){ $("#ajax-load-notification").hide(); $("#ajax-load-notification").ajaxStart(function(){$(this).show();}); $("#ajax-load-notification").ajaxComplete(function(event,request, settings){$(this).hide();}); $("#body").ajaxError(function(event,request, settings){ jQuery('#body').html(request.statusText); }); });
Listing 4 : Code du fichier gproducts.js
La ligne 28 du listing 3 montre l’appel à la page GSP partielle de la barre de menu de gauche. Le contenu de cette page GSP est le suivant :
<g:setProvider library="jquery"/> <div class="menu"> <span class="title">Menu principal</span> </div> <ul> <li> <g:remoteLink controller="categorie" action="list" update="body"> Categories </g:remoteLink> </li> <li> <g:remoteLink controller="produit" action="list" update="body"> Produits </g:remoteLink> </li> </ul>
Listing 5 : Code du fichier _leftbar.gsp
Remarquons que sur la ligne 1 du listing 5, nous indiquons à Grails que dans cette GSP partielle, nous allons utiliser jQuery comme framework Javascript pour nos appels asynchrones. Ces derniers étant à générer pour toute les balises « g:remoteLink » de cette GSP.
L’attribut « update » renseigne sur le nom de l’élément HTML à mettre à jour avec les résultats des appels asynchrones, dans notre cas, il s’agit de la div ayant « body » comme id.
L’application est maintenant prête, nous pouvons tester la nouvelle fonctionnalité ajoutée. Lançons l’application et vérifions le résultat :
Nous remarquons dans cet écran une anomalie. En effet, un clic sur le menu « Catégories », fait un appel AJAX vers la liste des catégories. Le résultat de cette requête est affiché incluant un header et un footer. C’est tout à fait normal, il fallait changer le layout de page GSP de la liste des catégories. La même règle est à appliquer pour l’ensemble des vues générées par le scaffold statique de Grails, qui seront appelées en asynchrone.
Pour cela, nous allons créer une nouvelle page « ajax.gsp » dans « views/layout » et nous allons modifier le meta « layout » des différentes vues du scaffold.
<g:setProvider library="jquery"/> <g:layoutBody />
Listing 6 : Code du fichier views/layout/ajax.gsp
Dans le listing 6, nous remarquons la spécification de jQuery comme librairie Javascript.
Voici un exemple de modification du meta « layout » de la page liste de catégories :
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta name="layout" content="ajax" /> <g:set var="entityName" value="${message(code: 'categorie.label', default: 'Categorie')}" /> <title><g:message code="default.list.label" args="[entityName]" /></title> </head> ... </html>
Listing 7 : Modification du meta layout
Suite à ces modifications, la navigation en utilisant le menu à gauche s’effectue correctement :
Etape 4 : Transformation des liens HTML
Après l’ajout du plugin jQuery et la modification du layout, il reste maintenant à modifier :
- les liens directs en liens invoquant des appels AJAX
- les liens des colonnes de tri et les liens de pagination
- les différentes actions des différents formulaires
Pour expliquer les modifications à apporter, nous allons se baser sur la gestion des catégories comme exemple. Les mêmes manipulations sont à apporter sur la gestion des produits.
Transformation des liens en liens invoquant des appels AJAX
Il s’agit des liens vers les pages de détails (pages de type show), des liens vers les formulaires de création et vers les pages de liste (à partir de la barre de navigation contextuelle).
Cette transformation est toute simple, il s’agit juste de remplacer les balises « g:link » par des balises « g:remoteLink » et renseigner l’attribut « update »
Prenons l’exemple de la page liste de catégories. Les liens à transformer sont :
- Le lien « New Categorie » dans la barre de navigation qui se trouve au dessus de la liste
- Les liens qui se trouvent dans la colonne « id » qui permettent d’afficher les détails d’une catégorie.
Après transformation, nous obtenons le code suivant :
<div class="nav"> <span class="menuButton"> <a class="home" href="${createLink(uri: '/')}">Home</a> </span> <span class="menuButton"> <g:remoteLink update="body"class="create" action="create"> <g:message code="default.new.label" args="[entityName]" /> </g:remoteLink> </span> </div>
Listing 8: Modification du lien « New Categorie »
<g:each in="${categorieInstanceList}" status="i" var="categorieInstance"> <tr class="${(i % 2) == 0 ? 'odd' : 'even'}"> <td> <g:remoteLink update="body" action="show" id="${categorieInstance.id}"> ${fieldValue(bean: categorieInstance, field: "id")} </g:remoteLink> </td> ... </tr> </g:each>
Listing 9 : Modification du lien « Show Catégorie »
Il nous reste à appliquer les modifications sur le reste des balises « g:link » dans les différentes pages GSP de l’application.
N’oublions pas d’ajouter, dans toutes les pages ayant des liens à transformer, la balise suivante
Transformation des liens de tri et de pagination en pagination AJAX
Les liens de tri dans les colonnes des listes
Les colonnes triables utilisées dans les vues liste de catégories et liste de produits (générées par le scaffold) utilisent la balise « g:sortableColumn ». Cette balise permet de générer un lien HTML vers l’action de liste dans le contrôleur correspondant.
Pour générer des liens de type AJAX offrant cette fonctionnalité de tri, nous allons utiliser un nouveau plugin nommé : Remote Pagination. Ce plugin offre deux balises différentes :
- util:remoteSortableColumn : créer des colonnes de tri en AJAX
- util:remotePaginate : créer des colonnes de pagination en AJAX (sera utilisée dans le prochain paragraphe)
Nous commençons par installer le plugin :
grails install-plugin remote-pagination
Ensuite nous transformons les balises « g:sortableColumn » par « util:remoteSortableColumn » dans les pages de liste. Exemple pour la liste des catégories
<div class="list"> <table> <thead> <tr> <util:remoteSortableColumn update="body" action="list" property="id" title="${message(code: 'categorie.id.label', default: 'Id')}" /> <util:remoteSortableColumn update="body" action="list" property="nom" title="${message(code: 'categorie.nom.label', default: 'Nom')}" /> <util:remoteSortableColumn update="body" action="list" property="description" title="${message(code: 'categorie.description.label', default: 'Description')}" /> <util:remoteSortableColumn update="body" action="list" property="image" title="${message(code: 'categorie.image.label', default: 'Image')}" /> <util:remoteSortableColumn update="body" action="list" property="ordre" title="${message(code: 'categorie.ordre.label', default: 'Ordre')}" /> </tr> </thead> ... </table> </div>
Listing 10 : Modification des colonnes du tableau liste des catégories
Nous remarquerons que la balise « util:remoteSortableColumn » nécessite la spécification de l’action à exécuter dans le contrôlleur correspondant, et un attribut « update » qui renseigne sur l’élément HTML à modifier avec le résultat de l’appel AJAX.
N’oublions pas d’ajouter, dans toutes les pages ayant des liens à transformer, la balise suivante
Rappelons que cette balise est nécessaire pour utiliser jQuery pour effectuer les appels AJAX.
Nous pouvons maintenant tester le tri des colonnes des listes de catégories et de produits.
Pagination
Cette transformation est aussi simple que la précédente, il suffit d’utiliser la balise « util:remotePaginate » au lieu de la balise « g:paginate » en renseignant l’action à appeler dans le contrôleur en correspondant, ainsi que l’attribut « update ». Voici un exemple d’utilisation :
<div class="paginateButtons"> <util:remotePaginate update="body" action="list" total="${categorieInstanceTotal}" /> </div>
Listing 11 : Modification liens de pagination de la liste des catégories
Transformation des actions des formulaires
Les formulaires générés par le scaffold de Grails sont trois types :
- Les formulaires de création: contiennent un seul bouton de submit
- Les formulaires de consultation: contiennent deux boutons « edit » et « delete »
- Les formulaires d’édition : contiennent deux boutons « update » et « delete »
Modification des formulaires de création
Les modifications à apporter aux formulaires de création sont simples. Il s’agit, en effet, de remplacer la balise « g:form » par la balise « g:formRemote » et renseigner les attributs suivants :
- name : le nom du formulaire
- update : l’id de l’élément HTML à mettre à jour avec le retour de l’appel AJAX
- url : une map ayant un élément renseignant le nom de l’action à appeler dans le contrôleur correspondant
Voici la nouvelle déclaration du formulaire de création de catégorie
<g:formRemote url="[controller:'categorie', action:'save']" update="body" method="post" > ... </g:formRemote>
Modification des formulaires de consultation
Les transformations à faire dans le cas de ce type de formulaire consistent à :
- remplacer les balises g:actionSubmit par g:submitToRemote pour les deux boutons « edit » et « delete »
- renseigner l’attribut « update » pour les deux boutons
- renseigner l’attribut « id » pour les deux boutons, la valeur de cet attribut étant l’id de l’objet domaine en cours de consultation
- remplacer l’événement « onclick » du bouton « delete » par l’attribut « before » comme suit :
before="if(!confirm('${message(code: 'default.button.delete.confirm.message', <strong>default</strong>: 'Are you sure?')}')) return false;"
L’attribut « before » correspond au code Javascript à exécuter avant l’appel AJAX. Dans notre cas, ce code correspond à demander une confirmation de suppression.
Voici l’exemple de transformation du formulaire de consultation d’une catégorie
<div class="buttons"> <g:form> <span class="button"> <g:submitToRemote id="${categorieInstance?.id}" update="body" class="edit" action="edit" value="${message(code: 'default.button.edit.label', default: 'Edit')}" /> </span> <span class="button"> <g:submitToRemote id="${categorieInstance?.id}" update="body" class="delete" action="delete" value="${message(code: 'default.button.delete.label', default: 'Delete')}" before="if(!confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}')) return false;" /> </span> </g:form> </div>
Modification des formulaires d’édition
Les dernières transformations concernent les formulaires d’édition. Ces transformations sont aussi simples que les précédentes :
- transformer la balise « g:form » en « g:formRemote » en pointant l’action du formulaire sur l’action « update », en renseignant l’attribut « update » et en lui donnant un nom
- garder le bouton « update » sans modification
- transformer le bouton « delete » comme pour les formulaires de consultation
Voici l’exemple du formulaire d’édition d’une catégorie
<g:formRemote name="updateForm" update="body" url="[controller:'categorie', action:'update']" method="post" > ... <div class="buttons"> <span class="button"> <g:actionSubmit class="save" action="update" value="${message(code: 'default.button.update.label', default: 'Update')}" /> </span> <span class="button"> <g:submitToRemote id="${categorieInstance?.id}" update="body" class="delete" action="delete" value="${message(code: 'default.button.delete.label', default: 'Delete')}" before="if(!confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}')) return false;" /> </span> </div> </g:formRemote>
Conclusion
Durant cet article, nous avons pu tester la transformation d’une application simple générée par Grails, en une application entièrement en AJAX en gardant les fonctionnalités de :
- CRUD
- Pagination
- Tri des colonnes des listes
- Validation des formulaires
Nous avons aussi pu utiliser deux plugins utilitaires :
- jQuery plugin : pour utiliser jQuery comme framework javascript pour les routines AJAX
- Remote Paginate plugin : pour effectuer la pagination et le tri en AJAX
J’espère que mon premier article est réussi et que l’idée soit considérée intéressante, en attendant d’autres articles autour de Grails.
Les sources de l’application sont disponibles ici.
Article bien sympa. Par contre, j’ai du louper un truc sous STS, comment tu fais apparaitre la console Grails? (je n’ai pas vu comment faire autrement que Run as>grails command…)
Merci Christophe,
pour la console Grails, j’ai modifié mes préférences STS : dans Window > Preferences > General > Key, tu peux rechercher « Open Grails Command Prompt » et tu choisi ton nouveau raccourci.
Par défaut il faut un « Ctrl + Alt + Shirt + G » en ayant sélectionné le projet concerné par la commande.
Personnellement je l’ai pointé sur un « Ctrl + G »
Nabil
Félicitations pour le lancement de ton nouveau blog. Je trouve le type d’articles que tu viens de publier très enrichissant. Merci de partager…
Une phrase ou deux qui présentent Grails et le principe de scaffolding auraient aussi été les bienvenus pour les novices comme moi 🙂
Merci Jaz,
je suis tout à fait d’accord par rapport au fait qu’il fallait que je présente Grails dans un petit paragraphe. Je le fais dans ce commentaire :
Grails est un framework web basé sur le langage dynamique Groovy et qui permet de créer des applications web composées de couche métier persistante, une couche contrôle et une couche vue (MVC). Il est aussi basé sur spring et hibernate (pour la couche de données)
Ce n’est qu’une petite définition, elle n’est pas exhaustive 🙂
Pour faire plus simple, Grails, Ruby on Rails, CakePHP, Symphonie font partie de la même famille.
Toutes mes félicitations pour ton blog! Tu pourrais peut être nous recommander quelques pistes pour bien démarrer avec Grails.
Merci Atef,
pour les pistes pour bien démarrer avec Grails, je te conseille déjà le site officiel : http://grails.org tu trouveras plein de tutoriels dans la rubrique Community > Tutorials. Après il y a aussi les livres, personnellement, j’ai pu lire « The Definitive Guide to Grails » de Graeme Rocher, je trouve qu’il est suffisant pour bien démarrer Grails.
J’estime aussi qu’une connaissance du langage Groovy est nécessaire.
félicitations SIKI et bonne continuation
Bonjour et merci pour ce blog instrctif,
par contre il doit manquer quelque chose concernant le listing 6, je ne vois rine apparaitre…
Listing 6 : Code du fichier views/layout/ajax.gsp
Merci pour cette remarque.
Le contenu du listing 6 a été ajouté.
Hello,
un grand merci pour ce boulot et également pour la présentation de grails sur slideshare.net .
Suis actuelmnt sur un projet grails et là je me heurte à un problème en base de données :Je voudrai éviter que les éléments saisis par un utilisateur X soit visible par un utilisateur Y et vise versa mais j’y arrive pas.
Mon soucis n’a rien à voir avec le tuto ci dessus et je m’en excuse cependant votre aide me fera le plus grand bien.
Merci d’avance
Stan
Bonjour Stan
tout d’abord merci pour le commentaire.
Je vois bien ce que tu voudrais développer et la solution est simple. Cependant, je pense que ce n’est pas le meilleur endroit pour traiter ce sujet.
Je t’invite à reposer la question dans le forum Grails sur developpez.com (http://bit.ly/bQ90bS) et je te répondrai volontier.
ça va demander des questions/réponses et le forum est mieux adapté pour ça 😉
à toute sur le forum
Nabil
Merci bcp Nabil et à toute 😉
Hello Nabil,
Félicitations pour ton Blog que je trouve très intéressant, merci
Vive Grails, la haute productivité 😉