51. Thymeleaf flow control and fragments#
51.1. Introduction#
This post is the second on Thymeleaf. It deals with these templating techniques that Thymeleaf offers:
conditionals
loops
using fragments as building blocks
For a complete overview you should refer to the official docs.
These examples all make use of the Movie
class:
public class Movie {
private String title;
private int year;
private double rating = -1;
private Set<String> mainActors = new HashSet<>();
private static List<Movie> movies;
public Movie() {
}
public Movie(String title, int year) {
this.title = title;
this.year = year;
}
public Movie(String title, int year, double rating) {
this.title = title;
this.year = year;
this.rating = rating;
}
public Movie(String title, int year, double rating, List<String> mainActors) {
this.title = title;
this.year = year;
this.rating = rating;
this.mainActors.addAll(mainActors);
}
public String getTitle() {
return title;
}
public int getYear() {
return year;
}
public double getRating() {
return rating;
}
public List<String> getMainActors() {
List<String> actors = new ArrayList<>();
actors.addAll(mainActors);
actors.sort(String::compareTo);
return actors;
}
public void setRating(double rating) {
this.rating = rating;
}
public void addActor(String actor) {
mainActors.add(actor);
}
public static List<Movie> getAllMovies() {
return Movie.movies;
}
//create a list from IMDB top 20
static {
List<Movie> movies = new ArrayList<>();
Movie movie;
movie = new Movie("The Shawshank Redemption", 1994, 9.2, List.of("Tim Robbins", "Morgan Freeman"));
movies.add(movie);
movie = new Movie("The Dark Knight", 2008, 9.0, List.of("Christian Bale,", "Heath Ledger"));
movies.add(movie);
movie = new Movie("Pulp Fiction", 1994, 8.9, List.of("John Travolta", "Uma Thurman", "Samuel L. Jackson"));
movies.add(movie);
movie = new Movie("Fight Club ", 1999, 8.8, List.of("Brad Pitt", "Edward Norton"));
movies.add(movie);
movie = new Movie("Forrest Gump", 1994, 8.7, List.of("Tom Hanks", "Robin Wright", "Gary Sinise"));
movies.add(movie);
movie = new Movie("Inception", 2010, 8.7, List.of("Leonardo DiCaprio", "Joseph Gordon-Levitt"));
movies.add(movie);
movie = new Movie("One Flew Over the Cuckoo's Nest", 1975, 8.7, List.of("Jack Nicholson", "Louise Fletcher", "Will Sampson"));
movies.add(movie);
movie = new Movie("The Usual Suspects", 1995, 8.5, List.of("Kevin Spacey", "Gabriel Byrne", "Chazz Palminteri"));
movies.add(movie);
Movie.movies = movies;
}
}
The list of movies is attached to the WebContxt object using
ctx.setVariable("movies", Movie.getAllMovies());
so for all examples the $movies
variable is available.
51.2. Iteration#
Iteration with Thymeleaf is as simple as in Java code; simply use th:each
.
This is an example with the movies list:
<table>
<thead>
<tr>
<th>Title</th>
<th>Year of release</th>
<th>IMDB rating</th>
</tr>
</thead>
<tbody>
<tr th:each="movie:${movies}">
<td th:text="${movie.title}">_title_</td>
<td th:text="${movie.year}">_year_</td>
<td th:text="${movie.rating}">_rating_</td>
</tr>
</tbody>
</table>
Alternatively, you can use the local scope syntax:
<tr th:each="movie:${movies}" th:object="${movie}">
<td th:text="*{title}">_title_</td>
<td th:text="*{year}">_year_</td>
<td th:text="*{rating}">_rating_</td>
</tr>
both of which result in this table:
Title | Year of release | IMDB rating |
---|---|---|
The Shawshank Redemption | 1994 | 9.2 |
The Dark Knight | 2008 | 9.0 |
Pulp Fiction | 1994 | 8.9 |
Fight Club | 1999 | 8.8 |
Forrest Gump | 1994 | 8.7 |
Inception | 2010 | 8.7 |
One Flew Over the Cuckoo's Nest | 1975 | 8.7 |
The Usual Suspects | 1995 | 8.5 |
You can also sort stuff in Thymeleaf. Given an extra model parameter passed from the servlet, and the utility object #lists
.
Of course, sorting is something you usually leave to your Javascript front-end (Datatables.js). But there are uses for it, and this prevents you from attaching the same list twice to the WebContext object.
//java 8+ type Comparator
ctx.setVariable("movies_year_sorter", (Comparator<Movie>) (o1, o2) -> Integer.compare(o1.getYear(), o2.getYear()));
//or, a pre-java 8 Comparator
ctx.setVariable("movies_year_sorter", new Comparator<Movie>() {
@Override
public int compare(Movie m1, Movie m2) {
return Integer.compare(m1.getYear(), m2.getYear());
}
});
then in Thymeleaf, use the Comparator object:
<tr th:each="movie:${#lists.sort(movies, movies_year_sorter)}">
Thymeleaf also provides a mechanism that stores the state of the iteration process. It has several useful properties:
index
: the current iteration index, starting with 0 (zero)count
: the number of elements processed so farsize
: the total number of elements in the listeven/odd
: boolean - true if the current iteration index is even or oddfirst
: boolean - true if the current iteration is the first onelast
: boolean - true if the current iteration is the last one
The next piece of code demonstrates the use of this iteration statistics object, as well as another technique: the if/else ternary operator for Thymeleaf. This example shows you can even nest these.
<table>
<thead>
<tr>
<th>Pos</th>
<th>Title</th>
<th>Year of release</th>
<th>IMDB rating</th>
</tr>
</thead>
<tbody>
<tr th:each="movie, it_stat:${movies}"
th:class="${it_stat.first} ? 'first' : (${it_stat.even} ? 'even' : 'odd')">
<td th:text="${it_stat.count} + ' / ' + ${it_stat.size}"></td>
<td th:text="${movie.title}">_title_</td>
<td th:text="${movie.year}">_year_</td>
<td th:text="${movie.rating}">_rating_</td>
</tr>
</tbody>
</table>
when this css is applied:
.even {
background-color: aliceblue !important;
}
.odd {
background-color: lightcyan !important;
}
.first {
background-color: coral !important;
}
this is the result:
Pos | Title | Year of release | IMDB rating |
---|---|---|---|
1 / 8 | The Shawshank Redemption | 1994 | 9.2 |
2 / 8 | The Dark Knight | 2008 | 9.0 |
3 / 8 | Pulp Fiction | 1994 | 8.9 |
4 / 8 | Fight Club | 1999 | 8.8 |
5 / 8 | Forrest Gump | 1994 | 8.7 |
6 / 8 | Inception | 2010 | 8.7 |
7 / 8 | One Flew Over the Cuckoo's Nest | 1975 | 8.7 |
8 / 8 | The Usual Suspects | 1995 | 8.5 |
There is a lot going on in this statement:
<tr th:each="movie, it_stat:${movies}"
th:class="${it_stat.first} ? 'first' : (${it_stat.even} ? 'even' : 'odd')">
.
Let’s dissect.
First there is the iteration initialization where an it_stat
object is requested as well: th:each="movie, it_stat:${movies}"
. In the second part, a nested ternary statement sets the class
attribute of the current row. The first level determines the first row: th:class="${it_stat.first} ? 'first' : (<nested ternary>)"
. The nested ternary determines the class of all rows except the first: ${it_stat.even} ? 'even' : 'odd'
.
51.3. Conditionals: if (and th:block)#
Besides the ternary operator structure shown in the previous section, regular “if” tests can be applied. Here is a test for the rating of the movie; only movies with a rating higher than 9 are displayed:
<ul>
<li th:each="movie:${movies}"
th:text="${movie.title} + ' (' + ${movie.year} + ') - ' + ${movie.rating}"
th:if="${movie.rating >= 9}">_movie_</li>
</ul>
The specified expression evaluates to a boolean following these rules:
test evaluates to true
if value is not null
and value is
a boolean and is true
a number and is non-zero
a character and is non-zero
a String and is not “false”, “off” or “no”
not a boolean, a number, a character or a String
The Thymeleaf th:if
attribute has an inverse attribute, th:unless
.
This makes an if/else-like structure possible. Here, you see it in combination with th:block
which is a handy Thymeleaf element which does not generate a html DOM element. Using th:block prevents excessive use of nested div and span elements.
<ul>
<th:block th:each="movie : ${movies}">
<li th:if="${movie.rating >= 9}" th:text="${movie.title} + ' - ' + ${movie.rating} + ' - WOW!'" ></li>
<li th:unless="${movie.rating >= 9}" th:text="${movie.title} + ' - ' + ${movie.rating} + ' - JUST OK'" ></li>
</th:block>
</ul>
51.4. Conditionals: switch#
Here you see a combination of iteration and switch/case to build an ordered list.
<ol>
<li th:each="user:${users}" th:switch="${user.role.toString()}">
<span th:case="${'ADMIN'}"
th:text="${user.name} + ' (' + ${user.role} + ') manages all accounts'">_admin_</span>
<span th:case="${'USER'}"
th:text="${user.name} + ' (' + ${user.role} + ') can browse and share all site content'">_user_</span>
<span th:case="${'GUEST'}"
th:text="${user.name} + ' (' + ${user.role} + ') can only see our front page'">_guest_</span>
<span th:case="*"
th:text="${user.name} + '(' + ${user.role} + ') we do not know this role'">_unknown_</span>
</li>
</ol>
The th:switch
expression selects the User role (an enum) in each loop iteration, takes its String representation and compares this to a string literal in each th:case
. Only the th:case
that matches will be evaluated. When no match is found, the default is evaluated, specified with th:case="*"
. The result when applied to this list of User instances:
List<User> users = new ArrayList<>();
User u;
u = new User("Hank", "henk@example.com", Role.ADMIN);
users.add(u);
u = new User("Roger", "roger@example.com", Role.USER);
users.add(u);
u = new User("Diana", "diana@example.com", Role.GUEST);
users.add(u);
is this:
<ol>
<li>
<span>Hank (ADMIN) manages all accounts</span>
</li>
<li>
<span>Roger (USER) can browse and share all site content</span>
</li>
<li>
<span>Diana (GUEST) can only see our front page</span>
</li>
</ol>
Note that with just one th:case
, you can put everything within a single li
tag - no need for the span
.
51.5. Working with fragments#
Parts of your pages will be reused over multiple other pages. Specifically, this is the case for banners, footers, menus and headers. Instead of copy-and-paste, Thymeleaf offers a nice mechanism for reusing page fragments - even conditionally! The strategy of choice here is to create a single Thymeleaf html page called “template” or “fragments” and to include elements of this page in others.
Consider this html file called template.html
:
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head th:fragment="headerFragment">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>_TITLE_</title>
<link rel="stylesheet" th:href="@{/css/main.css}" href="../../css/main.css">
</head>
<body>
<div th:fragment="banner">
<img id="logo" th:src="@{/graphics/Awesome.png}" src="../../graphics/Awesome.png" alt="banner pic"/>
<h1 id="banner_title">Welcome, ye of lesser quality</h1>
</div>
<!--these can be conditionally selected-->
<div th:fragment="menu" class="admin">
<a href="#">my secret content</a>
</div>
<div th:fragment="menu" class="normaluser">
<a href="#">my regular content</a>
</div>
<div>
<h2>Template layout</h2>
<p>
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
ex ea commodo consequat."
</p>
</div>
<!-- receives an argument called pub -->
<div th:fragment="footer(pub)">
© 2018 Team Awesome. Please don't contact us; we are too busy!<br/>
But if you really need to communicate, you can find us at the local pub:
<span th:text="${pub}">_PUB_</span><br />
</div>
</body>
</html>
The different parts (divs) have been marked with th:fragment="<fragment_name>"
. These th:fragment
tags make them available for inclusion in other pages.
This is how you include a fragment from this page into another:
<div th:insert="~{template :: banner}"></div>
<!-- shorthand -->
<div th:insert="template :: banner"></div>
The th:insert
expression expects a fragment expression (~{...}
), which is an expression that results in a fragment. The first part of this expression is the name of the html file (template.html
) without the extension and the second part is the fragment to be included. With a non-complex fragment expression, the (~{
,}
) enclosing is optional, so the shorthand notation suffices.
Here is another example, the footer. This fragment takes an argument called pub
that is available as variable by that same name. This is how to pass a variable
<div th:insert="~{template :: footer('The Bear Inn')}"></div>
and using it with the fragment is standard variable expression syntax: ${pub}
.
Finally, different variants of a fragment can be conditionally included - here demonstrated with a ternary statement:
<div th:insert="~{template :: (${users[0].getRole().toString() == 'ADMIN'} ? 'menu.admin' : 'menu.normaluser')}"></div>
If the Role is ADMIN, <div th:fragment="menu" class="admin">...
is selected, otherwise <div th:fragment="menu" class="normaluser">...
.
51.5.1. The difference between th:insert
and th:replace
#
th:insert is the simplest: it will simply insert the specified fragment as the body of its host tag.
The th:replace
expression actually replaces its host tag with the specified fragment.
So, this:
<div th:insert="~{template :: (${users[0].getRole().toString() == 'ADMIN'} ? 'menu.admin' : 'menu.normaluser')}"></div>
results in
<div>
<div class="admin">
<a href="#">my secret content</a>
</div>
</div>
whereas
<div th:replace="~{template :: (${users[0].getRole().toString() == 'ADMIN'} ? 'menu.admin' : 'menu.normaluser')}"></div>
results in
<div class="admin">
<a href="#">my secret content</a>
</div>