Getting Started with Thymeleaf in Java and Spring

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.

Free eBook: Git Essentials

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.

Last Updated: September 11th, 2023
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

Dhrubajyoti BhattacharjeeAuthor

I am a software developer and a DevOps Engineer with hands-on experience in Core Java, Restful web services for mobile applications. Developing microservice applications with Spring and Spring Boot.

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms