Thymeleaf iteration and fragments

Posted at — Sep 15, 2021
Taming Thymeleaf cover
Interested in learning more about Thymeleaf? Check out my book Taming Thymeleaf. The book combines all of my Thymeleaf knowledge into an easy to follow step-by-step guide.

Iteration in Thymeleaf templates is done using th:each. But combining it with fragments can yield some surprising results. This blog post shows an overview of what can happen and common pitfalls to watch out for.

A basic iteration in Thymeleaf looks like this:

src/main/resources/templates/index.html
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      lang="en">
<main>
    <ul>
        <li th:each="user : ${users}">
            <div th:text="${user.name}"></div>
            <div th:text="${user.country}"></div>
        </li>
    </ul>
</main>
</html>

If our controller looks something like this:

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@Controller
@RequestMapping
public class IndexController {

    @GetMapping
    public String index(Model model) {
        model.addAttribute("users", getUserList());
        return "index";
    }

    private List<User> getUserList() {
        return List.of(
                new User("Wim Deblauwe", "Belgium"),
                new User("Philip Riecks", "Germany")
        );
    }

    public static record User(String name, String country){}
}

Then the generated HTML will be this:

<html xmlns="http://www.w3.org/1999/xhtml"
      lang="en">
<div>
    <ul>
        <li>
            <div>Wim Deblauwe</div>
            <div>Belgium</div>
        </li>
        <li>
            <div>Philip Riecks</div>
            <div>Germany</div>
        </li>
    </ul>
</div>
</html>

Using th:insert

We can create a fragment to encapsulate how we display the user information:

src/main/resources/templates/fragments.html
<div th:fragment="userInfo(user)"> (1)
    <!--/*@thymesVar id="user" type="com.wimdeblauwe.examples.thymeleafiterationfragments.IndexController.User"*/--> (2)
    <div th:text="${user.name}"></div> (3)
    <div th:text="${user.country}"></div> (4)
</div>
1 th:fragment indictes the name of the fragment and the parameter for the fragment
2 This comment signals to IntelliJ what the expected class of the user variable is. This is needed to have code completion for the properties of the user.
3 Show the name property of the user.
4 Show the country property of the user.

We can now use the fragment in our index.html like this:

<ul>
    <li th:each="user : ${users}" th:insert="fragments :: userInfo(${user})">
    </li>
</ul>

You can also do this:

<ul>
    <li th:each="user : ${users}" th:insert="fragments :: userInfo(user)">
    </li>
</ul>

Where you use user instead of ${user} on the calling side of the fragment. But be aware that this does not always work the way you might think it does, so it is better to always use ${user}.

The resulting HTML looks like this:

<ul>
    <li> (1)
        <div> (2)
            <div>Wim Deblauwe</div>
            <div>Belgium</div>
        </div>
    </li>
    <li> (1)
        <div> (2)
            <div>Philip Riecks</div>
            <div>Germany</div>
        </div>
    </li>
</ul>
1 These <li/> elements are generated by the th:each
2 The fragment has a <div> as top-level element, so it is inserted below the <li> as we are using th:insert.

Using th:replace

The difference betwen th:insert and th:replace is that th:insert will insert the fragment as a "child" element of the element that the attribute is declared on. With th:replace, it replaces the element that it is declared on.

You might think that to get the exact HTML output we had in the first example, we can use th:replace as follows:

<!-- Attention: does not work! -->
<ul>
    <li th:each="user : ${users}" th:replace="fragments :: userInfoLi(${user})">
    </li>
</ul>

Where userInfoLi fragment is defined as:

<li th:fragment="userInfoLi(user)"> (1)
    <div th:text="${user.name}"></div>
    <div th:text="${user.country}"></div>
</li>
1 The root element of the fragment is <li>

However, this does not work! The logging shows this exception:

Property or field 'name' cannot be found on object of type 'java.lang.String'

The reason for this is Attribute Precedence in Thymeleaf.

Thymeleaf processes the th:insert and th:replace attributes before the th:each.

So in our example, due to the th:replace, Thymeleaf generates this:

<ul>
    <li>
        <div th:text="${user.name}"></div>
        <div th:text="${user.country}"></div>
    </li>
</ul>

Because the <li> in index.html is replaced with the <li> from the fragment, the th:each is no longer there. As a consequence, the user variable is unknown and Thymeleaf gives an error processing the template.

Using th:insert with th:remove

One solution to have the clean HTML output we had without using fragments is combining th:insert with th:remove.

Because th:remove has the lowest precedence, we can have Thymeleaf process the iteration and the insertion of the template, and at the end remove the extra tag that is the root element of the fragment (th:remove="tag" removes the top-level tag, but keeps the children of that tag).

src/main/resource/templates/index.html
<ul>
    <li th:each="user : ${users}" th:insert="fragments :: userInfoLi(${user})" th:remove="tag">
    </li>
</ul>

This results in the following HTML:

<ul>
    <li>
        <div>Wim Deblauwe</div>
        <div>Belgium</div>
    </li>
    <li>
        <div>Philip Riecks</div>
        <div>Germany</div>
    </li>
</ul>

The processing step-by-step happens like this:

Step 1

Process the th:insert:

<ul>
    <li th:each="user : ${users}" th:remove="tag">
    <li>
        <div th:text="${user.name}"></div>
        <div th:text="${user.country}"></div>
    </li>
    </li>
</ul>

Step 2

Process the th:each:

<ul>
    <li th:remove="tag">
    <li>
        <div>Wim Deblauwe</div>
        <div>Belgium</div>
    </li>
    </li>
    <li th:remove="tag">
    <li>
        <div>Philip Riecks</div>
        <div>Germany</div>
    </li>
    </li>
</ul>

Step 3

Process the th:remove:

<ul>
    <li>
        <div>Wim Deblauwe</div>
        <div>Belgium</div>
    </li>
    <li>
        <div>Philip Riecks</div>
        <div>Germany</div>
    </li>
</ul>

Using th:block

Alternatively, you can also use th:block, which is a HTML element (See docs for more info).

You can combine th:block with th:insert or th:replace. We’ll use th:replace for this example:

<ul>
    <th:block th:each="user : ${users}">
        <li th:replace="fragments :: userInfoLi(${user})">
        </li>
    </th:block>
</ul>

This results in the following HTML:

<ul>
    <li>
        <div>Wim Deblauwe</div>
        <div>Belgium</div>
    </li>
    <li>
        <div>Philip Riecks</div>
        <div>Germany</div>
    </li>
</ul>

Conclusion

There are a few things you need to take into account when combining iteration via th:each with using fragments via th:insert or th:replace. I hope this post has been a good overview of the different options you have at your disposal when using Thymeleaf.

To see the full code of this example, see GitHub.

If you want to be notified in the future about new articles, as well as other interesting things I'm working on, join my mailing list!
I send emails quite infrequently, and will never share your email address with anyone else.