Création des formulaires en Spring

Formulaires web avec Spring et taglib spring-form.tld

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

La partie précédente des articles consacrés au développement des applications web avec Spring concernait la couche des données. Cependant, pour avoir des données à manipuler, on doit pouvoir les rajouter dans le système. Le plus souvent on fera cela à travers les formulaires qui seront au cœur de cette partie.

"Flash" sessions

Avant d'aborder la problématique des formulaires, on verra comment transmettre les données d'une requête à l'autre dans les sessions.

Les données qui doivent être visibles uniquement pendant la transmission d'une requête à l'autre, s'appellent flash attributes. Il s'agit donc d'éléments stockés du côté du serveur qui sont disponibles dans le contrôleur invoqué après la redirection. On peut constater cela sur l'exemple suivant :

@RequestMapping("/question")
@Controller
public class QuestionController {
public String writeHandle(@ModelAttribute("question") @Validated({GeneralGroupCheck.class}) Question question, BindingResult binRes, 
    @LoggedUser AuthenticationFrontendUserDetails user, RedirectAttributes redAtt, @LocaleLang Lang lang) {
        logger.info("Received POST request " + question);
        if (binRes.hasErrors()) {
            redAtt.addFlashAttribute("error", true);
            redAtt.addFlashAttribute("question", question);
            redAtt.addFlashAttribute("errors", binRes);
        } else {
            try {
                question.setLang(lang);
                question.setSubscriber(conversionService.convert(user, Subscriber.class));
                questionService.addNew(question);
                redAtt.addFlashAttribute("success", true);
            } catch (Exception e) {
                binRes.addError(getExceptionError("question"));
                redAtt.addFlashAttribute("error", true);
                redAtt.addFlashAttribute("question", question);
                redAtt.addFlashAttribute("errors", binRes);
            }
        }
        return "redirect:/question/write";
    }
}

Ici la méthode addFlashAttribute() est chargée de passer des valeurs d'une requête à l'autre. Comme on peut constater, les flash attributes sont stockés selon la représentation key-value pair. Ces attributs sont importants car dans notre gestion des formulaires on va fonctionner selon le schéma suivant :

  1. 1. L'utilisateur remplit le formulaire et le soumet.
  2. 2. Une méthode d'un contrôleur traite cette requête. Ensuite, elle renvoie le résultat dans contrôleur chargé d'afficher la page avec le formulaire renseigné à la 1e étape.

Transformer un objet en formulaire sous Spring

Dans l'exemple évoqué dans le paragraphe précédent on a utilisé une nouvelle annotation, @ModelAttribute. Elle permet de présenter un objet dans une page web. L'objet présenté peut aussi bien être une entité qu'une classe du type Plain Old Java Object (POJO). La vue bénéficie de toutes les propriétés de l'objet présenté. Analysons maintenant deux exemples : un qui transformera l'entité en un formulaire et l'autre qui fera la même chose avec une classe prévue exprès pour le formulaire.

afficher le code

Et le fichier .JSP correspondant à ces fichiers :

<form:form modelAttribute="question" id="question" method="post">

<form:errors  />
  <form:label path="title">title</form:label>
  <form:input path="title" />
  <form:errors path="title" cssClass="error" />
  <form:label path="content">content</form:label>
  <form:textarea path="content" />
  <form:errors path="content" cssClass="error" />
  <form:errors path="subscriber" cssClass="error" />
  <form:errors path="lang" cssClass="error" />
  <button type="submit">Save</button>
  <button type="reset">Reset</button>
</form:form>

// TODO : modifier ce fichier de vue

Voici les classes spécifiques au formulaire qu'on souhaite afficher.

afficher le code

Et le fragment de la vue associée à cette classe :

<form:form modelAttribute="bookForm" id="bookForm" method="post">
<p>Alias : <form:input path="book.alias" />
	<form:errors path="book.alias" cssClass="error" /></p>
    <p><form:checkboxes items="${bookForm.categories}" path="categoriesChecked" itemValue="id" itemLabel="alias" />
	<form:errors path="categoriesChecked" cssClass="error" /></p>
    <p><form:checkboxes items="${bookForm.writers}" path="writersChecked" itemValue="id" itemLabel="fullname" />
	<form:errors path="writersChecked" cssClass="error" /></p>
</form:form>

Analysons maintenant le code de ces deux méthodes de création des formulaires sous Spring. Dans les deux on remarque la présence des tags <form:errors /> dans la partie de vue. Il s'agit des erreurs qui sont créées par le validateur dans la méthode chargée de traiter la requête envoyée. Pour pouvoir afficher ces erreurs au client, on doit se référer à des fonctions de traitement du formulaire (handleWrite() et addHandle()). Ces deux méthodes contiennent une ligne de code qui s'occupe de passer le résultat de validation dans la nouvelle requête via flash attributes. Il s'agit du fragment redAtt.addFlashAttribute("errors", binRes);. Ces résultats sont ensuite lisibles à travers les paramètres envoyés directement au modèle de vue, c'est-à-dire par l'implémentation de l'interface Model. Mais comment la vue sait de quelle erreur il s'agit ? Dans les deux contrôleurs, avant d'écrire nos méthodes, on spécifie un attribut addBinding. Il signale le nom du résultat de validation que va chercher <form:errors />.

D'ailleurs, les possibilités du taglib <form /> sont très puissantes. Tout d'abord, pour créer un champ du formulaire avec les données pré-remplies, il suffit de spécifier l'attribut path associé à la propriété de la classe du formulaire. Spring se chargera par la suite de récupérer la valeur et l'attribut au formulaire. Seule la création des champs gérant des collections s'avère plus compliquée. Dans notre cas, elle est illustrée par <form:checkboxes items="${bookForm.writers}" path="writersChecked" itemValue="id" itemLabel="fullname" /> :
- l'attribut items indique la collection des tous les éléments sélectionnables dans notre liste des inputs du type checkbox
- l'attribut path signifie des éléments déjà sélectionnés
- l'attribut itemValue signale quel champ de notre objet sera utilisé dans l'attribut value du checkbox généré
- l'attribut itemLabel réfère au champ qui jouera le rôle du libellé

Les deux exemples ont néanmoins une différence importante. Le deuxième, celui qui implémente une classe spécifique du formulaire, possède une méthode bindForm() annotée avec @InitBinder. Qu'est-ce qu'elle fait ? Elle identifie la méthode qui va initialiser WebDataBinder. Il s'agit d'un relieur qui traduit les éléments de la requête aux objets JavaBean. Dans l'exemple cité, binder enregistre des éditeurs qui s'occupent de transformer le texte reçu, séparé par des virgules en instances de la classe Category ou de la classe Writer. On va traiter cette thématique plus loin, dans l'article consacré à des property editors.

Bartosz KONIECZNY Formulaires

Une question ? Une remarque ?

*

*

Un conseil Symfony2

Problème avec les valeurs par défaut pour un champ type checkbox ?

La gestion des formulaires sous Symfony2 correspond parfaitement au slogan de jQuery "write less, do more". En effet, il suffit de déterminer les validateurs une fois et après seulement les adapter en fonction du groupe du formulaire. Egalement la définition des champs est très intuitive. La récupération des données saisies, en cas d'une erreur de validation, est aussi automatique. Cependant, il se peut que vous renctonreriez un problème avec les champs du type checkbox. Supposons, qu'on veut construire un checkbox qui prendra pour valeur des chiffres (integers) correspondant aux identifiants aux apparements recherchés (1 pièce, 2 pièces, 3 pièces etc.) :

protected static $types = array(1 => '1 room', 2 => '2 rooms', 3 => '3 rooms');
Après une validation incorrecte, on retourne sur la page avec le formulaire pré-rempli. Pour être sûr que la liste des checkboxes va avoir les champs pré-cochés (par exemple 1 room et 3 rooms), il faut s'assurer que les types des valeurs sauvegardées sont égales à celles de la variable statique $types. Souvent il faut faire une boucle pour régler un éventuel problème avec les valeurs pré-cochés dans multiple checkboxes sous Symfony2 :
    foreach($values as $v => $val)
    {
      $vals[] = (int)$val;
    }