Skip to content

5. Las vistas Thymeleaf

Volvamos a la arquitectura de una aplicación Spring MVC.

En los dos capítulos anteriores se han descrito diversos aspectos del bloque [1]: las acciones. Ahora abordaremos:

  • el bloque [2] de las vistas V;
  • el bloque [3] del modelo M mostrado por estas vistas;

Desde la creación de Spring MVC, la tecnología de generación de las páginas HTML enviadas a los navegadores de los clientes era la de las páginas JSP (Java Server Pages). Desde hace algunos años, también se puede utilizar la tecnología [Thymeleaf] [http://www.thymeleaf.org/]. Es esta la que presentamos ahora.

5.1. El proyecto STS

Creamos un nuevo proyecto:

  • en [3], indicamos que el proyecto necesita las dependencias [Thymeleaf]. Esto añadirá, además de las dependencias [Spring MVC] del proyecto anterior, las del marco [Thymeleaf] [5];

Ahora, desarrollemos este proyecto de la siguiente manera:

  

Nos inspiramos en el proyecto anterior:

  • [istia.st.springmvc.controllers] contendrá los controladores;
  • [istia.st.springmvc.models] contendrá los modelos de las acciones y las vistas;
  • [istia.st.springmvc.main] es el paquete de la clase ejecutable de Spring Boot;
  • [templates] contendrá las vistas Thymeleaf;
  • [i18n] contendrá los mensajes internacionalizados mostrados por las vistas;

La clase [Application] es la siguiente:


package istia.st.springmvc.main;

import org.springframework.boot.SpringApplication;

public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Config.class, args);
    }
}

La clase [Config] es la siguiente:


package istia.st.springmvc.main;

import java.util.Locale;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Bean
    public CookieLocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setCookieName("lang");
        localeResolver.setDefaultLocale(new Locale("fr"));
        return localeResolver;
    }
}

Esta configuración permite, por el momento, la gestión de las configuraciones regionales.

El controlador [ViewController] es el siguiente:


package istia.st.springmvc.actions;

import org.springframework.stereotype.Controller;

@Controller
public class ViewsController {

}
  • En la línea 5, la anotación [@Controller] ha sustituido a la anotación [@RestController], ya que, a partir de ahora, las acciones no generarán la respuesta al cliente. Lo que harán será:
    • construir un modelo M
    • devolver un tipo [String], que será el nombre de la vista [Thymeleaf] encargada de mostrar este modelo. Es la combinación de esta vista V y este modelo M la que generará el flujo HTML enviado al cliente;

El archivo [messages.properties] está vacío por el momento.

5.2. [/v01]: los fundamentos de Thymeleaf

Consideramos la siguiente acción en [ViewsController]:


    // Fundamentos de Thymeleaf - 1
    @RequestMapping(value = "/v01", method = RequestMethod.GET)
    public String v01() {
        return "v01";
}
  • línea 3: la acción devuelve un tipo [String]. Este será el nombre de la acción;
  • línea 4: esta vista será [v01]. Por defecto, debe encontrarse en la carpeta [templates] y llamarse [v01.html];

La vista [v01.html] es la siguiente:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="'Les vues'">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <h2 th:text="'Les vues dans Spring MVC'">Spring 4 MVC</h2>
</body>
</html>

Se trata de un archivo HTML. La presencia de Thymeleaf se aprecia:

  • en el espacio de nombres [th] de la línea 2;
  • en los atributos [th:text] de las líneas 4 y 8;

Tenemos aquí un archivo HTML válido que se puede visualizar. Lo colocamos en la carpeta [static] [2] con el nombre [vue-01.html] y lo solicitamos directamente con un navegador:

Si examinamos el código fuente de la página en [2], podemos observar que los atributos [th:text] han sido enviados por el servidor y han sido ignorados por el navegador. Cuando una vista es el resultado de una acción, Thymeleaf entra en acción e interpreta los atributos [th] antes de enviar la respuesta al cliente.

La etiqueta HTML:


<title th:text="'Les vues'">Spring 4 MVC</title>

es procesada de la siguiente manera por Thymeleaf:

  • th:text tiene la sintaxis th:text="expresión", donde expresión es una expresión que se va a evaluar. Cuando esta expresión es una cadena de caracteres, como en este caso, debe ir entre comillas;
  • el valor de [expression] sustituye al texto de la etiqueta HTML, en este caso el texto de la etiqueta [title];

Tras el procesamiento, la etiqueta anterior ha quedado así:


<title>Les vues</title>

Solicitemos la acción [/v01]:

  • en [2], vemos el trabajo de sustitución realizado por Thymeleaf;

Ahora solicitemos el URL [http://localhost:8080/v01.html]:

 

¿Cómo hay que interpretar esto? ¿Se ha servido la vista [templates/v01.html] directamente sin pasar por una acción? Para aclarar las cosas, creamos la siguiente acción [/v02]:


    // Fundamentos de Thymeleaf - 2
    @RequestMapping(value = "/v02", method = RequestMethod.GET)
    public String v02() {
        System.out.println("action v02");
        return "vue-02";
}

La vista [vue-02.html] es una copia de [v01.html]:

  

Ahora solicitamos URL [http://localhost:8080/vue-02.html]:

 

No se ha encontrado URL. Ahora solicitemos URL [http://localhost:8080/v02.html]

  • en los registros de la consola, en [1], vemos que se ha llamado a la acción [/v02], y esta ha mostrado la vista [vue-02.html] en [2];

Ahora sabemos que URL [http://localhost:8080/v02.html] también puede hacer referencia a un archivo [/v02.html] en la carpeta [static]. ¿Qué ocurre si este archivo existe? Lo probamos. Creamos en la carpeta [static] el siguiente archivo [v02.html]:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <h2>Spring 4 MVC</h2>
</body>
</html>

y luego solicitamos el URL [http://localhost:8080/v02.html]:

[1] y [2] muestran que se ha llamado a la acción [/v02]. Por lo tanto, hay que tener en cuenta que cuando la acción URL solicitada tiene la forma [/x.html], Spring / Thymeleaf:

  • ejecuta la acción [/x] si existe;
  • sirve la página [/static/x.html] si existe;
  • lanza una excepción 404 Not found en caso contrario;

Para evitar confusiones, a partir de ahora, las acciones y las vistas no tendrán los mismos nombres.

5.3. [/v03]: internacionalización de las vistas

La integración Spring / Thymeleaf permite a Thymeleaf utilizar los archivos de mensajes de Spring. Consideremos la siguiente nueva acción [/v03]:


    // internacionalización de las vistas
    @RequestMapping(value = "/v03", method = RequestMethod.GET)
    public String v03() {
        return "vue-03";
}

Muestra la siguiente vista [vue-03.html]:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <h2 th:text="#{title}">Spring 4 MVC</h2>
</body>
</html>

En las líneas 4 y 8, la expresión del atributo [th:text] es #{title}, cuyo valor es el mensaje de clave [title]. Creamos los siguientes archivos [messages_fr.properties] y [messages_en.properties]:

[messages_fr.properties]


title=Les vues dans Spring MVC

[messages_en.properties]


title=Views in Spring MVC

Solicitemos los URL, [http://localhost:8080/v03.html?lang=fr] y [http://localhost:8080/v03.html?lang=en]:

Obsérvese que hemos aplicado lo que hemos aprendido recientemente. En lugar de designar la acción [v03] como [/v03], la hemos designado como [/v03.html].

5.4. [/v04]: creación de la plantilla M de una vista V

Consideremos la siguiente nueva acción [/v04]:


    // creación de la plantilla M de una vista V
    @RequestMapping(value = "/v04", method = RequestMethod.GET)
    public String v04(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        System.out.println(String.format("Modèle=%s", model));
        return "vue-04";
}
  • línea 4: la plantilla de la vista se inyecta en los parámetros de la acción. Por defecto, esta plantilla inicial está vacía. Veremos que es posible rellenarla previamente;
  • línea 4: una plantilla de tipo [Model] es una especie de diccionario de elementos de tipo <String, Object>. Línea 4: añadimos una entrada en este diccionario con la clave [personne] asociada a un valor de tipo [Personne];
  • línea 5: mostramos en la consola la plantilla para ver cómo se ve;
  • línea 6: mostramos la vista [vue-04.html];

La clase [Personne] es la utilizada en el capítulo anterior:

  

package istia.st.springmvc.models;

public class Personne {

    // identificador
    private Integer id;
    // nombre
    private String nom;
    // edad
    private int age;

    // constructores
    public Personne() {

    }

    public Personne(String nom, int age) {
        this.nom = nom;
        this.age = age;
    }

    public Personne(Integer id, String nom, int age) {
        this(nom, age);
        this.id = id;
    }

    @Override
    public String toString() {
        return String.format("[id=%s, nom=%s,  age=%d]", id, nom, age);
    }

    // getters y setters
...
}

La vista [vue-04.html] es la siguiente:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <p>
            <span th:text="#{personne.nom}">Nom :</span>
            <span th:text="${personne.nom}">Bill</span>
        </p>
        <p>
            <span th:text="#{personne.age}">Age :</span>
            <span th:text="${personne.age}">56</span>
        </p>
    </body>
</html>
  • En la línea 10, se introduce un nuevo tipo de expresión Thymeleaf ${var}, donde var es una clave del modelo M de la vista. Recordemos que la acción [/v04] ha introducido en la plantilla una clave [personne] asociada a un tipo Personne[id, nom, age];
  • línea 10: muestra el nombre de la persona presente en el modelo;
  • línea 14: muestra su edad;

Los archivos de mensajes se modifican para añadir las claves [personne.nom] y [personne.age] de las líneas 9 y 13. El resultado es el siguiente:

y se encuentra la naturaleza de la plantilla M en los registros de la consola [2].

Cabe preguntarse por qué no se escribe la vista [vue-04] de la siguiente manera:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}"></title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <p>
            <span th:text="#{personne.nom}" /></span>
            <span th:text="${personne.nom}"></span>
        </p>
        <p>
            <span th:text="#{personne.age}"></span>
            <span th:text="${personne.age}"></span>
        </p>
    </body>
</html>

Esta vista es perfectamente válida y dará el mismo resultado que la anterior. Uno de los objetivos de Thymeleaf es que la página Thymeleaf se pueda mostrar aunque no pase por Thymeleaf. Así pues, creemos dos nuevas páginas estáticas:

  

La vista [vue-04b.html] es una copia de la vista [vue-04.html]. Lo mismo ocurre con la vista [vue-04a.html], pero se han eliminado los textos estáticos de la página. Si visualizamos las dos páginas, obtenemos los siguientes resultados:

En el caso de [1], la estructura de la página no aparece, mientras que en el caso de [2] sí es visible. De ahí el interés de incluir textos estáticos en una vista Thymeleaf, aunque en la ejecución vayan a ser sustituidos por otros textos.

Ahora veamos un detalle técnico. En la vista [vue-04.html], formateamos el código mediante [ctrl-Maj-F]. Obtenemos el siguiente resultado:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <p>
        <span th:text="#{personne.nom}">Nom :</span> <span
            th:text="${personne.nom}">Bill</span>
    </p>
    <p>
        <span th:text="#{personne.age}">Age :</span> <span
            th:text="${personne.age}">56</span>
    </p>
</body>
</html>

Las etiquetas están mal alineadas y el código se vuelve más difícil de leer. Si renombramos [vue-04.html] como [vue-04.xml] y reformateamos el código, las etiquetas vuelven a estar alineadas. Por lo tanto, el sufijo [xml] sería más práctico. Es posible trabajar con este sufijo. Para ello, hay que configurar Thymeleaf. Para no deshacer lo que hemos hecho, duplicamos el proyecto [springmvc-vues] estudiado en un proyecto [springmvc-vues-xml]

  

Modificamos el archivo [pom.xml] de la siguiente manera:


    <groupId>istia.st.springmvc</groupId>
    <artifactId>springmvc-vues-xml</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springmvc-vues-xml</name>
<description>Les vues dans Spring MVC</description>

El nombre del proyecto se cambia en las líneas 2 y 6. Además, cambiamos el sufijo de las vistas presentes en la carpeta [templates]:

  

El documento [http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html] enumera las propiedades de configuración de Spring Boot que se pueden utilizar en el archivo [application.properties]:

  

Este documento indica las propiedades que Spring Boot utiliza al realizar la autoconfiguración y que se pueden modificar realizando una configuración diferente en [application.properties]. Para Thymeleaf, las propiedades de autoconfiguración son las siguientes:


# THYMELEAF (ThymeleafAutoConfiguration)
spring.thymeleaf.check-template-location=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html # ;se añade charset=<encoding>
spring.thymeleaf.cache=true # establecido en false para la actualización en caliente

Por lo tanto, bastaría con añadir la línea


spring.thymeleaf.suffix=.xml

en [application.properties]. Vamos a seguir otro camino, el de la configuración mediante programación. Vamos a configurar Thymeleaf en la clase [Config]:


package istia.st.springmvc.main;

import java.util.Locale;

...
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;

@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    ...

    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".xml");
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCharacterEncoding("UTF-8");
     templateResolver.setCacheable(true);
        return templateResolver;
    }

    @Bean
    SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }

}
  • las líneas 16-24 configuran un [TemplateResolver] para Thymeleaf. Este es el objeto que se carga a partir de un nombre de vista proporcionado por una acción, para encontrar el archivo correspondiente;
  • las líneas 18 y 19 establecen el prefijo y el sufijo que se deben añadir al nombre de la vista para encontrar el archivo. Así, si el nombre de la vista es [vue04], el archivo buscado será [classpath:/templates/vue04.xml]. [classpath:/templates] es una sintaxis de Spring que designa una carpeta [/templates] situada en la raíz del Classpath del proyecto;
  • línea 21: para que en la respuesta enviada al cliente aparezca el encabezado HTTP:

Content-Type:text/html;charset=UTF-8
  • línea 20: indica que la vista cumple con la norma HTML5;
  • línea 22: indica que las vistas Thymeleaf se pueden almacenar en caché;
  • líneas 26-31: establece el motor de resolución de vistas del binomio Spring/Thymeleaf con el motor de resolución anterior;

Ejecutemos el ejecutable de este nuevo proyecto y solicitemos el URL [http://localhost:8080/v04.html?lang=en]:

 

Observamos que en el URL, la acción [/v04] ha podido sustituirse de nuevo por [v04.html].

5.5. [/v05]: factorización de un objeto en una vista Thymeleaf

Creamos la siguiente acción [/v05]:


    // creación del modelo M de una vista V - 2
    @RequestMapping(value = "/v05", method = RequestMethod.GET)
    public String v05(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        return "vue-05";
}

Es idéntica a la acción [/v04]. La vista [vue-05.xml] es la siguiente:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${personne}">        
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • líneas 8-17: dentro de estas líneas se define un objeto Thymeleaf mediante el atributo [th:object="${personne}"] (línea 8). Este objeto es aquí el objeto de clave [personne] que se encuentra en la plantilla:
  • línea 11: la expresión Thymeleaf [*{nom}] es equivalente a [${objet.nom}], donde [objet] es el objeto Thymeleaf actual. Por lo tanto, aquí la expresión [*{nom}] es equivalente a [${personne.nom}];
  • línea 15: lo mismo;

El resultado:

 

5.6. [/v06]: las pruebas en una vista Thymeleaf

Consideremos la siguiente acción [/v06]:


    // creación del modelo M de una vista V - 3
    @RequestMapping(value = "/v06", method = RequestMethod.GET)
    public String v06(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        return "vue-06";
}

Es idéntica a las dos acciones anteriores. Muestra la siguiente vista [vue-06.xml]:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${personne}">
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
            <p th:if="*{age} >= 18" th:text="#{personne.majeure}">Vous êtes majeur</p>
            <p th:if="*{age} &lt; 18" th:text="#{personne.mineure}">Vous êtes mineur</p>
        </div>
    </body>
</html>
  • línea 17: el atributo [th:if] evalúa una expresión booleana. Si esta expresión es verdadera, se muestra la etiqueta; de lo contrario, no se muestra. Por lo tanto, aquí si ${personne.age}>=18, se mostrará el texto [#{personne.majeure}], es decir, el mensaje de clave [personne.majeure] de los archivos de mensajes;
  • línea 18: no se puede escribir [*{age} < 18] porque el signo < es un carácter reservado. Por lo tanto, hay que utilizar su equivalente HTML [&lt;], también denominado entidad HTML [http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references];

Los archivos de mensajes se modifican:

[messages_fr.properties]


title=Les vues dans Spring MVC
personne.nom=Nom :
personne.age=Age :
personne.mineure=Vous êtes mineur
personne.majeure=Vous êtes majeur

[messages_en.properties]


title=Views in Spring MVC
personne.nom=Name:
personne.age=Age:
personne.mineure=You are under 18
personne.majeure=You are over 18

El resultado es el siguiente:

5.7. [/v07]: iteración en una vista Thymeleaf

Consideremos la siguiente acción [/v07]:


    // creación del modelo M de una vista V - 4
    @RequestMapping(value = "/v07", method = RequestMethod.GET)
    public String v07(Model model) {
        model.addAttribute("liste", new Personne[] { new Personne(7, "martin", 17), new Personne(8, "lucie", 32),
                new Personne(9, "paul", 7) });
        return "vue-07";
}
  • la acción crea una lista de tres personas, la coloca en la plantilla asociada a la clave [liste] y muestra la vista [vue-07];

La vista [vue-07.xml] es la siguiente:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h3 th:text="#{liste.personnes}">Liste de personnes</h3>
        <ul>
            <li th:each="element : ${liste}" th:text="'['+ ${element.id} + ', ' +${element.nom}+ ', ' + ${element.age} + ']'">[id,nom,age]</li>
        </ul>
    </body>
</html>
  • línea 10: el atributo [th:each] repite la etiqueta en la que se encuentra, en este caso una etiqueta <li>. Aquí tiene dos parámetros: [element : collection], donde [collection] es una colección de objetos, en este caso una lista de personas. Thymeleaf recorrerá la colección y generará tantas etiquetas <li> como elementos haya en la colección. Para cada etiqueta <li>, [element] representará el elemento de la colección asociado a la etiqueta. Para este elemento, se evaluará el atributo [th:text]. Su expresión es aquí una concatenación de cadenas para obtener el resultado [id, nom, age];
  • línea 8: se añade la clave [liste.personnes] a los archivos de mensajes;

Este es el resultado:

5.8. [/v08-/v10]: @ModelAttribute

Volvemos sobre algo que vimos al estudiar las acciones: la función de la anotación [@ModelAttribute]. Añadimos la siguiente acción nueva:


    // --------------- Enlace y ModelAttribute ----------------------------------

    // si el parámetro es un objeto, se instancia y, en su caso, se modifica mediante los parámetros de la consulta
    // formará parte automáticamente del modelo de la vista con la clave [key]
    // para el parámetro @ModelAttribute("xx"), key será igual a xx
    // para el parámetro @ModelAttribute, la clave será igual al nombre de la clase del parámetro, comenzando por una minúscula
    // si @ModelAttribute no está presente, entonces todo ocurre como si estuviera presente sin clave
    // Cabe señalar que esta presencia automática en el modelo no se produce si el parámetro no es un objeto

    @RequestMapping(value = "/v08", method = RequestMethod.GET)
    public String v08(@ModelAttribute("someone") Personne p, Model model) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-08";
}
  • línea 11: la anotación [@ModelAttribute("someone")] añadirá automáticamente el objeto [Personne p] en el modelo, asociado a la clave [someone];
  • línea 12: para verificar el modelo;
  • línea 13: muestra la vista [vue-08.xml];

La vista [vue-08.xml] es la siguiente:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${someone}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • línea 8: el objeto Thymeleaf se inicializa con el objeto de clave [someone];

El resultado es el siguiente:

 

y en la consola, tenemos el siguiente registro:

Modèle={someone=[id=4, nom=x,  age=11], org.springframework.validation.BindingResult.someone=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

Consideremos ahora la siguiente acción [/v09]:


    @RequestMapping(value = "/v09", method = RequestMethod.GET)
    public String v09(Personne p, Model model) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-09";
}
  • línea 1: la presencia del parámetro [Personne p] colocará automáticamente a la persona [p] en la plantilla. Como no se especifica ninguna clave, la clave utilizada es el nombre de la clase con su primer carácter en minúscula. Por lo tanto, [Personne p] es equivalente a [@ModelAttribute("personne") Personne p];

La vista [vue.09.xml] es la siguiente:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${personne}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • línea 8: la clave de modelo utilizada es [personne];

Este es un resultado:

 

y el registro en la consola del servidor:

Modèle={personne=[id=4, nom=x,  age=11], org.springframework.validation.BindingResult.personne=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

Ahora, consideremos la siguiente acción nueva [/v10]:


    @ModelAttribute("uneAutrePersonne")
    private Personne getPersonne(){
        return new Personne(24,"pauline",55);
    }

    @RequestMapping(value = "/v10", method = RequestMethod.GET)
    public String v10(Model model) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-10";
}
  • líneas 1-4: definen un método que crea en el modelo de cada solicitud un elemento de clave [uneAutrePersonne] asociado al objeto [new Personne(24,"pauline",55)];
  • líneas 6-10: la acción [/v10] no hace nada más que pasar el modelo que recibe a la vista [vue-10.xml]. Cabe señalar que el parámetro [Model model] solo es necesario para la instrucción de la línea 8. Sin él, es innecesario;

La vista [vue-10.xml] es la siguiente:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${uneAutrePersonne}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>

El resultado es el siguiente:

 

y el registro de la consola es el siguiente:

Modèle={uneAutrePersonne=[id=24, nom=pauline,  age=55]}

5.9. [/v11]: @SessionAttributes

Volvemos sobre algo que vimos al estudiar las acciones: el papel de la anotación [@SessionAttributes]. Añadimos la siguiente nueva acción [/v11]:


    @ModelAttribute("jean")
    private Personne getJean(){
        return new Personne(33,"jean",10);
    }

    @RequestMapping(value = "/v11", method = RequestMethod.GET)
    public String v11(Model model, HttpSession session) {
        System.out.println(String.format("Modèle=%s, Session[jean]=%s", model, session.getAttribute("jean")));
        return "vue-11";
}

Tenemos algo similar a lo que acabamos de estudiar. La diferencia radica en una anotación [@SessionAttributes] colocada en la propia clase:


@Controller
@SessionAttributes("jean")
public class ViewsController {
  • línea 2: se indica que la clave [jean] del modelo debe colocarse en la sesión;

Por eso, en la línea 7 de la acción, se ha inyectado la sesión. En la línea 8, se muestra el valor de la sesión asociada a la clave [jean].

La vista [vue-11.xml] es la siguiente:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${jean}">
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
        <hr />
        <div th:object="${session.jean}">
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>

Se muestran dos personas:

  • líneas 8-21: la persona con clave [jean] en el modelo;
  • líneas 23-36: la persona con clave [jean] en la sesión;

Los resultados son los siguientes:

  • en [1], la persona con clave [jean] en el modelo;
  • en [2], la persona con clave [jean] en la sesión;

El registro de la consola es el siguiente:


Modèle={uneAutrePersonne=[id=24, nom=pauline,  age=55], jean=[id=33, nom=jean,  age=10]}, Session[jean]=null

En lo anterior, vemos que la clave [jean] no está en la sesión que recibe la acción. De ello se deduce que la clave [jean] se introdujo en la sesión tras la ejecución de la acción y antes de la visualización de la vista.

Ahora, consideremos el caso en el que una clave es referenciada tanto por [@ModelAttribute] como por [@SessionAttributes]. Construimos las dos acciones siguientes:


    @RequestMapping(value = "/v12a", method = RequestMethod.GET)
    @ResponseBody
    public void v12a(HttpSession session) {
        session.setAttribute("paul", new Personne(51, "paul", 33));
    }

    // en el caso de que la clave de [@ModelAttribute] sea también una clave de [@SessionAttributes]
    // en este caso, el parámetro correspondiente se inicializa con el valor de la sesión
    @RequestMapping(value = "/v12b", method = RequestMethod.GET)
    public String v12b(Model model, @ModelAttribute("paul") Personne p) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-12";
}

La acción [/v12a] solo sirve para introducir en la sesión el elemento ['paul',new Personne(51, "paul", 33)]. No hace nada más. El hecho de que esté etiquetada por [@ResponseBody] indica que es ella la que genera la respuesta al cliente. Como su tipo es [void], no se genera ninguna respuesta.

La acción [/v12b] admite como parámetro [@ModelAttribute("paul") Personne p]. Si no se hace nada más, se instancia un objeto [Personne] y se inicializa con los parámetros de la solicitud, y este objeto no tiene nada que ver con el objeto de clave [paul] introducido en la sesión por la acción [/v12a]. Vamos a añadir la clave [paul] a los atributos de sesión de la clase:


@Controller
@SessionAttributes({ "jean", "paul" })
public class ViewsController {
  • línea 2, ahora hay dos atributos de sesión;

Volvamos a los parámetros de la acción [/v12b]:


public String v12b(Model model, @ModelAttribute("paul") Personne p) {

Ahora, el objeto [Personne p] no se instanciará, sino que hará referencia al objeto de clave [paul] en la sesión. A continuación, el procedimiento sigue siendo el mismo. El objeto clave [paul] aparecerá, en particular, en la plantilla de la vista que se mostrará. Esto es lo que queremos ver en la línea 11 de la acción [/v12b].

La vista [vue-12.xml] será la siguiente:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${paul}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • línea 8: se hace referencia a la clave [paul] del modelo de la vista;

Esto da el siguiente resultado (tras ejecutar la acción [/v12a], que introduce la clave [paul] en la sesión):

 

El registro de la consola es el siguiente:


Modèle={jean=[id=33, nom=jean,  age=10], uneAutrePersonne=[id=24, nom=pauline,  age=55], paul=[id=51, nom=paul,  age=33], org.springframework.validation.BindingResult.paul=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

La clave [paul] se ha introducido correctamente en la plantilla con el valor asociado a la clave [paul] en la sesión.

5.10. [/v13]: generar un formulario de entrada

Ahora abordamos la introducción de datos en los formularios y su validación. Creamos un primer formulario con la siguiente acción [/v13]:


  // genera un formulario para introducir una persona
  @RequestMapping(value = "/v13", method = RequestMethod.GET)
  public String v13() {
    return "vue-13";
}

que se limita a mostrar la vista [vue-13.xml] siguiente:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/someURL" th:action="@{/v14.html}" method="post">
            <h2 th:text="#{personne.formulaire.titre}">Entrez les informations suivantes</h2>
            <div th:object="${personne}">
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td th:text="#{personne.id}">Id :</td>
                            <td>
                                <input type="text" name="id" value="11" th:value="''" />
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.nom}">Nom :</td>
                            <td>
                                <input type="text" name="nom" value="Tintin" th:value="''" />
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.age}">Age :</td>
                            <td>
                                <input type="text" name="age" value="17" th:value="''" />
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
            <input type="submit" value="Valider" th:value="#{personne.formulaire.valider}" />
        </form>
    </body>
</html>

Si colocamos esta vista en la carpeta [static] con el nombre [vue-13.html] y solicitamos URL [http://localhost:8080/vue-13.html], obtenemos la siguiente página:

 
  • En la línea 8 del formulario, encontramos la etiqueta <form> con el atributo [th:action]. Este atributo será evaluado por Thymeleaf y su valor sustituirá al valor actual del atributo [action], que, por lo tanto, solo está ahí como decoración. Aquí, el valor del atributo [th:action] será [/v14.html];
  • líneas 17, 23 y 29, el valor del atributo [th:value] sustituirá al del atributo [value]. Aquí, este valor será la cadena vacía;

Cuando se solicita el URL [/v13.html], se obtiene el siguiente resultado:

 

Veamos el código fuente generado por Thymeleaf:


<!DOCTYPE html>

<html>
    <head>
        <title>Views in Spring MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/v14.html" method="post">
            <h2>Please, enter information and validate</h2>
            <div>
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td>Identifier:</td>
                            <td>
                                <input type="text" name="id" value="" />
                            </td>
                        </tr>
                        <tr>
                            <td>Name:</td>
                            <td>
                                <input type="text" name="nom" value="" />
                            </td>
                        </tr>
                        <tr>
                            <td>Age:</td>
                            <td>
                                <input type="text" name="age" value="" />
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
            <input type="submit" value="Validate" />
        </form>
    </body>
</html>

En las líneas 9, 18, 24 y 30, vemos la evaluación de los atributos [th:action] y [th:value] realizada por Thymeleaf.

5.11. [/v14]: gestionar los valores enviados por un formulario

La acción [/v14] es la que recibe los valores enviados. Es la siguiente:


  // procesa los valores del formulario
  @RequestMapping(value = "/v14", method = RequestMethod.POST)
  public String v14(Personne p) {
    return "vue-14";
}
  • línea 3: los valores enviados se encapsulan en un objeto [Personne p]. Sabemos que este objeto forma parte automáticamente del modelo M de la vista V que mostrará la acción, asociado a la clave [personne];
  • línea 4: la vista mostrada es la vista [vue-14.xml];

La vista [vue-14.xml] es la siguiente:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
    <h2 th:text="#{personne.formulaire.saisies}">Voici vos saisies</h2>
        <div th:object="${personne}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • línea 9: se recupera en el modelo el objeto asociado a la clave [personne];
  • líneas 12, 16 y 20: se muestran las características de este objeto;

Esto da el siguiente resultado:

5.12. [/v15-/v16]: validación de un modelo

Con el ejemplo anterior, veamos la siguiente secuencia:

  • en [1], se introducen valores erróneos para los campos [id] y [age] de tipo [int];
  • en [2], la respuesta del servidor nos indica que se han producido dos errores;

Vamos a utilizar el mismo formulario, pero en caso de errores de validación, redirigiremos a una página que indique dichos errores para que el usuario pueda corregirlos.

La acción [/v15] es la siguiente:


    // ---------------------- visualización de un formulario
    @RequestMapping(value = "/v15", method = RequestMethod.GET)
    public String v15(SecuredPerson p) {
        return "vue-15";
}

Recibe como parámetro un tipo [SecuredPerson] como sigue:

  

package istia.st.springmvc.models;

import javax.validation.constraints.NotNull;

import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;

public class SecuredPerson {

    @Range(min = 1)
    private int id;
    
    @Length(min = 4, max = 10)
    private String nom;
    
    @Range(min = 8, max = 14)
    private int age;

    // constructores
    public SecuredPerson() {

    }

    public SecuredPerson(int id, String nom, int age) {
        this.id=id;
        this.nom = nom;
        this.age = age;
    }

    // getters y setters
...
}

Los campos [id, nom, age] se han anotado con restricciones de validación. La vista [vue-15.xml] mostrada por la acción [/v15] es la siguiente:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/someURL" th:action="@{/v16.html}" method="post">
            <h2 th:text="#{personne.formulaire.titre}">Entrez les informations suivantes</h2>
            <div th:object="${securedPerson}">
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td th:text="#{personne.id}">Id :</td>
                            <td>
                                <input type="text" name="id" value="11" th:value="*{id}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('id')}" th:errors="*{id}" style="color: red">Identifiant erroné</span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.nom}">Nom :</td>
                            <td>
                                <input type="text" name="nom" value="Tintin" th:value="*{nom}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('nom')}" th:errors="*{nom}" style="color: red">Nom erroné</span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.age}">Age :</td>
                            <td>
                                <input type="text" name="age" value="17" th:value="*{age}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('age')}" th:errors="*{age}" style="color: red">Âge erroné</span>
                            </td>
                        </tr>
                    </tbody>
                </table>
                <input type="submit" value="Valider" th:value="#{personne.formulaire.valider}" />
                <ul>
                    <li th:each="err : ${#fields.errors('*')}" th:text="${err}" style="color: red" />
                </ul>
            </div>
        </form>
    </body>
</html>
  • líneas 10-47: se recupera el objeto del modelo de la página asociado a la clave [securedPerson]. Al finalizar la acción GET, se obtiene un objeto con su valor de instanciación [id=0, nom=null, age=0];
  • línea 17: el valor del campo [securedPerson.id];
  • línea 20: la expresión [${#fields.hasErrors('id')}] permite saber si se han producido errores de validación en el campo [securedPerson.id]. Si es así, el atributo [th:errors="*{id}"] muestra el mensaje de error asociado;
  • este escenario se repite en las líneas 29 para el campo [nom] y 38 para el campo [age];
  • línea 45: la expresión [${#fields.errors('*')}] designa el conjunto de errores en los campos del objeto [securedPerson]. Así, es el conjunto de estos errores el que se mostrará en las líneas 44-46;
  • línea 16: se observa que los valores del formulario se enviarán a la acción [/v16]. Esta es la siguiente:

    // -------------------- validación de un modelo------------------
    @RequestMapping(value = "/v16", method = RequestMethod.POST)
    public String v16(@Valid SecuredPerson p, BindingResult result) {
        // ¿Errores?
        if (result.hasErrors()) {
            return "vue-15";
        } else {
            return "vue-16";
        }
}
  • línea 3, la anotación [@Valid SecuredPerson p] obliga a validar los valores enviados;
  • línea 5: comprueba si el modelo de la acción es erróneo o no;
  • línea 6: si es erróneo, se devuelve el formulario [vue-15.xml]. Como este muestra los mensajes de error, los veremos;
  • línea 8: si el modelo de la acción se valida, se muestra la siguiente vista [vue-16.xml]:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
    <h2 th:text="#{personne.formulaire.saisies}">Voici vos saisies</h2>
        <div th:object="${securedPerson}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>

Estos son algunos ejemplos de ejecución:

5.13. [/v17-/v18]: comprobación de los mensajes de error

Cuando se solicita por primera vez la acción [/v15], se obtiene el siguiente resultado:

 

Es posible que se prefiera un formulario vacío en lugar de ceros en los campos [Identifiant, Age]. Para conseguirlo, modificamos el modelo de la acción de la siguiente manera:


package istia.st.springmvc.models;

import javax.validation.constraints.Digits;

import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;

public class StringSecuredPerson {

    @Range(min = 1)
    @Digits(fraction = 0, integer = 4)
    private String id;

    @Length(min = 4, max = 10)
    private String nom;

    @Range(min = 8, max = 14)
    @Digits(fraction = 0, integer = 2)
    private String age;

    // constructores
    public StringSecuredPerson() {

    }

    public StringSecuredPerson(String id, String nom, String age) {
        this.id = id;
        this.nom = nom;
        this.age = age;
    }

    // getters y setters
...

}
  • líneas 12 y 19: los campos [id] y [age] se cambian al tipo [String];
  • línea 11: se indica que el campo [id] debe ser un número de cuatro dígitos como máximo, sin decimales;
  • línea 18: lo mismo para el campo [age], que debe ser un número entero de dos dígitos como máximo;

La acción [/v17] queda así:


    // ---------------------- visualización de un formulario
    @RequestMapping(value = "/v17", method = RequestMethod.GET)
    public String v17(StringSecuredPerson p) {
        return "vue-17";
}

La vista [vue-17.xml] mostrada por la acción [/v17] es la siguiente:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/someURL" th:action="@{/v18.html}" method="post">
            <h2 th:text="#{personne.formulaire.titre}">Entrez les informations suivantes</h2>
            <div th:object="${stringSecuredPerson}">
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td th:text="#{personne.id}">Id :</td>
                            <td>
                                <input type="text" name="id" value="11" th:value="*{id}" />
                            </td>
                            <td>
                                <span th:each="err,status : ${#fields.errors('id')}" th:if="${status.index}==0" th:text="${err}" style="color: red">
                                    Identifiant erroné
                                </span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.nom}">Nom :</td>
                            <td>
                                <input type="text" name="nom" value="Tintin" th:value="*{nom}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('nom')}" th:errors="*{nom}" style="color: red">Nom erroné</span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.age}">Age :</td>
                            <td>
                                <input type="text" name="age" value="17" th:value="*{age}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('age')}" th:errors="*{age}" style="color: red">Âge erroné</span>
                            </td>
                        </tr>
                    </tbody>
                </table>
                <input type="submit" value="Valider" th:value="#{personne.formulaire.valider}" />
                <ul>
                    <li th:each="err : ${#fields.errors('*')}" th:text="${err}" style="color: red" />
                </ul>
            </div>
        </form>
    </body>
</html>

Los cambios se producen en las siguientes líneas:

  • línea 10: ahora se trabaja con el objeto del modelo de clave [stringSecuredPerson];
  • línea 20: se recorre la lista de errores del campo [id]. En la sintaxis [th:each="err,status : ${#fields.errors('id')}"], es la variable [err] la que recorre la lista. La variable [status] proporciona información sobre cada iteración. Se trata de un objeto [index, count, size, current] donde:
    • índice: es el número del elemento actual,
    • current: el valor de este elemento actual,
    • count, size: el tamaño de la lista recorrida;
  • línea 20: solo se muestra el primer elemento de la lista [th:if="${status.index}==0"];

La acción [/v18] que procesa el POST de la acción [/v17] es la siguiente:


    // -------------------- validación de un modelo------------------
    @RequestMapping(value = "/v18", method = RequestMethod.POST)
    public String v18(@Valid StringSecuredPerson p, BindingResult result) {
        // ¿Errores?
        if (result.hasErrors()) {
            return "vue-17";
        } else {
            return "vue-18";
        }
}

Los archivos de mensajes evolucionan de la siguiente manera:

[messages_fr.properties]


title=Les vues dans Spring MVC
personne.nom=Nom :
personne.age=Age :
personne.id=Identifiant :
personne.mineure=Vous êtes mineur
personne.majeure=Vous êtes majeur
liste.personnes=Liste de personnes
personne.formulaire.titre=Entrez les informations suivantes et validez
personne.formulaire.valider=Valider
personne.formulaire.saisies=Voici vos saisies
notNull=La donnée est obligatoire
Range.securedPerson.id=L''identifiant doit être un nombre entier >=1
Range.securedPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.securedPerson.nom=Le nom doit avoir entre 1 et 4 caractères
typeMismatch=Donnée invalide
Range.stringSecuredPerson.id=L''identifiant doit être un nombre entier >=1
Range.stringSecuredPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.stringSecuredPerson.nom=Le nom doit avoir entre 1 et 4 caractères
Digits.stringSecuredPerson.id=Tapez un nombre entier de 4 chiffres au plus
Digits.stringSecuredPerson.age=Tapez un nombre entier de 2 chiffres au plus

[messages_en.properties]


title=Views in Spring MVC
personne.nom=Name:
personne.age=Age:
personne.id=Identifier:
personne.mineure=You are under 18
personne.majeure=You are over 18
liste.personnes=Persons' list
personne.formulaire.titre=Please, enter information and validate
personne.formulaire.valider=Validate
personne.formulaire.saisies=Here are your inputs
NotNull=Data is required
Range.securedPerson.id=Identifier must be an integer >=1
Range.securedPerson.age=Only kids who are 8 to 14 years old are allowed on this site
Length.securedPerson.nom=Name must be 4 to 10 characters long
typeMismatch=Invalid format
Range.stringSecuredPerson.id=Identifier must be an integer >=1
Range.stringSecuredPerson.age=Only kids who are 8 to 14 years old are allowed on this site
Length.stringSecuredPerson.nom=Name must be 4 to 10 characters long
Digits.stringSecuredPerson.id=Should be an integer with at most four digits
Digits.stringSecuredPerson.age=Should be an integer with at most two digits

Veamos algunos ejemplos:

 

En [1] se observa que se han ejecutado los dos validadores del campo [age]:


    @Range(min = 8, max = 14)
    @Digits(fraction = 0, integer = 2)
    private String age;

¿Existe un orden en los mensajes de error? Para el campo [age], parece que los validadores se han ejecutado en el orden [Digits, Range]. Pero si se realizan varias consultas, se puede observar que este orden puede cambiar. Por lo tanto, no se puede confiar en el orden de los validadores. En [2], solo se muestra uno de los dos mensajes del campo [id]. En [3], se ven todos los mensajes de error.

5.14. [/v19-/v20]: uso de diferentes validadores

Consideremos el siguiente nuevo modelo de acción:

  

package istia.st.springmvc.models;

import java.util.Date;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
import org.springframework.format.annotation.DateTimeFormat;

public class Form19 {

    @NotNull
    @AssertFalse
    private Boolean assertFalse;

    @NotNull
    @AssertTrue
    private Boolean assertTrue;
    
    @NotNull
    @Future
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInFuture;
    
    @NotNull
    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInPast;
    
    @NotNull
    @Max(value = 100)
    private Integer intMax100;
    
    @NotNull
    @Min(value = 10)
    private Integer intMin10;
    
    @NotNull
    @NotEmpty
    private String strNotEmpty;
    
    @NotNull
    @NotBlank
    private String strNotBlank;
    
    @NotNull
    @Size(min = 4, max = 6)
    private String strBetween4and6;
    
    @NotNull
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$")
    private String hhmmss;
    
    @NotNull
    @Email
    @NotBlank
    private String email;
    
    @NotNull
    @Length(max = 4, min = 4)
    private String str4;
    
    @Range(min = 10, max = 14)
    @NotNull
    private Integer int1014;
    
    @URL
    @NotBlank
    private String url;

    // getters y setters
...
}

Se mostrará mediante la siguiente acción [/v19]:


    // ------------------ visualización de un formulario
    @RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v19(Form19 formulaire) {
        return "vue-19";
}
  • línea 3: la acción recibe como parámetro un objeto [Form19 formulaire]. Si GET no recibe parámetros, este objeto se inicializará con los valores predeterminados de Java;
  • línea 4: se muestra la vista [vue-19.xml]. Esta es la siguiente:

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
        <h3>Formulaire - Validations côté serveur</h3>
        <form action="/someURL" th:action="@{/v20.html}" method="post" th:object="${form19}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Contrainte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Erreur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">@NotEmpty</td>
                        <td class="col2">
                            <input type="text" th:field="*{strNotEmpty}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('strNotEmpty')}" th:errors="*{strNotEmpty}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@NotBlank</td>
                        <td class="col2">
                            <input type="text" th:field="*{strNotBlank}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('strNotBlank')}" th:errors="*{strNotBlank}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@assertFalse</td>
                        <td class="col2">
                            <input type="radio" th:field="*{assertFalse}" value="true" />
                            <label th:for="${#ids.prev('assertFalse')}">True</label>
                            <input type="radio" th:field="*{assertFalse}" value="false" />
                            <label th:for="${#ids.prev('assertFalse')}">False</label>
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('assertFalse')}" th:errors="*{assertFalse}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@assertTrue</td>
                        <td class="col2">
                            <select th:field="*{assertTrue}">
                                <option value="true">True</option>
                                <option value="false">False</option>
                            </select>
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('assertTrue')}" th:errors="*{assertTrue}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Past</td>
                        <td class="col2">
                            <input type="date" th:field="*{dateInPast}" th:value="*{dateInPast}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('dateInPast')}" th:errors="*{dateInPast}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Future</td>
                        <td class="col2">
                            <input type="date" th:field="*{dateInFuture}" th:value="*{dateInFuture}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('dateInFuture')}" th:errors="*{dateInFuture}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Max</td>
                        <td class="col2">
                            <input type="text" th:field="*{intMax100}" th:value="*{intMax100}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('intMax100')}" th:errors="*{intMax100}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Min</td>
                        <td class="col2">
                            <input type="text" th:field="*{intMin10}" th:value="*{intMin10}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('intMin10')}" th:errors="*{intMin10}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Size</td>
                        <td class="col2">
                            <input type="text" th:field="*{strBetween4and6}" th:value="*{strBetween4and6}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('strBetween4and6')}" th:errors="*{strBetween4and6}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Pattern(hh:mm:ss)</td>
                        <td class="col2">
                            <input type="text" th:field="*{hhmmss}" th:value="*{hhmmss}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('hhmmss')}" th:errors="*{hhmmss}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Email</td>
                        <td class="col2">
                            <input type="text" th:field="*{email}" th:value="*{email}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Length</td>
                        <td class="col2">
                            <input type="text" th:field="*{str4}" th:value="*{str4}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('str4')}" th:errors="*{str4}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Range</td>
                        <td class="col2">
                            <input type="text" th:field="*{int1014}" th:value="*{int1014}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('int1014')}" th:errors="*{int1014}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@URL</td>
                        <td class="col2">
                            <input type="text" th:field="*{url}" th:value="*{url}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('url')}" th:errors="*{url}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>

Este código muestra la siguiente vista:

 

La página presenta una tabla de tres columnas:

  • columna 1: el validador del campo de entrada;
  • columna 2: el campo de entrada;
  • columna 3: los mensajes de error del campo de entrada;

Veamos, por ejemplo, el código de la vista [/v19.html] para el validador [@Pattern]:


                    <tr>
                        <td class="col1">@Pattern(hh:mm:ss)</td>
                        <td class="col2">
                            <input type="text" th:field="*{hhmmss}" th:value="*{hhmmss}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('hhmmss')}" th:errors="*{hhmmss}" class="error">Donnée erronée</span>
                        </td>
</tr>

Encontramos aquí el código que acabamos de estudiar con los formularios de tipo [Personne]:

  • línea 2: la primera columna: el nombre del validador probado;
  • línea 4: el atributo Thymeleaf [th:field="*{hhmmss}] generará los atributos HTML, [id="hhmmss"] y [name="hhmmss"]. El atributo Thymeleaf [th:value="*{hhmmss}"] generará el atributo HTML [value="valeur de [form19.hhmmss]]";
  • línea 7: si el valor introducido para el campo [form19.hhmmss] es incorrecto, la línea 7 muestra los mensajes de error asociados a este campo;

Los valores contabilizados se procesan mediante la siguiente acción [/v20]:


    // ----------------- validación del modelo del formulario
    @RequestMapping(value = "/v20", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String v20(@Valid Form19 formulaire, BindingResult result, RedirectAttributes redirectAttributes) {
        if (result.hasErrors()) {
            return "vue-19";
        } else {
            // redirección a [vue-19]
            redirectAttributes.addFlashAttribute("form19", formulaire);
            return "redirect:/v19.html";
        }
}
  • línea 3: los valores introducidos rellenarán los campos del objeto [Form19 formulaire] si son válidos;
  • líneas 4-6: si los valores introducidos no son válidos, se vuelve a mostrar el formulario [vue-19] con los mensajes de error;
  • líneas 6-10: si los valores enviados son válidos, el objeto [Form19 formulaire] creado con estos valores se pone a disposición de la siguiente solicitud, en este caso la de redireccionamiento. A continuación, se destruye;
  • línea 9: se redirige al cliente a la acción [/v19.html]. Esta volverá a mostrar el formulario [vue-19], que contiene código como:

<form action="/someURL" th:action="@{/v20.html}" method="post" th:object="${form19}">

El atributo [th:object="${form19}"] recuperará entonces el objeto asociado al atributo Flash [form19] y, de este modo, volverá a mostrar el formulario tal y como se ha introducido.

El código del formulario merece algunas explicaciones más. Consideremos el siguiente código:


                    <tr>
                        <td class="col1">@assertFalse</td>
                        <td class="col2">
                            <input type="radio" th:field="*{assertFalse}" value="true" />
                            <label th:for="${#ids.prev('assertFalse')}">True</label>
                            <input type="radio" th:field="*{assertFalse}" value="false" />
                            <label th:for="${#ids.prev('assertFalse')}">False</label>
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('assertFalse')}" th:errors="*{assertFalse}" class="error">Donnée erronée</span>
                        </td>
</tr>

Esto genera el siguiente código HTML:


<tr>
  <td class="col1">@assertFalse</td>
  <td class="col2">
    <input type="radio" value="true" id="assertFalse1" name="assertFalse" />
    <label for="assertFalse1">True</label>
    <input type="radio" value="false" id="assertFalse2" name="assertFalse" />
    <label for="assertFalse2">False</label>
  </td>
  <td class="col3">
  </td>
</tr>

En el código


<input type="radio" th:field="*{assertFalse}" value="true" />
<label th:for="${#ids.prev('assertFalse')}">True</label>
<input type="radio" th:field="*{assertFalse}" value="false" />
<label th:for="${#ids.prev('assertFalse')}">False</label>

Los atributos Thymeleaf de las líneas 1 y 3, [th:field="*{assertFalse}"], plantean un problema. Se ha dicho que este atributo genera los atributos HTML, [id=assertFalse] y [name=assertFalse]. La dificultad radica en que, al generarse en las líneas 1 y 3, tenemos dos atributos [name] idénticos y dos atributos [id] idénticos. Si bien esto es posible con el atributo [name], no lo es con el atributo [id]. Como se ve en el código HTML generado, Thymeleaf ha generado dos atributos [id] diferentes: [id=asserFalse1] y [id=assertFalse2]. Lo cual es positivo. El problema es que no conocemos estos identificadores y es posible que los necesitemos. Este es el caso de la etiqueta [label] de la línea 2. El atributo [for] de una etiqueta HTML [label] debe hacer referencia a un atributo [id], en este caso, el generado para la etiqueta [input] de la línea 1. La documentación de Thymeleaf indica que la expresión [${#ids.prev('assertFalse')}"] permite obtener el último atributo [id] generado para el campo [assertFalse].

Ahora consideremos el código de la lista desplegable del formulario:


<select th:field="*{assertTrue}">
   <option value="true">True</option>
   <option value="false">False</option>
</select>

Este código genera el código HTML de una lista desplegable:

1
2
3
4
<select id="assertTrue" name="assertTrue">
  <option value="true">True</option>
  <option value="false">False</option>
</select>

El valor enviado se hará con el nombre [name="assertTrue"].

La vista [vue-19.xml] utiliza una hoja de estilo:


    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
</head>

Línea 4: la hoja de estilo utilizada debe colocarse en la carpeta [static] del proyecto:

  

Su contenido es el siguiente:


@CHARSET "UTF-8";

.col1 {
    background: lightblue;
}

.col2 {
    background: Cornsilk;
}

.col3 {
    background: #e2d31d;
}

.error {
    color: red;
}

Ahora, veamos las fechas:


    @NotNull
    @Future
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInFuture;
    
    @NotNull
    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInPast;

Al examinar el tráfico de red en la herramienta de desarrollo de Chrome (Ctrl-Mayús-I), se observa que las fechas se envían en el formato (aaaa-mm-dd):

 

Por eso se han anotado las fechas con el validador:


@DateTimeFormat(pattern = "yyyy-MM-dd")

que establece el formato esperado para el valor enviado de las fechas.

Por último, el archivo de mensajes en francés [messages_fr.properties]:


title=Les vues dans Spring MVC
personne.nom=Nom :
personne.age=Age :
personne.id=Identifiant :
personne.mineure=Vous êtes mineur
personne.majeure=Vous êtes majeur
liste.personnes=Liste de personnes
personne.formulaire.titre=Entrez les informations suivantes et validez
personne.formulaire.valider=Valider
personne.formulaire.saisies=Voici vos saisies
NotNull=La donnée est obligatoire
Range.securedPerson.id=L''identifiant doit être un nombre entier >=1
Range.securedPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.securedPerson.nom=Le nom doit avoir entre 1 et 4 caractères
typeMismatch=Donnée invalide
Range.stringSecuredPerson.id=L''identifiant doit être un nombre entier >=1
Range.stringSecuredPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.stringSecuredPerson.nom=Le nom doit avoir entre 1 et 4 caractères
Digits.stringSecuredPerson.id=Tapez un nombre entier de 4 chiffres au plus
Digits.stringSecuredPerson.age=Tapez un nombre entier de 2 chiffres au plus
Future.form19.dateInFuture=La date doit être postérieure à celle d''aujourd'hui
Past.form19.dateInPast=La date doit être antérieure à celle d''aujourd'hui
Size.form19.strBetween4and6=la chaîne doit avoir entre 4 et 6 caractères
Min.form19.intMin10=La valeur doit être supérieure ou égale à 10
Max.form19.intMax100=La valeur doit être inférieure ou égale à 100
Length.form19.str4=La chaîne doit avoir quatre caractères exactement
Email.form19.email=Adresse mail invalide
URL.form19.url=URL invalide
Range.form19.int1014=La valeur doit être dans l''intervalle [10,14]
AssertTrue=Seule la valeur True est acceptée
AssertFalse=Seule la valeur False est acceptée
Pattern.form19.hhmmss=Tapez l''heure sous la forme hh:mm:ss
NotEmpty=La donnée ne peut être vide
NotBlank=La donnée ne peut être vide

Veamos algunos ejemplos de ejecución:

 
 

Arriba, entre [1] y [2], parece que no ha pasado nada. Sin embargo, si miramos los intercambios de red (Ctrl-Mayús-I), vemos que ha habido dos intercambios de red con el servidor:

  • en [1], el POST inicial hacia [/v20];
  • en [2], la respuesta a esta acción es una redirección;
  • en [3], la segunda solicitud, esta vez a [/v19];

A continuación se ejecuta la acción [/v19]:


    // ------------------ visualización de un formulario
    @RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v19(Form19 formulaire) {
        return "vue-19";
}
  • línea 3, el parámetro [Form19 formulaire] se inicializa con el atributo Flash de la clave [form19], que había sido creado por la acción anterior [/v19] y que era un objeto de tipo [Form19] con los siguientes valores: los valores enviados a la acción [/v19];
  • línea 4: se mostrará la vista [vue-19.xml] con un objeto [Form19 formulaire] en su modelo, inicializado con los valores enviados. Por eso, el usuario ve el formulario tal y como lo envió;

¿Por qué una redirección? ¿Por qué no se ha enviado simplemente a la acción [/v19] anterior? Se habría obtenido el mismo resultado. Salvo algunas diferencias:

  • el navegador habría puesto en su barra de direcciones [http://localhost:8080/v20.html] en lugar de [http://localhost:8080/v19.html] como lo ha hecho aquí, ya que muestra la última URL llamada;
  • si el usuario actualiza la página (F5), no se obtiene en absoluto el mismo resultado:
    • en el caso de la redirección, el URL que se muestra es el [http://localhost:8080/v19.html] obtenido con un GET. El navegador volverá a ejecutar este último comando y obtendrá entonces un formulario completamente nuevo (el atributo Flash solo se utiliza una vez),
    • en caso de no redireccionamiento, el URL mostrado es [http://localhost:8080/v20.html] obtenido con un POST. El navegador volverá a ejecutar este último comando y, por lo tanto, volverá a generar un POST con los mismos valores enviados que anteriormente. En este caso no tiene consecuencias, pero a menudo es indeseable, por lo que generalmente se prefiere la redirección;

5.15. [/v21-/v22]: gestionar botones de radio

Consideremos el siguiente componente Spring [Listes]:

  

package istia.st.springmvc.models;

import org.springframework.stereotype.Component;

@Component
public class Listes {

    private String[] deplacements = new String[] { "0", "1", "2", "3", "4" };
    private String[] libellesDeplacements = new String[] { "vélo", "marche", "train", "avion", "autre" };
    private String[] libellesBijoux = new String[] { "émeraude", "rubis", "diamant", "opaline" };

    // getters y setters
  ...

}
  • línea 5: la clase [Listes] será un componente Spring;
  • líneas 8-10: listas utilizadas para alimentar botones de radio, casillas de verificación y listas desplegables;

En la clase de configuración [Config], se indica:


@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
  • línea 2: el paquete [models], donde se encuentra el componente [Listes], será explorado correctamente por Spring;

Creamos las siguientes acciones nuevas:


    // ------------------ formulario con botones de radio
    @Autowired
    private Listes listes;

    @RequestMapping(value = "/v21", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v21(@ModelAttribute("form") Form21 formulaire, Model model) {
        model.addAttribute("listes", listes);
        return "vue-21";
    }

    @RequestMapping(value = "/v22", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String v22(@ModelAttribute("form") Form21 formulaire, RedirectAttributes redirectAttributes) {
        redirectAttributes.addFlashAttribute("form", formulaire);
        return "redirect:/v21.html";
}
  • líneas 2-3: el componente [Listes] se inyecta en el controlador;
  • línea 6: gestionamos un formulario de tipo [Form21] que vamos a describir. Cabe señalar que hemos especificado su clave [form] en el modelo de la vista. Recordemos que, por defecto, habría sido [form21];
  • línea 7: inyectamos el componente [Listes] en el modelo. La vista lo va a necesitar;
  • línea 8: se muestra la vista [vue-21.xml]. Esta vista mostrará el formulario [Form21] y los valores enviados se pasarán a la acción [/v22] de las líneas 12-15;
  • líneas 12-15: la acción [/v22] se limita a redirigir a la acción [/v21], colocando los valores enviados que ha recibido en un atributo Flash con clave [form]. Es importante que esta clave sea la misma que la utilizada en la línea 6;

El modelo [Form21] es el siguiente:

  

package istia.st.springmvc.models;

public class Form21 {

    // valores enviados
    private String marie = "non";
    private String deplacement = "4";
    private String[] couleurs;
    private String strCouleurs;
    private String[] bijoux;
    private String strBijoux;
    private int couleur2;
    private int[] bijoux2;
    private String strBijoux2;

    // getters y setters
    ...
}

La vista [vue-21.xml] es la siguiente:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>

        <h3>Formulaire - Boutons radio</h3>
        <form action="/someURL" th:action="@{/v22.html}" method="post" th:object="${form}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Etes-vous marié(e)</td>
                        <td class="col2">
                            <input type="radio" th:field="*{marie}" value="oui" />
                            <label th:for="${#ids.prev('marie')}">Oui</label>
                            <input type="radio" th:field="*{marie}" value="non" />
                            <label th:for="${#ids.prev('marie')}">Non</label>
                        </td>
                        <td class="col3">
                            <span th:text="*{marie}"></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Mode de déplacement</td>
                        <td class="col2">
                            <span th:each="mode, status : ${listes.deplacements}">
                                <input type="radio" th:field="*{deplacement}" th:value="${mode}" />
                                <label th:for="${#ids.prev('deplacement')}" th:text="${listes.libellesDeplacements[status.index]}">Autre</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span th:text="*{deplacement}"></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>
  • líneas 36-40: cabe destacar el uso del componente [Listes] incluido en la plantilla para generar los textos de las casillas de selección;
  • La columna 3 permite conocer el valor introducido para un POST, o el valor inicial del formulario en el GET inicial;

Este código muestra la página siguiente:

 

correspondiente al siguiente código HTML:


<!DOCTYPE HTML>

<html>
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>

        <h3>Formulaire - Boutons radio</h3>
        <form action="/v22.html" method="post">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Etes-vous marié(e)</td>
                        <td class="col2">
                            <input type="radio" value="oui" id="marie1" name="marie" />
                            <label for="marie1">Oui</label>
                            <input type="radio" value="non" id="marie2" name="marie" checked="checked" />
                            <label for="marie2">Non</label>
                        </td>
                        <td class="col3">
                            <span>non</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Mode de déplacement</td>
                        <td class="col2">
                            <span>
                                <input type="radio" value="0" id="deplacement1" name="deplacement" />
                                <label for="deplacement1">vélo</label>
                            </span>
                            <span>
                                <input type="radio" value="1" id="deplacement2" name="deplacement" />
                                <label for="deplacement2">marche</label>
                            </span>
                            <span>
                                <input type="radio" value="2" id="deplacement3" name="deplacement" />
                                <label for="deplacement3">train</label>
                            </span>
                            <span>
                                <input type="radio" value="3" id="deplacement4" name="deplacement" />
                                <label for="deplacement4">avion</label>
                            </span>
                            <span>
                                <input type="radio" value="4" id="deplacement5" name="deplacement" checked="checked" />
                                <label for="deplacement5">autre</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span>4</span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>

Se observa que los valores enviados (atributos name) se introducen en los siguientes campos del modelo [Form21]:


    private String marie = "non";
    private String deplacement = "4";

Se invita al lector a realizar pruebas. Cabe destacar que es el atributo [value] de los botones de radio el que se envía.

5.16. [/v23-/v24]: gestionar casillas de selección

Añadimos la siguiente acción nueva:


    // ------------------ formulario con casillas de selección
    @RequestMapping(value = "/v23", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String av20(@ModelAttribute("form") Form21 formulaire, Model model) {
        model.addAttribute("listes", listes);
        return "vue-23";
}
  • línea 3: seguimos utilizando la plantilla [Form21];

La vista [vue-23.xml] es la siguiente:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
        <h3>Formulaire - Cases à cocher</h3>
        <form action="/someURL" th:action="@{/v24.html}" method="post" th:object="${form}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Vos couleurs préférées</td>
                        <td class="col2">
                            <input type="checkbox" th:field="*{couleurs}" value="0" />
                            <label th:for="${#ids.prev('couleurs')}">rouge</label>
                            <input type="checkbox" th:field="*{couleurs}" value="1" />
                            <label th:for="${#ids.prev('couleurs')}">vert</label>
                            <input type="checkbox" th:field="*{couleurs}" value="2" />
                            <label th:for="${#ids.prev('couleurs')}">bleu</label>
                        </td>
                        <td class="col3">
                            <span th:text="*{strCouleurs}"></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées</td>
                        <td class="col2">
                            <span th:each="label, status : ${listes.libellesBijoux}">
                                <input type="checkbox" th:field="*{bijoux}" th:value="${status.index}" />
                                <label th:for="${#ids.prev('bijoux')}" th:text="${label}">Autre</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span th:text="*{strBijoux}"></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>
  • líneas 37-41: cabe destacar el uso del componente [Listes] para generar los textos de las casillas de selección;

Este código muestra la siguiente página:

 

procedente del siguiente código HTML:


<!DOCTYPE HTML>

<html>
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
        <h3>Formulaire - Cases à cocher</h3>
        <form action="/v24.html" method="post">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Vos couleurs préférées</td>
                        <td class="col2">
                            <input type="checkbox" value="0" id="couleurs1" name="couleurs" /><input type="hidden" name="_couleurs" value="on" />
                            <label for="couleurs1">rouge</label>
                            <input type="checkbox" value="1" id="couleurs2" name="couleurs" /><input type="hidden" name="_couleurs" value="on" />
                            <label for="couleurs2">vert</label>
                            <input type="checkbox" value="2" id="couleurs3" name="couleurs" /><input type="hidden" name="_couleurs" value="on" />
                            <label for="couleurs3">bleu</label>
                        </td>
                        <td class="col3">
                            <span></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées</td>
                        <td class="col2">
                            <span>
                                <input type="checkbox" value="0" id="bijoux1" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux1">émeraude</label>
                            </span>
                            <span>
                                <input type="checkbox" value="1" id="bijoux2" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux2">rubis</label>
                            </span>
                            <span>
                                <input type="checkbox" value="2" id="bijoux3" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux3">diamant</label>
                            </span>
                            <span>
                                <input type="checkbox" value="3" id="bijoux4" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux4">opaline</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>

Cabe señalar que los valores enviados (atributos name) se introducen en los siguientes campos de [Form21]:


    private String[] couleurs;
    private String[] bijoux;

Se trata de tablas, ya que para cada campo hay varias casillas de selección con el nombre del campo. Por lo tanto, es posible que lleguen varios valores enviados con el mismo nombre (atributo name del formulario). Por lo tanto, se necesita una tabla para recuperarlos.

Volvamos al código Thymeleaf de la columna 3 de la página:


  <td class="col3">
    <span th:text="*{strCouleurs}"></span>
  </td>
</tr>
<tr>
  <td class="col1">Pierres préférées</td>
  <td class="col2">
    <span th:each="label, status : ${listes.libellesBijoux}">
      <input type="checkbox" th:field="*{bijoux}" th:value="${status.index}" />
      <label th:for="${#ids.prev('bijoux')}" th:text="${label}">Autre</label>
    </span>
  </td>
  <td class="col3">
    <span th:text="*{strBijoux}"></span>
  </td>
</tr>

Los campos a los que se hace referencia en las líneas 2 y 14 son los siguientes:


    private String strCouleurs;
    private String strBijoux;

Se calculan mediante la acción [/v24], que gestiona la acción POST:


    // mapeador Jackson / jSON
    private ObjectMapper mapper = new ObjectMapper();

    @RequestMapping(value = "/v24", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String av21(@ModelAttribute("form") Form21 formulaire, RedirectAttributes redirectAttributes) throws JsonProcessingException {
        redirectAttributes.addFlashAttribute("form", formulaire);
        formulaire.setStrCouleurs(mapper.writeValueAsString(formulaire.getCouleurs()));
        formulaire.setStrBijoux(mapper.writeValueAsString(formulaire.getBijoux()));
        return "redirect:/v23.html";
}

Hay que recordar aquí que la biblioteca jackson / jSON se encuentra entre las dependencias del proyecto.

  • línea 2: se crea un tipo [ObjectMapper] que permite serializar/deserializar objetos en jSON,
  • línea 7: se serializa en jSON la tabla de colores. El resultado se coloca en el campo [strCouleurs];
  • línea 8: se serializa en jSON la tabla de joyas. El resultado se coloca en el campo [strBijoux];

He aquí un ejemplo de ejecución:

Cabe destacar que lo que se envía es el atributo [value] de las casillas de selección.

5.17. [/25-/v26]: gestionar listas

Añadimos la siguiente acción [/v25]:


  // ------------------ formulario con listas
  @RequestMapping(value = "/v25", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
  public String v25(@ModelAttribute("form") Form21 formulaire, Model model) {
        model.addAttribute("listes", listes);
        return "vue-25";
}

La vista [vue-25.xml] es la siguiente:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>

        <h3>Formulaire - Listes</h3>
        <form action="/someURL" th:action="@{/v26.html}" method="post"
            th:object="${form}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Votre couleur préférée</td>
                        <td class="col2">
                            <select th:field="*{couleur2}">
                                <option value="0">rouge</option>
                                <option value="1">bleu</option>
                                <option value="2">vert</option>
                            </select>
                        </td>
                        <td class="col3">
                            <span th:text="*{couleur2}"></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées (choix multiple)</td>
                        <td class="col2">
                            <select th:field="*{bijoux2}" multiple="multiple" size="3">
                                <option th:each="label, status : ${listes.libellesBijoux}"
                                    th:text="${label}" th:value="${status.index}">
                                </option>
                            </select>
                        </td>
                        <td class="col3">
                            <span th:text="*{strBijoux2}"></span>
                        </td>
                    </tr>

                </tbody>
            </table>
            <input type="submit" value="Valider" />
        </form>
    </body>
</html>
  • líneas 38-42: generación de una lista de selección múltiple en la que los rótulos se toman del componente [Listes] que ya hemos utilizado;

La página mostrada es la siguiente:

 

generada por el siguiente código HTML:


<!DOCTYPE HTML>

<html>
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>

        <h3>Formulaire - Listes</h3>
        <form action="/v26.html" method="post">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Votre couleur préférée</td>
                        <td class="col2">
                            <select id="couleur2" name="couleur2">
                                <option value="0" selected="selected">rouge</option>
                                <option value="1">bleu</option>
                                <option value="2">vert</option>
                            </select>
                        </td>
                        <td class="col3">
                            <span>0</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées (choix multiple)</td>
                        <td class="col2">
                            <select multiple="multiple" size="3" id="bijoux2" name="bijoux2">
                                <option value="0">émeraude</option>
                                <option value="1">rubis</option>
                                <option value="2">diamant</option>
                                <option value="3">opaline</option>
                            </select>
                            <input type="hidden" name="_bijoux2" value="1" />
                        </td>
                        <td class="col3">
                            <span></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>
  • línea 44: se puede observar que Thymeleaf ha creado un campo oculto. No he entendido su función:
  • los valores enviados (atributos «value» de las etiquetas option) se insertarán en los siguientes campos (atributos name) de [Form21]:

    private int couleur2;
    private int[] bijoux2;
  • línea 38: la lista [bijoux2] es de selección múltiple. Por lo tanto, se pueden enviar varios valores asociados al nombre [bijoux2]. Para recuperarlos, el campo [bijoux2] debe ser una matriz. Cabe señalar que se trata de una matriz de enteros. Esto es posible, ya que los valores publicados pueden convertirse a este tipo;

Los valores se envían a la siguiente acción [/v26]:


  @RequestMapping(value = "/v26", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
  public String v26(@ModelAttribute("form") Form21 formulaire, RedirectAttributes redirectAttributes) throws JsonProcessingException {
    redirectAttributes.addFlashAttribute("form", formulaire);
    formulaire.setStrBijoux2(mapper.writeValueAsString(formulaire.getBijoux2()));
    return "redirect:/v25.html";
}

No hay nada aquí que no hayamos visto ya. He aquí un ejemplo de ejecución:

5.18. [/v27]: configuración de los mensajes

Consideremos la siguiente acción [/v27]:


  // ------------------ mensajes configurados
  @RequestMapping(value = "/v27", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
  public String v27(Model model) {
        model.addAttribute("param1","paramètre un");
        model.addAttribute("param2","paramètre deux");
        model.addAttribute("param3","paramètre trois");
        model.addAttribute("param4","messages.param4");        
        return "vue-27";
}

La acción se limita a introducir cuatro valores en la plantilla y muestra la vista [vue-27.xml] siguiente:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{messages.titre}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h2 th:text="#{messages.titre}">Spring 4 MVC</h2>
        <p th:text="#{messages.msg1(${param1})}"></p>
        <p th:text="#{messages.msg2(${param2},${param3})}"></p>
        <p th:text="#{messages.msg3(#{${param4}})}"></p>
    </body>
</html>
  • línea 8: un mensaje sin parámetros;
  • línea 9: un mensaje con un parámetro [$param1] tomado del modelo;
  • línea 10: un mensaje con dos parámetros [$param2, $param3] tomados del modelo;
  • línea 11: un mensaje con un parámetro. Este parámetro es a su vez una clave de mensaje (presencia de #). La clave la proporciona [$param4];

El archivo de mensajes en francés es el siguiente:

[messages_fr.properties]


messages.titre=Messages paramétrés
messages.msg1=Un message avec un paramètre : {0}
messages.msg2=Un message avec deux paramètres : {0}, {1}
messages.msg3=Un message avec une clé de message comme paramètre : {0}
messages.param4=paramètre quatre

Para indicar la presencia de parámetros en el mensaje, se utilizan los símbolos {0}, {1}, ...

La fusión de la plantilla creada por la acción [/v27] con la vista [vue-27] generará el siguiente código HTML:


<!DOCTYPE html>

<html>
    <head>
        <title>Messages paramétrés</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h2>Messages paramétrés</h2>
        <p>Un message avec un paramètre : paramètre un</p>
        <p>Un message avec deux paramètre : paramètre deux, paramètre trois</p>
        <p>Un message avec une clé de message comme paramètre : paramètre quatre</p>
    </body>
</html>

lo que da como resultado la siguiente vista:

 

El archivo de mensajes en inglés es el siguiente:

[messages_fr.properties]


messages.titre=Parameterized messages
messages.msg1=Message with one parameter: {0}
messages.msg2=Message with two parameters: {0}, {1}
messages.msg3=Message with a message key as a parameter: {0}
messages.param4=parameter four

La fusión de la plantilla creada por la acción [/v27] con la vista [vue-27] generará el siguiente código HTML:


<!DOCTYPE html>

<html>
    <head>
        <title>Parameterized messages</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h2>Parameterized messages</h2>
        <p>Message with one parameter: paramètre un</p>
        <p>Message with two parameters: paramètre deux, paramètre trois</p>
        <p>Message with a message key as a parameter: parameter four</p>
    </body>
</html>

lo que da como resultado la siguiente vista:

 

Se observa que el último mensaje se ha internacionalizado de principio a fin, lo que no ocurre con los dos anteriores.

5.19. Uso de una página maestra

En una aplicación web, es frecuente que las vistas compartan una serie de elementos que se pueden agrupar en una página maestra. He aquí un ejemplo:

Arriba tenemos dos páginas similares en las que el fragmento [1] ha sido sustituido por el fragmento [2]. La vista es la de una página maestra con tres fragmentos fijos [3-5] y un fragmento variable [6].

5.19.1. El proyecto

Creamos un proyecto [springmvc-masterpage] siguiendo el procedimiento del apartado 5.1.

  

El archivo [pom.xml] es el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>istia.st.springmvc</groupId>
    <artifactId>springmvc-masterpage</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springmvc-masterpage</name>
    <description>Page maître</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.9.RELEASE</version>
        <relativePath/> <!-- búsqueda de elemento principal en el repositorio -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>istia.st.springmvc.main.Main</start-class>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Una de las dependencias que aporta este archivo es necesaria para la página maestra:

 

Los paquetes [config] y [main] son idénticos a los del proyecto anterior con los mismos nombres.

5.19.2. La página maestra

  

La página maestra es la vista [layout.xml] siguiente:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <title>Layout</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <table style="width: 400px">
            <tr>
                <td colspan="2" bgcolor="#ccccff">
                    <div th:include="entete" />
                </td>
            </tr>
            <tr style="height: 200px">
                <td bgcolor="#ffcccc">
                    <div th:include="menu" />
                </td>
                <td>
                    <section layout:fragment="contenu">
                        <h2>Contenu</h2>
                    </section>
                </td>
            </tr>
            <tr bgcolor="#ffcc66">
                <td colspan="2">
                    <div th:include="basdepage" />
                </td>
            </tr>
        </table>
    </body>
</html>
  • línea 2: la página maestra debe definir el espacio de nombres [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"], de cuyo elemento se utiliza uno en la línea 19;
  • líneas 10-12: generan el campo [1] que se muestra a continuación. La etiqueta Thymeleaf [th:include] permite incluir en la vista actual un fragmento definido en otro archivo. Esto permite factorizar los fragmentos utilizados en varias vistas;
  • líneas 15-17: generan el área [2] que se muestra a continuación;
  • líneas 19-20: generan el área [3] que se muestra a continuación. El atributo [layout:fragment] es un atributo del espacio de nombres [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"]. Indica un área que, en tiempo de ejecución, puede sustituirse por otra;
  • líneas 24-28: generan el campo [4] que se muestra a continuación;

5.19.3. Los fragmentos

Los fragmentos [entete.xml], [menu.xml] y [basdepage.xml] son los siguientes:

[entete.xml]


<!DOCTYPE html>
<html>
    <h2>entête</h2>
</html>

[menu.xml]


<!DOCTYPE html>
<html>
    <h2>menu</h2>
</html>

[basdepage.xml]


<!DOCTYPE html>
<html>
    <h2>bas de page</h2>
</html>

El fragmento [page1.xml] es el siguiente:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layout">
    <section layout:fragment="contenu">
        <h2>Page 1</h2>
        <form action="/someURL" th:action="@{/page2.html}" method="post">
            <input type="submit" value="Page 2" />
        </form>
    </section>
</html>
  • línea 2: el atributo [layout:decorator="layout"] indica que la página actual [page1.xml] está «decorada», es decir, que pertenece a una página maestra. Esta es el valor del atributo, en este caso la vista [layout.xml];
  • línea 3: se indica en qué fragmento de la página maestra se insertará [page1.xml]. El atributo [layout:fragment="contenu"] indica que [page1.xml] se insertará en el fragmento denominado [contenu], es decir, el área [3] de la página maestra;
  • líneas 5-7: el contenido del fragmento es un formulario que ofrece un botón de POST hacia la acción [/page2.html];

El fragmento [page2.xml] es similar:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
    layout:decorator="layout">
    <section layout:fragment="contenu">
        <h2>Page 2</h2>
        <form action="/someURL" th:action="@{/page1.html}" method="post">
            <input type="submit" value="Page 1" />
        </form>
    </section>
</html>

5.19.4. Las acciones

 

El controlador [Layout.java] es el siguiente:


package istia.st.springmvc.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class Layout {
    @RequestMapping(value = "/page1")
    public String page1() {
        return "page1";
    }

    @RequestMapping(value = "/page2", method=RequestMethod.POST)
    public String page2() {
        return "page2";
    }
}
  • líneas 10-12: la acción [/page1] se limita a mostrar la vista [page1.xml];
  • líneas 15-17: lo mismo ocurre con la acción [/page2], que muestra la vista [page2.xml];