1. Overview

In this quick tutorial, we’re going to show how to bind a List object in Thymeleaf.

To learn how to integrate Thymeleaf with Spring, you can check out our main Spring article here – where you can also learn how to display fields, accept input, display validation errors, or convert data for display.

2. Lists in Thymeleaf Example

We’ll start by showing how to display elements of a List in a Thymeleaf page and how to bind a list of objects as user’s inputs in a Thymeleaf form.

For this purpose, we’ll use a simple model shown in the following code:

public class Book {
    private long id;

    private String title;

    private String author;
    
    // getters and setters
}

Beside displaying existing books in our example, we’re going to make it possible for the user to add multiple books to the collection and also to edit all existing books at once.

3. Displaying List Elements

Let’s take a look at the following Controller method that returns the allBooks page:

@GetMapping("/all")
public String showAll(Model model) {
    model.addAttribute("books", bookService.findAll());
    return "books/allBooks";
}

Here, we’ve added List of Book objects as a model attribute sent to the view, where we’ll display it using an HTML table:

<table>
    <thead>
        <tr>
            <th> Title </th>
            <th> Author </th>
        </tr>
    </thead>
    <tbody>
    <tr th:if="${books.empty}">
            <td colspan="2"> No Books Available </td>
        </tr>
        <tr th:each="book : ${books}">
            <td><span th:text="${book.title}"> Title </span></td>
            <td><span th:text="${book.author}"> Author </span></td>
        </tr>
    </tbody>
</table>

Here, we’re using the th:each property to iterate through the list and display properties of each object in it.

4. Binding a List Using Selection Expression

To send the list of objects from the view to the controller via form submit, we cannot use List object itself.

Instead, we have to add a wrapper object that will hold the submitted list:

public class BooksCreationDto {
    private List<Book> books;

    // default and parameterized constructor

    public void addBook(Book book) {
        this.books.add(book);
    }
    
    // getter and setter
}

Let’s now enable the user to add three books in one form submission.

First, we’ll prepare the form page, passing our command object as a Model attribute:

@GetMapping("/create")
public String showCreateForm(Model model) {
    BooksCreationDto booksForm = new BooksCreationDto();

    for (int i = 1; i <= 3; i++) {
        booksForm.addBook(new Book());
    }

    model.addAttribute("form", booksForm);
    return "books/createBooksForm";
}

As we can see, we passed a list of 3 empty Book objects to the view via the wrapper class.

Next, we need to add the form to a Thymeleaf page:

<form action="#" th:action="@{/books/save}" th:object="${form}"
  method="post">
    <fieldset>
        <input type="submit" id="submitButton" th:value="Save">
        <input type="reset" id="resetButton" name="reset" th:value="Reset"/>
        <table>
            <thead>
                <tr>
                    <th> Title</th>
                    <th> Author</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="book, itemStat : *{books}">
                    <td><input th:field="*{books[__${itemStat.index}__].title}" /></td>
                    <td><input th:field="*{books[__${itemStat.index}__].author}" /></td>
                </tr>
            </tbody>
        </table>
    </fieldset>
</form>

And this is what the page above will look like:

books2018/06/allbooks.png

Let’s have a closer look at what we did here. First, we used the th:object=”${form}” to specify the command object (the one we passed as a Model attribute).

The next thing worth noting is that we accessed the list with a selection expression using:

<tr th:each="book, itemStat : *{books}">

And finally, we’re mapping our inputs as properties of the list elements using th:field.

However, we also need to use the itemStat variable to define which list element we’re referring to, as demonstrated in:

th:field="*{books[__${itemStat.index}__].title}"

The last step is actually to manipulate the submitted data on the back-end. We’ll use the command object as the @ModelAttribute in our @PostMapping method in the controller, save the retrieved list of books and return all existing books to the user:

@PostMapping("/save")
public String saveBooks(@ModelAttribute BooksCreationDto form, Model model) {
    bookService.saveAll(form.getBooks());

    model.addAttribute("books", bookService.findAll());
    return "redirect:/books/all";
}

After submitting the form to the /save endpoint, we’ll get the page with all the newly added books:

allbooks

5. Binding a List Using Variable Expression

For this example, we’ll first load all existing books into the command object:

@GetMapping("/edit")
public String showEditForm(Model model) {
    List<Book> books = new ArrayList<>();
    bookService.findAll().iterator().forEachRemaining(books::add);

    model.addAttribute("form", new BooksCreationDto(books));
    return "books/editBooksForm";
}

The HTML page is similar, with the most noteworthy differences in the th:each block:

<tr th:each="book, itemStat : ${form.books}">
    <td>
        <input hidden th:name="|books[${itemStat.index}].id|" th:value="${book.getId()}"/>
    </td>
    <td>
        <input th:name="|books[${itemStat.index}].title|" th:value="${book.getTitle()}"/>
    </td>
    <td>
        <input th:name="|books[${itemStat.index}].author|" th:value="${book.getAuthor()}"/>
    </td>
</tr>

As shown in <tr th:each=”book, itemStat : ${form.books}”>, we accessed the list in a slightly different manner, using variable expression this time. Especially relevant is to notice that we provided name and value for input elements to properly submit data.

We also had to add hidden input which will bind the current book’s id because we don’t want to create new books but to edit existing ones.

6. Conclusion

In this article, we illustrated how to use List object in Thymeleaf and Spring MVC. We’ve shown how to display the list of objects sent to the view, but we put the primary focus on two ways of binding user inputs as a list in Thymeleaf form.

All of the code snippets, mentioned in the article, can be found in our GitHub repository.