Strutturare il layout delle pagine con Thymeleaf

Chi segue da un po’ di tempo i miei articoli su Spring, conosce già la mitica SuperRubricaTelefonica!, compagna di tante scorribande! :-)

Finora ci siamo concentrati sulle funzionalità dell’applicazione, ma ora è giunto il momento di dedicare un po’ di attenzione anche alle View della nostra cara Rubrica.

Sia chiaro: non ho alcuna intenzione di dare lezioni di UX o usabilità, quanto piuttosto di affrontare il tema dei layout di pagina e di come implementarli con Thymeleaf.

In questo articolo utilizzeremo sia il cosiddetto approccio standard o “include-style”, sia l’approccio gerarchico per strutturare in modo più efficace le nostre pagine, con un occhio di riguardo alla modularità e al riutilizzo del codice.

Lo Standard Layout System

Spesso le pagine di un’applicazione web sono strutturate seguendo un layout comune; inoltre può capitare che uno stesso componente, come per esempio un header o un particolare bottone, sia utilizzato all’interno di pagine differenti.

Risulta quindi molto utile scomporre le pagine delle nostre applicazioni in “moduli” che possano poi essere riutilizzati in differenti contesti.

L’approccio più comune per ottenere questo tipo di modularità è quello di creare dei frammenti di pagina da includere all’occorrenza all’interno dei nostri template.

Thymeleaf permette d’implementare l’inclusione dei page fragment tramite il suo Standard Layout System, che fa uso degli attributi th:insert e th:replace.

In particolare, utilizzando th:insert andremo ad inserire il frammento di pagina all’interno dell’elemento a cui abbiamo applicato l’attributo, sostituendo il suo contenuto, mentre con th:replace andremo a sostituire l’elemento stesso con il frammento specificato.

Se ci avete fatto caso, in precedenza abbiamo già utilizzato l’inclusione di fragment nelle pagine della rubrica telefonica. Il frammento page-header.html, che include il bottone per il Logout, viene inserito nei template HTML tramite l’attributo th:replace:

<div th:replace="fragments/page-header :: header"></div>

L’espressione fragments/page-header :: header indica a Thymeleaf che esiste un frammento denominato header all’interno del file fragments/page-header.html. Ed infatti se andate a guardare il codice presente nel file, troverete un blocco del tipo:

<div th:fragment="header">
  <h1>SuperRubricaTelefonica!</h1>
  ...
</div>

Piccola nota È possibile dichiarare differenti frammenti di pagina all’interno dello stesso file HTML, basterà semplicemente identificarli mediante un corrispondente attributo th:fragment.

Layout gerarchici

L’inclusione dei fragment all’interno dei template Thymeleaf è sicuramente l’approccio più semplice e intuitivo per mettere ordine nelle nostre pagine e riutilizzare in modo più efficace i componenti comuni.

Nonostante ciò, l’utilizzo dello Standard Layout System non risolve il problema della ripetizione del codice

Che succede se devo aggiungere/rimuovere un componente da tutti i template della mia applicazione?

Già… bisognerebbe mettere mano a tutti i file che necessitano di aggiornamento… Ecco dunque l’utilità di una soluzione basata su un approccio gerarchico.

Thymeleaf non implementa nativamente questo tipo di sistema per i layout dei template. Fortunatamente però il pacchetto Layout Dialect integra quanto manca nel framework originale.

Nello specifico il Thymeleaf Layout Dialect consente di utilizzare il concetto di decorator per definire una gerarchia di template sulla base dei quali andremo a costruire tutte le pagine della nostra applicazione.

Diamo un’occhiata al codice d’esempio per chiarire meglio il concetto:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
  <head>
    <meta charset="utf-8"/>
    <title layout:title-pattern="$CONTENT_TITLE | $DECORATOR_TITLE">
      SuperRubricaTelefonica!</title>
    ...
    <link rel="stylesheet" th:href="@{/css/main.css}"/>
  </head>
  <body>
    <div class="container">
      <div th:replace="fragments/page-header :: header"></div>
      <div layout:fragment="content"></div>
    </div>
  </body>
</html>

Il file main-template.html costituisce la radice della nostra gerarchia di template (gerarchia molto semplice, nel nostro caso).

Alcuni particolari da notare:

  • L’espressione layout:title-pattern="$CONTENT_TITLE | $DECORATOR_TITLE", nel tag title, permette di definire un pattern per il titolo delle pagine che ereditano dal template. In particolare, $CONTENT_TITLE consente di ottenere il titolo della pagina “figlio”, mentre con $DECORATOR_TITLE facciamo riferimento al titolo del template “padre”.
  • Possiamo utilizzare il Layout Dialect in combinazione con lo Standard Layout System ed includere eventuali frammenti nei nostri template (ad es. page-header).
  • All’interno del template possiamo definire le sezioni che saranno “sovrascritte” dai template figlio. Ad esempio, nel nostro caso la sezione indicata con l’attributo layout:fragment="content" costituisce il corpo principale della pagina, che sarà definito nei template che andranno a “decorare” main-template.

Nota In questo post faccio riferimento alla versione 1.x del Thymeleaf Layout Dialect, versione inclusa nel pacchetto spring-boot-starter-thymeleaf di Spring Boot. Recentemente è stata però rilasciata una nuova versione del Layout Dialect (la 2.x) che presenta alcune variazioni nel nome di alcune variabili e attributi. Se siete interessati alle differenze tra le due versioni della libreria vi rimando a questa pagina del manuale ufficiale.

Prendiamo ora il template della nostra home page osserviamo come possiamo sfruttare il Layout Dialect:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="main-template">
<head>
    <title>Home</title>
</head>
<body>
<div layout:fragment="content">
    <ul class="home-menu">
        <li><a th:href="@{/contacts/new}">Crea nuovo contatto</a></li>
        <li><a th:href="@{/contacts}">Lista contatti</a></li>
    </ul>
</div>
</body>
</html>

Nel file home.html notiamo innanzitutto che le parti comuni a tutte le pagine della nostra Rubrica sono scomparse; le abbiamo infatti riportate nel main-template. Abbiamo inoltre aggiunto due attributi importanti:

  • nel tag html, l’attributo layout:decorator="main-template" indica a Thymeleaf quale template stiamo estendendo/decorando;
  • il div con l’attributo layout:fragment="content" definisce invece il corpo della pagina e andrà a “rimpiazzare” la corrispondente sezione content del main-template.

In conclusione

In questo post abbiamo brevemente introdotto il concetto di page layout e di come implementare con Thymeleaf template modulari e riutilizzabili all’interno dei nostri progetti.

L’approccio include-style è sicuramente familiare a chi ha esperienza con le JSP, mentre l’utilizzo di template gerarchici sarà quello preferito da chi ha avuto modo di lavorare con framework come Apache Tiles.

Thymeleaf prende il meglio di entrambi i sistemi e permette di applicare questi due stili di template senza dover ricorrere a ulteriori strumenti/librerie. Sta a noi scegliere quale approccio si adatta meglio alle nostre esigenze e a quelle del progetto, oppure, semplicemente, possiamo utilizzare tutte e due le metodologie.

Buon lavoro e alla prossima,

David