Introduction
When developing web applications, an important choice is which engine will be taking care of the view layer.
Java Server Pages (JSPs) used to be very popular, though the overhead and time-consumption were some major drawbacks to using them. They required a fair bit of change to the HTML of the pages.
Nowadays, Thymeleaf is widely adopted and used as the templating engine for Spring/MVC applications. It can also be used for rich HTML email templating. While JSPs are compiled to Java servlet classes, Thymeleaf parses the plain HTML template files. Based on the expressions present in the file, it generates static content. It's capable of processing HTML, XML, JS, CSS, etc.
Thymeleaf Standard Dialects
Thymeleaf provides a wide range of attribute processors out of the box as a part of its Standard Dialects. These processors are enough for most typical template processing. Though, you could also extend them to make custom attribute processors if need be.
Let's take a look at the most important segment of the dialect - the Standard Expression Features. These are some of the expressions you'll be using fairly regularly:
- Variable Expressions:
${...}
- Selection Variable Expressions:
*{...}
- Message Expressions:
#{...}
- Link URL Expressions:
@{...}
- Fragment Expressions:
~{...}
Here are some literals you'll likely be using:
- Text literals:
'hello world'
,'Welcome to stackabuse'
,… - Number literals:
0
,123
,67.90
, … - Boolean literals:
true
,false
- Null literal:
null
Basic Operations:
-
String concatenation:
+
-
Literal substitutions:
|Welcome to ${city}|
-
Binary operators:
+
,-
,*
,/
, `% -
Binary operators:
and
,or
-
Boolean negation (unary operator):
!
,not
Comparisons:
- Comparators:
>
,<
,>=
,<=
(gt
,lt
,ge
,le
) - Equality operators:
==
,!=
(eq
,ne
)
Conditionals:
- If-then:
(if) ? (then)
- If-then-else:
(if) ? (then) : (else)
- Default:
(value) ?: (defaultvalue)
All of these expressions can be used in combination with one another to get the desired results.
Thymeleaf Dependency
The easiest way to get started with Thymeleaf via Maven is to include the dependency:
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>${version}</version>
</dependency>
Or, if you're using Gradle:
compile group: 'org.thymeleaf', name: 'thymeleaf', version: '${version}'
Template Engine and Template Resolvers
For Thymeleaf, the Template Resolver is responsible for loading the templates from a given location, while the Template Engine is responsible for processing it for a given context. We'll need to set up both in a configuration class:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public ClassLoaderTemplateResolver templateResolver() {
ClassLoaderTemplateResolver templateResolver =
new ClassLoaderTemplateResolver();
templateResolver.setPrefix("/templates/");
templateResolver.setSuffix(".html");
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
return templateEngine;
}
}
Here, we've instantiated a templateResolver
and set its prefix and suffix. The views will be located in the /templates
directory and will end with .html
.
After that, we've set up the templateEngine
, simply by setting the resolver and returning it.
Let's test out if it's working by trying to process a message:
StringWriter writer = new StringWriter();
Context context = new Context();
TemplateEngine templateEngine = templateEngine();
context.setVariable("message", "Welcome to thymeleaf article");
templateEngine.process("myTemplate", context, writer);
LOG.info(writer.toString());
The engine is used to process the myTemplate.html
file, located in the src/main/resources/templates
directory. The /resources
directory is the default one. A variable is passed into the context
, which allows us to reference it in the template itself:
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-3.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<body>
<h1 th:text="${message}"></h1>
</body>
</html>
The th:text
attribute will evaluate this message
's value and insert it into the body of the tag its located in. In our case, the body of the <h1>
tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<h1>Welcome to thymeleaf article</h1>
</body>
</html>
Works fine! Let's go ahead and configure a ViewResolver
so that we can populate views through controllers, rather than hard coding values into the context.
View Resolver
Right below the other configuration, let's set up the ViewResolver
. It maps the view names to the actual views. This allows us to simply reference views in controllers, rather than hard coding values:
@Bean
public ViewResolver viewResolver() {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
viewResolver.setCharacterEncoding("UTF-8");
return viewResolver;
}
Displaying Model Attributes
The most basic usage of most engines like Thymeleaf is displaying certain properties/attributes of models. Let's create a request handler that returns an object with a couple of fields set:
@GetMapping("/article")
public ModelAndView getArticle(ModelAndView modelAndView) {
Article article = new Article();
article.setAuthor(getName());
article.setContent(getArticleContent());
article.setTitle(getTitle());
modelAndView.addObject("article", article);
modelAndView.setViewName("articleView");
return modelAndView;
}
The handler is sending back the view, named articleView
and an object called article
. These two are now interconnected. We can access the article
on the articleView
page. This is similar to how we've injected the message
into the Context
object last time.
Let's take a look at how we can access an object and display its values on a page:
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-3.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<link th:href="@{/css/app.css}" rel="stylesheet"/>
<body class='typora-export os-windows'>
<div id='write' class='is-node'>
<h1 th:text="${article.title}">Article title</h1>
<h4 th:text="${article.author}">Author name</h4>
<p th:text="${article.content}">content</p></div>
</body>
</html>
Using the variable expression, ${...}
, we reference the article
object, and inject the fields into th:text
attributes accordingly. This is how the rendered page would look like:
Note: If a tag has a body, the th:text
will override it. If the value isn't present or if there are issues with displaying it, the body will be used instead.
Local Variables
Local variables in Thymeleaf come in quite handy. Local variables are defined within a specific fragment of a template. They're available only in the scope of the defining fragment.
With local variables, we avoid the need to do everything in the controller and perform operations on the page itself. Let's take a look:
<tr th:each="article : ${articles}">
<td th:text="${article.name}">name</td>
<td th:text="${article.author}">author</td>
<td th:text="${article.description">description</td>
</tr>
Here, the article
variable is a local variable. It represents an article
object from the articles
list. We can't reference the article
variable outside of the HTML table.
The article
variable wasn't passed down by the controller - it was defined on the page itself. The th:each
attribute will assign new values to the article
object on each pass of the list.
This would look something like:
Another way to define local variables is via the th:with
attribute:
<div th:with="article=${articles[0]}">
<p>
This article is written by <span th:text="${article.author}">John Doe</span>.
</p>
</div>
Here, we've defined a variable via the th:with
as the first element of the list passed down by the controller. We can reference this variable from within the <div>
tag it's defined in.
Similarly, we can define multiple variables with a single th:with
attribute:
<div th:with="article=${articles[0]}, category=${categories[1]}">
<p>
This article is written by <span th:text="${article.author}">John Doe</span>.
</p>
<p>
Category <span th:text="${category.name}">John Doe</span>.
</p>
</div>
We can also use these local variables to perform data manipulation or retrieval to reduce controller invocations:
<div th:with="article=${articles[0]}, author=${authors[article.author]}">
</div>
Note that we use the article
variable to get the author
details from the author's map. This allows us to reuse the variable within the same attribute.
Also, we now no longer need to depend on the controller to share the author details for each article, but can just pass on the list of authors besides the list of articles:
@GetMapping("/articles")
public ModelAndView getArticles(ModelAndView modelAndView) {
modelAndView.addObject("articles", getArticles());
modelAndView.addObject("authors", getAuthors());
modelAndView.setViewName("articles");
return modelAndView;
}
You don't have to set local variables bound to objects. You can just as easily use String literals or numerals:
<div th:with="name = 'John', age = 25}">
<p> Hello, <span th:text="${name}"></span>!</p>
</div>
Selection Variable Expressions
What's worth noting here are Selection Variable Expressions. Let's take a look at how they work:
<div th:object="${article}">
<td th:text="*{name}">name</td>
<td th:text="*{author}">author</td>
<td th:text="*{description">description</td>
</tr>
Instead of writing ${article.name}
, ${article.author}
, etc., we can just put a *{...}
expression. The th:object
attribute defines which object the referenced fields belong to.
Creating Forms and Inputs
Dealing with forms is frequent and is one of the most fundamental ways a user can send information to our backend. Thymeleaf provides various attributes to create and handle form submissions.
The th:action
attribute replaces the HTML action
attribute of a <form>
. The th:object
attribute is used to bind the fields of the form to an object. This is similar to the modelAttribute
or commandName
you'd typically use with JSPs.
Let's take a look at the definition of a form:
<form th:action="@{/article}" th:object="${article}" method="post">
</form>
Here, via a link expression, the form fires a POST request to the /article
URL. The bound object is an article
. Now, we'll need to put in a few input fields for us to actually populate the article
's info:
<form th:action="@{/article}" th:object="${article}" method="post">
<div class='is-node custom-form'>
<label>Title:</label>
<input type="text" th:field="*{title}"/>
</div>
<div class='is-node custom-form'>
<label>Content:</label>
<textarea th:field="*{content}"/>
</div>
</form>
We've bound an article
to this form, so the referenced title
and content
belong to it.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
Now, once the user inputs content into these fields, we'll want to process it and save it in the database. Let's make a /form
handler that'll render the form on the page first:
@GetMapping("/form")
public ModelAndView getArticleForm(ModelAndView modelAndView) {
Article article = new Article();
modelAndView.addObject("article", article);
modelAndView.setViewName("articleForm");
return modelAndView;
}
We have to add a blank article
object to the form, otherwise the th:object
attribute would be invalid. Now, let's make a POST request handler that the form hits:
@PostMapping("/article")
public String saveArticle(@ModelAttribute Article article) {
articleService.saveArticle(article);
return "articles";
}
Here, the @ModelAttribute
annotation binds the received model to the object proceeding it. It's all packed into the article
object which is then saved via a classic service that extends the CrudRepository
.
Though, a rudimentary form like this is oftentimes not enough. Let's take a look at how we can add radio buttons, checkboxes, dropdown menus, etc.
Radio Buttons
To add a radio button, we'd make a classic <input>
tag and define its type via HTML. What Thymeleaf is tasked with is binding the field and value of that radio button to the th:object
of the form:
<form th:action="@{/article}" th:object="${article}" method="post">
<div>
<label>Select a Category:</label>
<div th:each="category : ${categories}">
<input type="radio" th:field="*{category}" th:value="${category}" />
<label th:for="${#ids.prev('category')}" th:text="${category}"></label>
</div>
</div>
</form>
Once rendered, this would look something like:
Checkboxes
Checkboxes work in the exact same way:
<form th:action="@{/article}" th:object="${article}" method="post">
<div class='is-node custom-form'>
<label>Select Areas:</label>
<div th:each="area : ${areas}">
<input type="checkbox" th:field="*{area}" th:value="${area}"/>
<label th:for="${#ids.prev('area')}" th:text="${area}"></label>
</div>
</div>
</form>
This would look like:
Option Menus
And finally, let's take a look at how we can put in some options:
<form th:action="@{/article}" th:object="${article}" method="post">
<div class='is-node custom-form'>
<label>Select a Technology:</label>
<select th:field="*{technology}">
<option th:each="technology : ${technologies}" th:value="${technology}"
th:text="${technology}">
</option>
</select>
</div>
</form>
Typically, options are represented from a list. In this case, we've created an <option>
tag for each technology
in a list, and assigned the technology
value for the user to see.
This would look something like:
Conditional Statements
Websites aren't static. Depending on certain evaluations, elements are either displayed, hidden, replaced or customized. For example, we might choose to display a message instead of a table if there aren't any rows in the database.
Let's take a look at some basic conditional statements in Thymeleaf:
<body>
<table th:if="${not #list.isEmpty(articles)}">
<tr>
<th>Name</th>
<th>Author</th>
<th>Description</th>
<th>Category</th>
<th>Date</th>
</tr>
<tr th:each="article : ${articles}">
<td th:text="${article.name}">name</td>
<td th:text="${article.author}">author</td>
<td th:text="${article.description">description</td>
<td th:text="${article.category}">category</td>
<td th:text="${article.date}">date</td>
</tr>
</table>
<div th:if="${#lists.isEmpty(kv)}">
<h2>No data found</h2>
</div>
</body>
th:if
is used as a regular if
statement. If the articles
list is not empty, we populate a table - if it is empty, we display a message. Here, the #list
is a utility object used to perform convenience methods on collections.
Additionally, we can also have a th:switch
and th:case
statements. They're pretty straightforward:
<div>
<td th:switch="${article.category}">
<span th:case="'TECHNOLOGY'" th:text="Technical Articles"/>
<span th:case="'FASHION'" th:text="About latest fashion trends"/>
<span th:case="'FOOD'" th:text="Are you hungry..."/>
</td>
</div>
Only the matching case is displayed.
Externalizing Text for Internationalization
Out of the box, Thymeleaf comes with internationalization support. Create a myTemplate.properties
file in the same directory as of your templates.
Let's make a message and assign it a value:
welcome.message=Welcome to Stack Abuse
Now, in any template, we can reference the value by calling for the welcome.message
with a Message Expression:
<body>
<h1 th:text="#{welcome.message}"></h1>
</body>
For using different locales, create more files like myTemplate_de.properties
. While creating the context for the template, in the original setup, just pass the locale to it:
Context context = new Context(Locale.GERMAN);
Fragments and Layouts
Some things on a page don't change much throughout the entire front-end. Namely, the header and footer are typically the exact same. Also, once these are changed/updated, you have to go to each and every page and update the code there as well.
This boilerplate code can be reused and simply referenced on each page. Thymeleaf offers us fragments, which are individual files that you can insert into another file. Let's create a header fragment and include it in another template:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="header_fragment">
<h1>Welcome to Stack Abuse</h1>
</div>
</body>
</html>
We'll save this file, called header.html
in the same directory as other templates. Though, many save them in a subdirectory, called fragments
.
Now, we'll want to include this header in another page. Note that this won't include the entire file. Just the <div>
we marked as a th:fragment
. Let's put this header above our welcome message:
<body>
<div id="holder" th:insert="header :: header_fragment"></div>
<h1 th:text="#{welcome.message}"></h1>
</body>
When we render this file, the HTML page will look like this:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<div id="holder">
<div>
<h1>Welcome to Stack Abuse Article</h1>
</div>
</div
<h1>Welcome to world</h1>
</body>
</html>
Now, there are three ways to include fragments: th:insert
, th:replace
, and th:include
.
th:insert
adds the fragment as a child node inside the enclosing tag. As we can see in the example above, the header fragment is inserted into the <div>
with the holder
id.
th:replace
will replace the current tag with the fragment:
<body>
<div id="holder" th:replace="header :: header_fragment"></div>
<h1 th:text="#{welcome.message}"></h1>
</body>
This would render as:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<div>
<h1>Welcome to Stack Abuse Article</h1>
</div>
<h1>Welcome to world</h1>
</body>
</html>
The <div>
with the holder
id is now replaced with the fragment.
th:include
is a predecessor to the th:replace
tag and works in the same way. Now, it's deprecated.
Handling Errors and Error Messages
Handling errors is a very important aspect of web applications. When something is wrong, we want to guide the user to fix user-made issues - like incorrect form submissions.
For simplicity's sake, we'll use javax.validations
to check the fields of a form submission:
@PostMapping("/article")
public String saveArticle(@ModelAttribute @Valid Article article, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "articleForm";
}
articleService.saveArticle(article);
return "redirect:articles";
}
This is a classic form submission handler. We've packed the information into an article
object and saved it in a database. However, this time around, we've marked the article
as @Valid
, and added a check for the BindingResult
instance.
The @Valid
annotation makes sure that the object information received and packed conforms to the validations we've set in the Article
model:
public class Article {
@NotNull
@Size(min = 2, max = 30)
private String title;
private String author;
@NotNull
@Size(min = 2, max = 1000)
private String content;
private String category;
private String technology;
private String area;
}
If there are any violations of these rules, bindingResults.hasErrors()
will return true
. And thus we return the form back. instead of redirecting the user to the /articles
page.
The errors will be displayed in the form, at the designated places we've set with th:errors
:
<form th:action="@{/article}" th:object="${article}" method="post">
<div class='is-node custom-form'>
<label>Title:</label>
<input type="text" th:field="*{title}"/>
<span class="field-error" th:if="${#fields.hasErrors('title')}" th:errors="*{title}">Name Error</span>
</div>
<div class='is-node custom-form'>
<label>Content:</label>
<textarea th:field="*{content}"/>
<span class="field-error" th:if="${#fields.hasErrors('content')}" th:errors="*{content}">Name Error</span>
</div>
</form>
Using a couple of conditionals and the convenience #fields.hasErrors()
methods, we can let the user know what's wrong with the validations and politely ask for a revision of the submitted information.
This is how the rendered page would look like:
Alternatively, we may also group all errors together using a wild card or all
:
<li class="field-error" th:each="error : ${#fields.errors('*')}" th:text="${error}" />
<li class="field-error" th:each="error : ${#fields.errors('all')}" th:text="${error}" />
Conclusion
This article is meant as a gateway to Thymeleaf, a very popular, modern templating engine for Java/Spring applications.
While we haven't done a deep-dive into the engine, which is fairly extensive, the covered material should be more than enough to get you started with a good foundation for more advanced features.