Introduction to ThymeLeaf - Integration with Spring Boot

Introduction

For the previous big project I have done for myself, I used Spring Boot, packaged the application as a war file so that it can use TagLib. The project is a success. There is an unsolved issue. The TagLib jar file throws an exception when running under Java 9 or above. There is no replacement jars that solves this problem. I love TagLib and the template, and modularization of the html pieces. If this jar is going to give me trouble down the road, I need to have a solution for it.

The solution is ThymeLeaf. It came out sometime in 201x. I don't remember when. I didn't care to learn because I can use TagLib for all the design I needed to do. For any framework, I can use 10% of its capability to solve 95% of all the problem at hand. It was a handy skill. Now that TagLib is going to be a problem, it is time to switch to ThymeLeaf. This is part of evolution.

For this tutorial, I like to discuss how to setup a Spring Boot application that incorporate ThymeLeaf library, how to create re-usable pieces and use them on the actual pages. This tutorial will also discuss the three basic attributes that can solve the most common design problems.

The Maven POM File

The Maven POM file for the sample project is the standard Spring Boot project, using the Spring Boot starter parent as its parent.

To compile and package the application, the Maven POM file should have the following two dependencies:

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

The first dependency is the Spring Boot starter for web application. It takes care of the most of the bootstrap of constructing a simple web application.

The second dependency is the ThymeLeaf integration with Spring Boot. It takes care of the configuration of ThymeLeaf page rendering within the application.

With these two jars, the hard part of setting up the Spring configuration so that the application can run is all taken care. It saved a lot of time for programmers.

Prject Folder Structure

For this project, I used the same project file structure as before. The Java files are located in the sub-folder under folder src/main/java/. The html page template files are located in src/main/resources/templates. The fragments used by the ThymeLeaf templates are located in src/main/resources/templates/parts. And the static files such as CSS and JavaScript files are located in sub-folders of src/main/resources/static/

These folder locations are based on default configuration. I am sure I can change these locations in the application.properties file. These can be found online by search.

The Java Code

In order to run this sample application, only two java files are needed. One is the main entry, App.java. The other file is the controller class file called IndexController.java.

The main entry file looks like this:

package org.hanbo.spring.sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

This is the typical way of starting an Spring Web application using Spring Boot. In order to make the application more feature rich, I an add some more annotations, but for a simple example application, this is enough to get the application going.

The other file is the controller class. Inside, there are two methods, each method is used to handle the HTTP request:

package org.hanbo.spring.sample.controllers;

import java.util.ArrayList;
import java.util.List;

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

@Controller
public class IndexController
{
   @RequestMapping(value="/", method=RequestMethod.GET)
   public String home()
   {
      final String retVal = "redirect:/mixedup";
      
      return retVal;
   }

   @RequestMapping(value="/mixedup", method=RequestMethod.GET)
   public ModelAndView mixedup()
   {
      List<String> items = new ArrayList<String>();
      items.add("Simple Item 1");
      items.add("Simple Item 2");
      items.add("Simple Item 3");
      items.add("Simple Item 4");
      items.add("Simple Item 5");
      
      ModelAndView retVal = new ModelAndView();
      retVal.setViewName("mixedup");
      retVal.addObject("siteName", "Thymeleaf Sample");
      retVal.addObject("listTitle",
         "<span style=\"color: green;\">A Test Unordered List</span>");
      retVal.addObject("items", items);
      retVal.addObject("conditionVal", 5);
      
      return retVal;
   }
}

The second request handles the HTTP GET request to the URL: http://localhost:8080/mixedup

The second method returns an object of ModelAndView. This object has the name of the view, which is the template page file (with all the place holders. This object also has the data model, a hash map of objects that can be set as values to the place holders on the page template.

The request will trigger the page template created using ThymeLeaf markup. As discussed in the introduction, I created this sample application to exercise some simple aspects, which should solve 95% of the design issues I would face. These simple aspects included:

  • How to set the HTML text for an element on the page, using the model data returned by the Java method.
  • How to loop through a collection of items and display them on the page.
  • How to display an element conditionally.
  • How to add HTML components from another file in to the page, and use the data model from Java code to set element text. This last part is cut and slice the part of a page, define them as components so that they can be used in different pages.

How the template pages works with the values in the data model will be discussed next.

ThymeLeaf Page Template

For this sample application, I have just one page. It is called mixedup.html, located in src/main/resouces/templates.html. This page is constructed from several different parts. And these parts or components has the placeholder for values to be rendered by the methods in the controller class.

The full source code of the page looks like this:

<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <title>HanBo-ORG Mixedup Page</title>
    <link rel="stylesheet" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
    <link rel="stylesheet" th:href="@{/bootstrap/css/bootstrap-theme.min.css}"/>
    <link rel="stylesheet" th:href="@{/css/index.css}"/>
</head>
<body>
   <div th:replace="components/parts::header">
   </div>
   
   <div class="container">
      <div th:insert="components/parts::info">
      </div>
      <div th:replace="components/parts::looptest">
      </div>
      <div th:if="${conditionVal == 5}">
         <div th:replace="components/parts::showThis"></div>
      </div>
      <div th:unless="${conditionVal == 5}">
         <div th:replace="components/parts::showThat"></div>
      </div>
   </div>
    
   <div th:replace="components/parts::footer"></div>
   
   <div th:replace="components/parts::stdjs"></div>
 </body>
</html>

Let's start from the top. For any rendering engine, it is necessary to import the namespace of the rendering engine, so that it can find the special tags during the rendering processing. For ThymeLeaf, this is how it is done:

<html lang="en" xmlns:th="http://www.thymeleaf.org">

Then, at the head section, it is the first place where ThymeLeaf markup is used:

    <link rel="stylesheet" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
    <link rel="stylesheet" th:href="@{/bootstrap/css/bootstrap-theme.min.css}"/>
    <link rel="stylesheet" th:href="@{/css/index.css}"/>

In this case, I was use the attribute th:href. And I was passing string constants as URLs to these three lines.

Next, I used parts defined in another file to be inserted or placed at places defined in this page file, like this:

   <div th:replace="components/parts::header">
   </div>

There are some more usage like this:

      <div th:replace="components/parts::looptest">
      </div>
      <div th:if="${conditionVal == 5}">
         <div th:replace="components/parts::showThis"></div>
      </div>
      <div th:unless="${conditionVal == 5}">
         <div th:replace="components/parts::showThat"></div>
      </div>
...
   <div th:replace="components/parts::footer"></div>
   
   <div th:replace="components/parts::stdjs"></div>

Taking one from these lines, the attribute is called th:replace. It can also be th:insert. There is also the th:include which is deprecated, and soon it will not be available.

The value of these attributes specifies where there the parts file is and which code snippet in this file will be placed here. For example, the value "components/parts::looptest" indicates that the file that contains reusable parts is located at a sub-folder "components" in the templates folder, the name of the file is called "parts.html". The :: separator specifies in the parts file where the fragment can be found. In this example, the fragment in the part file is called "looptest".

The last significant part of this file is the conditional placement of value, something like an if ... else ... block. This is how it is done with ThymeLeaf:

      <div th:if="${conditionVal == 5}">
         ...
      </div>
      <div th:unless="${conditionVal == 5}">
         ...
      </div>

Note that the else block (attribute th:unless) must have the same condition check as the condition check for the if. If you use a different condition check, the display will be really weird.

Time to see what the fragments look like. These fragments are defined in one file called parts.html. We will discuss this next.

ThymeLeaf Fragments

In order to build a page with reusable parts, the parts has to be placed somewhere so that when the page is rendered, these parts can be extracted from this common location and be added to the page. ThymeLeaf does it differently. The reusable parts can be packaged in a file. Each part has an ID. Then in the constructed page, these fragments can be referenced through the targeted folder, the parts file, and the fragment ID.

Remember from the previous section, the parts being replaced, inserted or included, in the legacy way. It may looks like this:

   <div th:replace="components/parts::header">
   </div>

The value in the attribute th:replace "components/parts::header". The value "components/parts" is the file where the fragments are defined. If you assume the file is called "components/parts.html", you are right. The base path to find the file is "resources/templates". So the relative full path is "resources/templates/components/parts.html". This file looks like this:

<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
   <head>
      <meta charset="UTF-8"/>
      <title>Spring Boot Thymeleaf Application - Fragments</title>
   </head>
   <body>
      <div th:fragment="header">
    <nav class="navbar navbar-default navbar-fixed-top">
      <div class="container-fluid">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" th:href="@{/}" th:text="${siteName}"></a>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
        </div>
      </div>
    </nav>
      </div>

      <div class="row page-start" th:fragment="info">
         <div class="col-xs-12">
            <p>
            Lorem ipsum dolor sit amet, per eu natum probatus, no habeo posse invidunt eos. Qui ad audire vivendum detraxit, quod dico vocibus pri in, et purto feugait vim. Ius causae ceteros dolores in, at noster delenit nam. Aliquip integre offendit sit ut.
            </p>
            <p>
            Graecis definitiones et pri. Postea detraxit nec ei, audiam diceret maluisset eam cu. Ut his etiam minim semper, duis postea epicuri nec id, an maiestatis vituperata his. Ei sea verear dissentias, qui simul senserit efficiantur te. Te dicam soluta nam, ea eum persius iudicabit. Eu omnes offendit splendide pro, discere definitionem vel id, veritus habemus quaestio ad quo.
            </p>
            <p>
            Nam an melius consequat, id nam inermis accusata reprehendunt, qui eu quem unum omnium. Per enim nostrud et, quodsi omnesque referrentur at usu. Ut qui gubergren reprehendunt, ne alia veritus vis. Ut has cibo mediocrem consequuntur. Mundi facilisi eam an.
            </p>
            <p>
            Ex eos movet persequeris referrentur. Essent mediocritatem eu eos. Sea at elit vulputate, alia ludus choro vim id, mel at munere moderatius definitiones. Mei eu debet partem ubique, cu verear noluisse mei. Eum ullum dictas consulatu an, dicunt delicatissimi ius te, ne feugait tincidunt has.
            </p>
            <p>
            Ex posse perfecto sit, soluta ocurreret scribentur ut sea. Admodum intellegam at nec, eam ex dictas accusam dolores. Ut nec dicta veritus, in meis verear fuisset vix, sea in solum tantas virtute. No mea agam graecis, an adhuc everti senserit eam, ne qui dolore legere fastidii. Usu ornatus dissentiunt ex. In errem dicunt pri.
            </p>
         </div>
      </div>

      <div class="row" th:fragment="looptest">
         <div class="col-xs-12">
            <h3 th:utext="${listTitle}"></h3>
            <ul th:each="item: ${items}">
               <li th:text="${item}"></li>
            </ul>
         </div>
      </div>
      
      <div class="row" th:fragment="showThis">
         <div class="col-xs-12">
            <h3>Show This</h3>
            <p>This is one paragraph.</p>
         </div>
      </div>
      
      <div class="row" th:fragment="showThat">
         <div class="col-xs-12">
            <h3>Show This</h3>
            <p>This is another paragraph.</p>
         </div>
      </div>

      <div class="container-fluid" th:fragment="footer">
         <div class="row footer">
            <div class="col-xs-12">
               <hr/>
               &copy 2020, hanbo.org.
            </div>
         </div>
      </div>
      
      <div th:fragment="stdjs">
   <script type="text/javascript" th:src="@{/jquery/js/jquery.min.js}"></script>
   <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
      </div>
   </body>
</html>

In this file, the fragments are defined as:

<div class="row" th:fragment="showThat">
...
</div>

The attribute th:fragment sets the ID of the fragment. This ID is the one referenced after the scope separator "::". When this fragment is being rendered to the actual page, the entire div element with all its children elements are rendered to the page. Then the placeholders of the entire page will be set with real value from the ModelAndView object returned by the controller method.

The next question is, how do we set value for the placeholder? In this example, there are four different ways:

  • Place text for an HTML element
  • Place unescaped text for an HTML element
  • Iterate through a collection of elements and render them as text
  • And the if-else condition blocks as we have seen in previous section.

Another worthy concept to learn is the switch-case block, it is quite easy so I won't cover here.

First, how to display a text based value from the ModelAndView to the rendered page. The text string is defined as this:

...

ModelAndView retVal = new ModelAndView();
...
retVal.addObject("siteName", "Thymeleaf Sample");

To render this text to an HTML element is done as this:

...
<a class="navbar-brand" ... th:text="${siteName}"></a>
...

Aside from displaying plain text, it is also useful to display HTML text and have it rendered. This is easily accomplished as the following. Assuming the Java code for the html string is defined as this:

...

ModelAndView retVal = new ModelAndView();
...
retVal.addObject("listTitle",
   "<span style=\"color: green;\">A Test Unordered List</span>");
...

To render the HTML string, here is how:

...
<h3 th:utext="${listTitle}"></h3>
...

In both case, I use "${variableName}" to reference the object/variable that is stored inside the ModelAndView object. These are the keys to the real values inside the "hashmap" in ModelAndView object.

To render a list of string values in a list or other iterate-able collections. The following is how this is done. Assuming the Java code for defining the list object as the following:

List<String> items = new ArrayList<String>();
items.add("Simple Item 1");
items.add("Simple Item 2");
items.add("Simple Item 3");
items.add("Simple Item 4");
items.add("Simple Item 5");

ModelAndView retVal = new ModelAndView();
...
retVal.addObject("items", items);
...

Then the rendering of these items in the list is done like this:

...
<div class="col-xs-12">
   ...
   <ul th:each="item: ${items}">
      <li th:text="${item}"></li>
   </ul>
</div>
...

This is slightly different from the display of single value text. First you must get an iterator for the collection. This is done with the attribute th:each, the iterator variable is then used like it was in a for loop, via "${item}" to get to the real value.

We have already seeing the use of the if-else statements. Let's go over this again. Assuming the Java code for the variable and value is set like this:

ModelAndView retVal = new ModelAndView();
...
retVal.addObject("conditionVal", 5);

The way if-else works is like this:

   <div th:if="${conditionVal == 5}">
      ...
   </div>
   <div th:unless="${conditionVal == 5}">
      ...
   </div>

The first part is easy to understand. If the variable "conditionVal" equals 5. The second one is a little hard to understand. It uses th:unless. And it basically said unless the variable "conditionVal" equals 5, this will show, and if it is equal to 5, then it won't shown. As I have said before, in both conditions, the value to check must be the same. Or the rendering will show something really weird.

Summary

In this tutorial, I discussed how to integrate ThymeLeaf page rendering engine with a Spring Boot powered web application. My main objective is find an alternative to the existing rendering engine I am using. And it is also an opportunity to learn something new. My impression of this new rendering engine is that:

  • It is super easy to integrate ThymeLeaf with a Spring Boot based web application.
  • It is easy to customize the configurations so that you don't need to rely on default configurations.
  • The rendering rules are pretty easy to grasp.
  • I had no trouble of using ThymeLeaf to do anything I have done before, due to its user-friendliness.

What I am unsure of, is the integration with Spring Security. Spring Security uses its own namespaces for its tags, and ThymeLeaf use its own. The ways the two imports namespaces are different. I am a bit unsure about the difference and whether it will create any issues or not. I am confident there shouldn't be any because both were well used together by many projects. I will create another tutorial for this later.

I created this tutorial in a rush. It is not the best of my work. It is done. I hope you enjoy it.

History

  • 10/11/2020 - Initial Draft

Add Comment

Comments