Introduction à Thymeleaf

Thymeleaf est un template engine (moteur de rendu de document) écrit en Java. Principalement conçu pour produire des vues Web, il fournit un support pour la génération de documents HTML, XHTML, JavaScript et CSS (voire de n’importe quel format texte).

Il existe plusieurs template engines en Java : JSP (directement inclus dans le moteur de Servlet des serveurs Web Java), FreeMarker, Velocity… Il n’est donc pas facile de savoir quelle solution choisir. Thymeleaf a cependant plusieurs avantages par rapport aux autres template engines :

  • il permet de créer des templates qui respectent le format du document. Si vous écrivez un template pour une page HTML, le template sera une page HTML valide.

  • il permet de fournir des valeurs par défaut dans un template. On peut ainsi concevoir des templates pour un site Web avec des données d’exemple et le site reste navigable hors ligne comme un simple site statique. Cela est particulièrement utile si vous travaillez en collaboration avec des designers Web ou si vous voulez réaliser un maquettage de votre site.

  • il s’intègre parfaitement dans une application Spring puisque vous pourrez utiliser le langage d’expression de Spring (SpEL) pour dynamiser vos templates.

  • il est facile à apprendre.

Note

Ce chapitre est une brève introduction à Thymeleaf, n’hésitez pas à vous reporter à la documentation officielle ainsi qu’à la documentation sur l’intégration dans Spring.

Intégration de Thymeleaf

Nous allons voir comment intégrer le moteur de rendu Thymeleaf dans une application.

Pour une application avec Spring Boot

Pour une application utilisant Spring Boot, la configuration est très simple. Il vous suffit d’ajouter une dépendance dans votre projet au module spring-boot-starter-thymeleaf. Si vous utilisez Maven, il vous faut ajouter dans votre fichier pom.xml :

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

Par défaut, les templates Thymeleaf doivent être placés dans le répertoire src/main/resources/templates de votre projet. Les fichiers statiques (non transformés par Thymeleaf) doivent être placés dans le répertoire src/main/resources/static de votre projet.

Note

Si vous voulez changer les répertoires par défaut, vous pouvez utiliser les propriétés spring.thymeleaf.prefix et spring.web.resources.static-locations dans le fichier de configuration src/main/resources/application.properties.

Il existe de nombreuses autres propriétés qui sont documentées dans la section Templating Properties de la documentation Spring Boot pour paramétrer l’intégration de Thymeleaf.

Pour une application sans Spring Boot

L’intégration de Thymeleaf dans une application sans Spring Boot est plus compliquée car il est nécessaire de créer la configuration du moteur du rendu. Si vous voulez vraiment faire cette intégration, alors vous pouvez vous reporter à la documentation officielle de Thymeleaf.

Les attributs Thymeleaf

Un template Thymeleaf pour une page HTML5 est une page HTML utilisant des attributs Thymeleaf qui seront interprétés par le serveur pour produire la page finale.

Afin de respecter le format HTML5, les attributs thymeleaf sont déclarés comme des attributs data-th-*. Ces attributs ne seront pas présents dans la page finale mais permettent de garantir que le template est lui-même une page HTML5 valide.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Une page</title>
  </head>
  <body>
        <div data-th-text="${ user.login }">David</div>
  </body>
</html>

Dans l’exemple ci-dessus, on utilise l’attribut data-th-text pour indiquer le contenu texte de la balise <div>. Le texte David sera remplacé à l’exécution par le résultat de l’expression ${ user.login }.

Note

Si vous voulez produire des documents XHTML ou XML, vous pouvez déclarer Thymeleaf comme un espace de nom XML pour garantir que le template est un document XHTML ou XML bien formé.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
                      "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>Une page</title>
  </head>
  <body>
    <div th:text="${ user.login }">David</div>
  </body>
</html>

Le contenu des attributs Thymeleaf peut être du simple texte ou une expression qui sera interprétée à l’exécution. Les expressions sont délimitées par :

${…}

Pour les expressions à évaluer

*{…}

Pour les expressions de sélection à évaluer

#{…}

Pour les expressions de message

@{…}

Pour les expressions d’URL

~{…}

Pour les expressions de fragment

${…} : l’expression d’évaluation

Une expression délimitée par ${...} représente une expression à évaluer. Dans une application Spring, cette expression est écrite avec SpEL et peut référencer n’importe quel objet présent dans le modèle.

${ user.login }
${ facture.montantTotal }

Note

Par défaut, les objets param, session et application sont disponibles pour accéder aux paramètres de la requête, aux objets présents dans la session utilisateur et aux objets présents dans la portée Java EE de l’application :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Une page</title>
  </head>
  <body>
    <div data-th-text="${ 'Bonjour ' + param.nom }">Bonjour vous</div>
  </body>
</html>

Le template ci-dessus utilise la valeur du paramètre de la requête nom pour construire le message d’accueil.

Il est également possible de référencer n’importe quel bean du contexte d’application en précisant @ devant son nom. Si dans une application Spring, nous avons un bean :

package dev.gayerie;

import org.springframework.stereotype.Component;

@Component
public class Info {
    private String message = "Bonjour";

    public String getMessage() {
      return message;
    }
}

Il est possible d’accéder dans une expression à la valeur de l’attribut message du bean info :

${ @info.message }

Il existe également des objets prédéfinis qui sont accessibles en préfixant leur nom avec #. On trouve des objets dépendants du contexte d’exécution comme #locale :

${ #locale.displayLanguage }

On trouve également des objets utilitaires pour nous aider à manipuler ou à formater des données (date, URI, nombres, listes…)

${ #strings.listJoin({'hello', 'world'}, ' ') }

L’expression ci-dessus appelle la méthode listJoin de l’objet utilitaire #strings pour concaténer un tableau passé en paramètre et en insérant un séparateur passé en second paramètre. Le résultat de cette expression est donc :

hello world

Note

Même si cela n’est pas indiqué dans la documentation officielle, Thymeleaf fournit un objet #temporals pour nous aider à manipuler les objets du package java.time pour représenter les dates et le temps :

${ #temporals.format( T(java.time.LocalDate).now(), 'dd MMMM YYYY') }

*{…} : l’expression de sélection

Un expression délimitée par *{...} représente une expression à évaluer pour sélectionner un attribut d’un objet. On utilise l’attribut Thymeleaf object pour définir l’objet de référence. Ce type d’expression est très utile lorsqu’on veut construire des formulaires ou afficher les propriétés d’un objet du modèle. Imaginons que nous voulons afficher les informations d’un objet du modèle qui s’appelle utilisateur, cet objet contient les propriétés prenom, nom et age. Nous pouvons indiquer que cet objet est l’objet de référence et sélectionner chaque propriété avec *{...} :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Une page</title>
  </head>
  <body>
    <div data-th-object="${ utilisateur }">
        <p>Prénom : <span data-th-text="*{ prenom }">Jean</span></p>
        <p>Nom : <span data-th-text="*{ nom }">Dubois</span></p>
        <p>Age : <span data-th-text="*{ age }">35</span> ans</p>
    </div>
  </body>
</html>

#{…} : l’expression de message

Une expression délimitée par #{...} représente un message à extraire d’un fichier de ressources. Dans une application Spring Boot, vous pouvez externaliser vos messages dans le fichier src/main/resources/messages.properties. Pour en savoir plus, reportez-vous à la section du chapitre Spring Web MVC consacrée à ce sujet.

@{…} : l’expression d’URL

Une expression délimitée par @{...} représente une URL qui doit être résolue par rapport au contexte de génération de la page. Tous les liens internes à votre application référençant vos pages, vos feuilles de style, vos fichiers de script, … doivent utiliser des attributs Thymeleaf pour positionner le chemin. L’attribut HTML correspondant peut être conservé pour permettre d’ouvrir la page localement.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Une page</title>
    <link rel="stylesheet" href="../static/style.css" data-th-href="@{/style.css}">
    <script src="../static/app.js" data-th-src="@{/app.js}"></script>
  </head>
  <body>
    <nav>
      <ul>
        <li><a href="./inscription.html" data-th-href="@{/inscription}">S'inscrire</a></li>
        <li><a href="./reservation.html" data-th-href="@{/reservation}">Réserver</a></li>
      </ul>
    </nav>
  </body>
</html>

Prévention de l’injection

Thymeleaf est configuré par type de fichier pour prévenir l’injection de données. Pour la génération de page HTML, Thymeleaf fera un échappement HTML pour les caractères réservés comme < ou >.

Ainsi le template :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Une page</title>
  </head>
  <body>
    <div data-th-text="${ '<strong>Bonjour</strong>' }"></div>
  </body>
</html>

produira la page HTML finale :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Une page</title>
  </head>
  <body>
    <div>&lt;strong&gt;Bonjour&lt;/strong&gt;</div>
  </body>
</html>

Ainsi, les données dynamiques dans la page ne modifient pas la structure du template et l’injection HTML n’est pas possible.

Les conditions et les boucles

Dans un template, il est très souvent nécessaire de produire une partie d’un document à certaines conditions ou de répéter certaines portions (par exemple pour générer un tableau). Le langage SpEL ne permet pas d’exprimer des structures aussi complexes. Par contre, il existe les attributs Thymeleaf if et each qui sont fait pour cela.

L’attribut if permet de contrôler la production de la balise et de tout ce qu’elle contient suivant l’expression d’une condition :

<div data-th-if="${ commande.remise > 0 }">
    Vous pouvez disposez d'une remise de
    <span data-th-text="${ commande.remise }"></span>%
</div>

L’attribut each permet de parcourir un tableau ou une liste ou n’importe quelle structure itérable et de répéter la balise et tout son contenu. On peut ainsi produire un tableau HTML :

<table>
  <thead>
    <tr>
      <th>Nom</th>
      <th>Age</th>
   </tr>
  </thead>
  <tbody>
    <tr data-th-each="p : ${ personnes }">
      <td data-th-text="${ p.nom }">Jean Dubois</td>
      <td data-th-text="${ p.age }">35</td>
    </tr>
  </tbody>
</table>

Notez la syntaxe particulière de l’attribut each qui rappelle le for amélioré en Java. Il permet de déclarer une variable temporaire qui va représenter chaque élément de la collection. Dans notre exemple, on déclare la variable p.

Note

Il est possible d’utiliser conjointement l’attribut object afin d’utiliser des expressions de référence :

<table>
  <thead>
    <tr>
      <th>Nom</th>
      <th>Age</th>
   </tr>
  </thead>
  <tbody>
    <tr data-th-each="p : ${ personnes }" data-th-object="p">
      <td data-th-text="*{ nom }">Jean Dubois</td>
      <td data-th-text="*{ age }">35</td>
    </tr>
  </tbody>
</table>

Opérateur de sûreté

Attention à la gestion des valeurs nulles dans le modèle. Si on veut afficher la valeur de l’attribut nom d’un objet du modèle appelé utilisateur. On peut écrire :

<span data-th-text="${ utilisateur.nom }">le nom ici</span>

Mais que se passe-t-il si l’objet utilisateur n’est pas présent dans le modèle ? La génération de la page va échouer avec une erreur car utilisateur vaut null.

SpEL définit un opérateur de sûreté (safe navigation operator) : ?.. Si nous modifions l’expression :

<span data-th-text="${ utilisateur?.nom }">le nom ici</span>

La génération de la page n’échouera plus si l’objet utilisateur n’est pas disponible. Le résultat de l’expression utilisateur?.nom sera simplement une chaîne vide.

Note

L’opérateur de sûreté est utilisable à tous les niveaux d’une expression :

${ utilisateur?.adresse?.ville?.toUppercase() }

Le formatage des données

Les objets utilitaires disponibles dans les expressions peuvent nous permettre de formater les données à afficher. Par exemple, s’il existe un objet maintenant dans le modèle de type Date, nous pouvons utiliser l’objet utilitaire #dates pour spécifier un formatage de cette date :

${ #dates.format(maintenant, 'dd MMMM YYYY') }

Cependant, il est généralement plus simple de définir un format pour la données élémentaires (dates, nombres, booléen…) qui sera utilisé par défaut lors du rendu des pages.

Une application Spring peut enregistrer des formateurs de données à son lancement. Pour cela, il faut ajouter un bean de configuration dans le contexte de l’application. Ce bean doit implémenter l’interface WebMvcConfigurer et implémenter la méthode addFormatters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package dev.gayerie;

import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.number.NumberStyleFormatter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class PizzaSpringMvcConfigurer implements WebMvcConfigurer {
  @Override
  public void addFormatters(FormatterRegistry registry) {
    registry.addFormatter(new NumberStyleFormatter("#,##0.##"));
  }
}

Dans notre exemple ci-dessus, nous ajoutons une instance de NumberStyleFormatter (une classe fournie par le Spring Framework) dans les formateurs Spring. Cette classe utilise la classe Java standard NumberFormat et nous pouvons passer en paramètre le motif pour la mise en forme des nombres.

Nous pouvons activer le formatage par défaut en utilisant ${{...}} (deux paires d’accolades) plutôt que ${...} pour l’évaluation de nos expressions. Ainsi :

<div>
  <span data-th-text="${{ 1024 * 1024 }}"></span>
  <span data-th-text="${{ T(java.lang.Math).PI }}"></span>
</div>

produira :

<div>
  <span>1 048 576</span>
  <span>3,14</span>
</div>

Alors que si nous utilisons une seule paire d’accolades dans nos expressions :

<div>
  <span data-th-text="${ 1024 * 1024 }"></span>
  <span data-th-text="${ T(java.lang.Math).PI }"></span>
</div>

Le résultat sera :

<div>
  <span>1048576</span>
  <span>3.141592653589793</span>
</div>