Vous aimez ce que vous lisez sur ce blog ?
Envie d'aller plus loin avec véritable formation d'expertise en Java ?
Venez suivre ma formation Masterclasse Expertise Java !

"Même un développeur experimenté a besoin de continuer à apprendre. Et dans cette formation... j'ai appris beaucoup !" - A.G., Java Champion

Sessions intra-entreprises sur demande : contact[at]mokatech.net.
Inscrivez-vous vite !

Gestion des fenêtres modales avec Wicket

Le framework web Wicket fournit un composant ModalWindow permettant de gérer des fenêtres modales.
Dans cet article, nous verrons comment le mettre en oeuvre, au travers de cas d'utilisation de plus en plus complexes.

Une fenêtre basique

Pour réaliser une fenêtre modale, il nous faut :

  • Un composant ModalWindow ; celui-ci fournit tout le mécanisme d'apparition/disparition, déplacement et redimensionnement de la popup, ainsi que le masque d'arrière-plan qui empêchera l'utilisateur d'interagir avec le reste de l'application.
  • Un Panel quelconque, qui hébergera le contenu de la fenêtre modale.
  • Un brin d'Ajax pour piloter tout ça : AjaxButton, AjaxLink...

Pour notre premier exemple, nous afficherons le traditionnel "Hello World" dans une popup.
Voici le panel affichant le message :

  1. <wicket:panel>
  2. <span wicket:id="message"></span>
  3. </wicket:panel>
  1. public class SimpleContentPanel extends Panel {
  2. public SimpleContentPanel(String id) {
  3. super(id);
  4. add(new Label("message","Hello World !"));
  5. }
  6. }

Et voici la Page mettant en oeuvre la popup.
Notez que l'identifiant du Panel est fourni par la méthode getContentId() de la fenêtre modale :

  1. <wicket:extend>
  2. <h2>Basic modal popup</h2>
  3. <div wicket:id="modal"></div>
  4. <button wicket:id="showModalButton" type="button">Show</button>
  5. </wicket:extend>
  1. public class BasicModalPage extends BasePage {
  2.  
  3. public BasicModalPage() {
  4.  
  5. // La popup modale
  6. final ModalWindow modal = new ModalWindow("modal");
  7. modal.setContent(new SimpleContentPanel(modal.getContentId()));
  8. add(modal);
  9.  
  10. // Le bouton pour afficher la popup modale
  11. AjaxLink showModalButton = new AjaxLink("showModalButton") {
  12. @Override
  13. public void onClick(AjaxRequestTarget ajaxRequestTarget) {
  14. modal.show(ajaxRequestTarget);
  15. }
  16. };
  17. add(showModalButton);
  18.  
  19. }
  20. }

Nous obtenons ceci :

Basic1.png

Basic2.png



Une fenêtre améliorée (aka "plus belle la vitre")

Le composant ModalWindow fournit différentes options pour améliorer son apparence.

Titre

Premièrement, il est possible de changer son titre :

  1. modal.setTitle("Say hello");
Taille

Ensuite, les captures d'écran ci-dessus montraient que la fenêtre modale était bien trop grande par rapport à son contenu.
Dans ce cas, deux solutions se présentent :

  • Laisser la fenêtre s'adapter à la hauteur de son contenu :
  1. modal.setUseInitialHeight(false);
  • Définir manuellement sa taille optimale, et interdire à l'utilisateur de la redimensionner :
  1. modal.setInitialHeight(100);
  2. modal.setHeightUnit("px");
  3. modal.setInitialWidth(200);
  4. modal.setWidthUnit("px");
  5.  
  6. modal.setResizable(false);
Position

Une fois la fenêtre popup affichée, l'utilisateur peut (si vous l'y autorisez) la déplacer et la redimensionner. Si vous souhaitez que ces préférences soient sauvegardées et réutilisées au prochain affichage, il faut définir un cookie :

  1. modal.setCookieName("wicket-tips/styledModal");
Apparence

Enfin, il est possible de mofidier légèrement l'apparence de la fenêtre elle-même, et du calque interdisant à l'utilisateur de clique en-dehors de la popup :

  1. // Style graphique : CSS_CLASS_GRAY ou CSS_CLASS_BLUE
  2. modal.setCssClassName(ModalWindow.CSS_CLASS_GRAY);
  3. // Type du masque : TRANSPARENT ou SEMI_TRANSPARENT
  4. modal.setMaskType(ModalWindow.MaskType.TRANSPARENT);
Exemple

Voici un exemple de fenêtre modale mettant en oeuvre quelques-unes de ces options :

  1. public class StyledModalPage extends BasePage {
  2.  
  3. public StyledModalPage() {
  4.  
  5. // La popup modale
  6. final ModalWindow modal = new ModalWindow("modal");
  7. modal.setContent(new SimpleContentPanel(modal.getContentId()));
  8. add(modal);
  9.  
  10. // Configuration de la popup
  11. modal.setTitle("Say hello");
  12. modal.setCssClassName(ModalWindow.CSS_CLASS_GRAY);
  13. modal.setMaskType(ModalWindow.MaskType.TRANSPARENT);
  14. modal.setInitialWidth(200);
  15. modal.setWidthUnit("px");
  16. modal.setResizable(false);
  17. modal.setUseInitialHeight(false);
  18. modal.setCookieName("wicket-tips/styledModal");
  19.  
  20. // Le lien pour afficher la popup modale
  21. AjaxLink showModalButton = new AjaxLink("showModalButton") {
  22. @Override
  23. public void onClick(AjaxRequestTarget ajaxRequestTarget) {
  24. modal.show(ajaxRequestTarget);
  25. }
  26. };
  27. add(showModalButton);
  28.  
  29. }
  30. }

Styled2.png



Gestion d'un formulaire complet dans une popup

Voyons maintenant un exemple concret d'utilisation.

Use-case : l'utilisateur arrive sur la page, déclenche l'ouverture d'une popup qui lui présente un formulaire pour saisir son adresse (numéro, rue, ville). Il est ensuite redirigé vers une page récapitulant sa saisie.
Le POJO Adresse et la page récapitulative ne seront pas présentés ici, mais font partie de l'application en pièce jointe du billet.

Le formulaire est entièrement contenu et géré par un Panel, complètement indépendant de la page et du composant ModalWindow :

  1. <wicket:panel>
  2.  
  3. <form wicket:id="form">
  4. <table>
  5. <tr>
  6. <td>Number</td>
  7. <td><input type="text" wicket:id="number"/></td>
  8. </tr>
  9. <tr>
  10. <td>Street</td>
  11. <td><input type="text" wicket:id="street"/></td>
  12. </tr>
  13. <tr>
  14. <td>City</td>
  15. <td><select wicket:id="city"></select></td>
  16. </tr>
  17. </table>
  18. <input type="submit"/>
  19. </form>
  20.  
  21. </wicket:panel>
  1. public class FormContentPanel extends Panel {
  2.  
  3. public FormContentPanel(String id, IModel<Address> addressModel) {
  4. super(id, new CompoundPropertyModel<Address>(addressModel));
  5.  
  6. // Le formulaire : 3 champs, et le bouton de validation
  7. Form<Address> form = new Form<Address>("form", addressModel) {
  8. @Override
  9. protected void onSubmit() {
  10. setResponsePage(new ResultPage(getModelObject()));
  11. }
  12. };
  13. form.add(new TextField("number"));
  14. form.add(new TextField("street"));
  15. form.add(new DropDownChoice<String>("city", Arrays.asList( "Bordeaux", "Lille", "Lyon", "Marseille", "Paris", "Rennes")));
  16. add(form);
  17. }
  18.  
  19. }

A priori, la page permettant de lancer la fenêtre modale ne nécessite aucune modification, puisque le formulaire est entièrement contenu dans la popup.

Testons donc le formulaire :

Form2.png

Mais quand on valide le formulaire...

Form3.png

Ce message d'erreur, imprévu et pas très explicite, nous prévient que la soumission du formulaire n'est pas réalisée en Ajax, et qu'elle provoquera donc un changement de page. Les éventuelles données saisies dans la page principale pourraient donc être perdues.
En tant que développeur, vous savez ce que vous faites, ce message ne vous alarme donc pas ; mais il peut être troublant pour l'utilisateur.
Comment le désactiver ?

Le code Javascript qui affiche cette boîte de confirmation est automatiquement ajouté par le composant ModalWindow, impossible donc de l'éliminer totalement ; mais il consulte une variable nommée "Wicket.Window.unloadConfirmation" pour savoir s'il doit afficher l'alerte.
Il ne nous reste plus qu'à positionner cette variable à "false".

Le moyen le plus simple consiste à se brancher sur l'AjaxTargetRequest fournie par l'événement onClick() du bouton déclencheur :

  1. AjaxLink showModalButton = new AjaxLink("showModalButton") {
  2. @Override
  3. public void onClick(AjaxRequestTarget ajaxRequestTarget) {
  4. // Désactivation du message
  5. ajaxRequestTarget.appendJavascript( "Wicket.Window.unloadConfirmation = false;" );
  6. // Affichage de la popup
  7. modal.show(ajaxRequestTarget);
  8. }
  9. };
  10. add(showModalButton);



Gestion d'un formulaire partiel dans une popup

Nous avons vu qu'il était facile de gérer un formulaire complet dans une fenêtre modale. Mais qu'en est-il d'un formulaire partiel ?

Use-case : l'utilisateur remplit un formulaire dans une page, dont un des champs demande un traitement particulier (par exemple une recherche auprès d'une base de données). Ce sous-workflow est exécuté dans une fenêtre modale pour ne pas encombrer le formulaire principal. Le champ du formulaire principal est alors automatiquement mis à jour lorsque la fenêtre modale est fermée.

Dans ce use-case, le formulaire est situé dans la page "normale", dont voici le squelette.
Le formulaire est très similaire à celui vu plus haut, mais le champ city est en lecture seule, et un petit bouton placé à côté déclenche l'ouverture de la popup permettant de sélectionner sa valeur.

  1. <wicket:extend>
  2.  
  3. <h2>Partial form in a modal popup</h2>
  4.  
  5. <div wicket:id="modal"></div>
  6.  
  7. <form wicket:id="form">
  8. <table>
  9. <tr>
  10. <td>Number</td>
  11. <td><input type="text" wicket:id="number"/></td>
  12. </tr>
  13. <tr>
  14. <td>Street</td>
  15. <td><input type="text" wicket:id="street"/></td>
  16. </tr>
  17. <tr>
  18. <td>City</td>
  19. <td>
  20. <input type="text" wicket:id="city"/>
  21. <button wicket:id="showModalButton" type="button">...</button>
  22. </td>
  23. </tr>
  24. </table>
  25. <input type="submit"/>
  26. </form>
  27.  
  28. </wicket:extend>
  1. public class PartialFormModalPage extends BasePage {
  2.  
  3. public PartialFormModalPage() {
  4.  
  5. IModel<Address> addressModel = new Model<Address>(new Address());
  6. final TextField cityTextField = new TextField<String>("city");
  7.  
  8. // La popup modale
  9. final ModalWindow modal = new ModalWindow("modal");
  10. modal.setContent(new PartialFormContentPanel(modal.getContentId(), addressModel);
  11. modal.setInitialWidth(220);
  12. modal.setWidthUnit("px");
  13. modal.setUseInitialHeight(false);
  14. modal.setTitle("Choose a city");
  15. add(modal);
  16.  
  17. // Le formulaire
  18. Form<Address> form = new Form<Address>("form", new CompoundPropertyModel<Address>(addressModel)) {
  19. @Override
  20. protected void onSubmit() {
  21. // Redirection vers la page récapitulative
  22. setResponsePage(new ResultPage(getModelObject()));
  23. }
  24. };
  25. form.add(new TextField("number"));
  26. form.add(new TextField("street"));
  27. form.add(new TextField<String>("city").setEnabled(false));
  28. // Bouton d'affichage de la popup
  29. form.add(new AjaxLink("showModalButton") {
  30. @Override
  31. public void onClick(AjaxRequestTarget ajaxRequestTarget) {
  32. modal.show(ajaxRequestTarget);
  33. }
  34. });
  35. add(form);
  36. }
  37. }



Partial1.png

Comme expliqué plus haut, la popup contient un mini-formulaire permettant de choisir la ville :

  1. <wicket:panel>
  2.  
  3. <form wicket:id="form">
  4. <table>
  5. <tr>
  6. <td>City</td>
  7. <td><select wicket:id="city"></select></td>
  8. </tr>
  9. </table>
  10. <input type="submit" wicket:id="chooseCityButton"/>
  11. </form>
  12.  
  13. </wicket:panel>
  1. public class PartialFormContentPanel extends Panel {
  2.  
  3. public PartialFormContentPanel(String id, IModel<Address> addressModel) {
  4. super(id, new CompoundPropertyModel<Address>(addressModel));
  5.  
  6. // Le formulaire
  7. Form<Address> form = new Form<Address>("form", addressModel);
  8. form.add(new DropDownChoice<String>("city", Arrays.asList( "Bordeaux", "Lille", "Lyon", "Marseille", "Paris", "Rennes")));
  9. form.add(new AjaxButton("chooseCityButton") {
  10. @Override
  11. protected void onSubmit(AjaxRequestTarget ajaxRequestTarget, Form<?> form) {
  12. // Comment mettre à jour le champ du formulaire sur la page principale ?
  13. }
  14. });
  15. add(form);
  16. }
  17.  
  18. }



Partial2.png

Notez que la validation de ce mini-formulaire doit être réalisée en Ajax (ici grâce à un AjaxButton), car nous souhaitons simplement fermer la popup et laisser l'utilisateur continuer à remplir le formulaire sur la page principale.

A ce propos, comment allons-nous transmettre à ce formulaire la valeur sélectionnée dans la popup ? Depuis la méthode onSubmit(), nous n'avons aucune référence sur le champ à mettre à jour.
Puisque nous ne pouvons pas le faire depuis le contexte de la popup, il faut trouver un moyen d'exécuter cette opération dans le contexte de la page appelante, d'où le champ est accessible. Pour cela, le plus simple est de déléguer le traitement de la popup à une méthode abstraite, qui sera implémentée dans la page principale au niveau de la déclaration de la popup.

Voici donc le code modifié de la fenêtre modale. Notez que la méthode onSubmit() du formulaire délègue le traitement à la méthode abstraite onSave().

  1. public abstract class PartialFormContentPanel extends Panel {
  2.  
  3. public PartialFormContentPanel(String id, IModel<Address> addressModel) {
  4. super(id, new CompoundPropertyModel<Address>(addressModel));
  5.  
  6. // Le formulaire
  7. Form<Address> form = new Form<Address>("form", addressModel);
  8. form.add(new DropDownChoice<String>("city", Arrays.asList( "Bordeaux", "Lille", "Lyon", "Marseille", "Paris", "Rennes")));
  9. form.add(new AjaxButton("chooseCityButton") {
  10. @Override
  11. protected void onSubmit(AjaxRequestTarget ajaxRequestTarget, Form<?> form) {
  12. // Délégation à la méthode onSave()
  13. onSave(ajaxRequestTarget);
  14. }
  15. });
  16. add(form);
  17. }
  18.  
  19. // Méthode que la page appelante devra implémenter
  20. public abstract void onSave(AjaxRequestTarget ajaxRequestTarget);
  21.  
  22. }

Et voici le code de la page racine, où la méthode abstraite est implémentée (grâce à une classe anonyme, lignes 11 à 16 ci-dessous).
Autre légère modification du code : Le champ cityTextField devant être mis à jour en Ajax, il ne faut pas oublier d'appeler sa méthode setOutputMarkupId(true).

  1. public class PartialFormModalPage extends BasePage {
  2.  
  3. public PartialFormModalPage() {
  4.  
  5. IModel<Address> addressModel = new Model<Address>(new Address());
  6. final TextField cityTextField = new TextField<String>("city");
  7.  
  8. // La popup modale
  9. final ModalWindow modal = new ModalWindow("modal");
  10. modal.setContent(new PartialFormContentPanel(modal.getContentId(), addressModel) {
  11. @Override
  12. public void onSave(AjaxRequestTarget ajaxRequestTarget) {
  13. // Mise à jour du champ du formulaire avec la valeur saisie dans la popup
  14. ajaxRequestTarget.addComponent(cityTextField);
  15. modal.close(ajaxRequestTarget);
  16. }
  17. });
  18. modal.setInitialWidth(220);
  19. modal.setWidthUnit("px");
  20. modal.setUseInitialHeight(false);
  21. modal.setTitle("Choose a city");
  22. add(modal);
  23.  
  24. // Le formulaire
  25. Form<Address> form = new Form<Address>("form", new CompoundPropertyModel<Address>(addressModel)) {
  26. @Override
  27. protected void onSubmit() {
  28. setResponsePage(new ResultPage(getModelObject()));
  29. }
  30. };
  31. form.add(new TextField("number"));
  32. form.add(new TextField("street"));
  33. cityTextField.setEnabled(false);
  34. cityTextField.setOutputMarkupId(true); // Nécessaire pour un update ajax
  35. form.add(cityTextField);
  36. form.add(new AjaxLink("showModalButton") {
  37. @Override
  38. public void onClick(AjaxRequestTarget ajaxRequestTarget) {
  39. modal.show(ajaxRequestTarget);
  40. }
  41. });
  42. add(form);
  43. }
  44. }

Et voilà, nous avons maintenant un formulaire partiel qui fonctionne dans une boîte modale !



Conclusion

Le composant ModalWindow que propose Wicket en standart est fort pratique, et permet d'implémenter assez facilement un grand nombre de cas d'utilisation - il faut juste connaître les quelques techniques que nous avons vues ici.

Je vous invite à télécharger l'application de démonstration en pièce jointe de ce billet, qui reprend l'ensemble du code et des exemples.
le système de build utilisé est Gradle, qu'il vous faudra préalablement télécharger et installer.
Ensuite, il suffit de décompresser l'archive de l'application, d'ouvrir une console dans le répertoire racine, puis de lancer la commande suivante :

  1. gradle jettyRun

L'application est alors disponible à l'adresse : http://localhost:8080/Wicket-modal/.

Bonus : les utilisateurs de navigateurs récents apprécieront la police de caractère intégrée à l'application grâce à l'instruction CSS3 @font-face :)


Commentaires

1. Le vendredi 7 mai 2010, 16:42 par Ellène

Merci pour cet article Olivier, il m'a été très utile pour réaliser un petit proto Wicket :)

2. Le mercredi 25 août 2010, 09:54 par felix79

Super, c'est exactement ce que je voulait faire et je n'y arrivais pas toute seule ! C'est très bien expliqué et ca marche !

3. Le mercredi 25 août 2010, 10:21 par felix79

J'aimerais ajouter un bouton "Annuler" dans la fenêtre modale qui annulerait le choix effectué et fermerait la fenêtre... Une idée ?

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.