Thymeleaf is a popular server-side template engine for Java-based web applications. In this article, you'll learn how to create HTML forms in Thymeleaf and handle the form submission on the backend using Spring Boot. If you need more information on working with Thymeleaf in Spring Boot, read this guide.

Thymeleaf fully supports HTML5, so you can easily create a complex HTML form and handle the submission through the Spring Boot controller. For this article, I'll cover all the core HTML form elements, such as inputs, text areas, checkboxes, radio buttons, and dropdowns.

Dependencies

To use Thymeleaf with Spring Boot, you only need to include spring-boot-starter-web and spring-boot-starter-thymeleaf starter dependencies. For Gradle, add the following dependencies to your build.gradle file:

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'

For Maven, include the below dependencies to the pom.xml file:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

If you are starting from scratch, just use Spring Initializr web tool or Spring Boot CLI to bootstrap a new Spring Boot project with the above dependencies.

Java Model Class

Let us first create a simple Java model class named Project.java that stores information about web projects:

Project.java

public class Project implements Serializable {

    private Long id;
    private String title;
    private String type;
    private String color;
    private String description;
    private Integer days;
    private Double price;
    private Boolean featured;
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)        
    private LocalDateTime launchDate;

    // constructor, getters, and setters removed for brevity
}

Spring Boot Controller

In a Spring Boot web application, all HTTP requests are handled by a controller class. Such a class is annotated with the @Controller annotation to indicate that it is a web controller.

Let us create a Spring Boot controller class called ProjectController.java that defines two HTTP end-points to handle different GET and POST requests:

ProjectController.java

@Controller
public class ProjectController {

    @GetMapping("/create-project")
    public String createProjectForm(Model model) {
        
        model.addAttribute("project", new Project());
        return "create-project";
    }

    @PostMapping("/save-project")
    public String saveProjectSubmission(@ModelAttribute Project project) {

        // TODO: save project in DB here

        return "result";
    }
}

As you can see above, our controller class contains two methods. The createProjectForm() function is bound to handle GET requests at the /create-project HTTP end-point and simply returns the name of the Thymeleaf view (in this case, create-project) to render an HTML form. It uses a Model object to expose a new Project object to the view template.

The second method saveProjectSubmission() handles a POST request on the /save-project end-point once the HTML form is submitted. It receives a Project object that was populated by the form. Since the Project object is bind using the @ModelAttribute annotation, you can access it in the view template and display the result.

In a real-world scenario, you might want to persist the Project object in a database like MySQL using Spring Data JPA.

The @GetMapping and @PostMapping annotations map HTTP requests to specific controller methods.

Thymeleaf Templates

Thymeleaf templates are HTML static files that work in web applications and browsers. By default, these templates are stored in src/main/resources/templates/ directory.

Creating an HTML Form

Thymeleaf provides several special attributes to work with HTML forms:

  • th:field — Used for binding HTML form elements with a property in the form-backing bean.
  • th:object — Used for specifying a model attribute that acts as a command object.
  • th:errors — An attribute that holds all form validation errors.
  • th:errorclass — Used for setting a CSS class to a form input if that field has validation errors.

Let us now create a Thymeleaf template named create-project.html inside the src/main/resources/templates/ folder.

Command Object

The command object is the main form-backing bean attached to the form containing all properties related to input fields. Generally, it is a Java model class with declared getter and setter methods.

The following example demonstrates how you can use the th:object attribute to specify the command object in your <form> tag:

<form th:action="@{/save-project}" th:object="${project}" method="post">
    ...
</form>

There are two crtical things to remember while defining the command object:

  1. The value of the th:object attribute must be a variable expression (${...}) specifying only the name of a model attribute without property navigation. This means that an expression like ${project} is valid, but ${project.title} would throw an error.
  2. Inside an HTML <form> tag, you can not define another th:object attribute. This is because the HTML forms can not be nested.

Input & Dropdown Fields

You can easily bind an input field to a property in the bean class using the th:field attribute. This attribute renders differently depending on whether it is attached to an <input>, <select>, or <textarea> tag.

The th:field also considers the type of the <input> tag. Thymeleaf supports all modern HTML5 input types like type="email", type="number", type="color", type="date", etc.

Let us add some input fields to the above <form> tag:

<form th:action="@{/save-project}" th:object="${project}" method="post">
    <div class="form-row">
        <div class="col-md-6 form-group">
            <label>Project ID</label>
            <input type="number" class="form-control" th:field="*{id}">
        </div>
        <div class="col-md-6 form-group">
            <label>Title</label>
            <input type="text" class="form-control" th:field="*{title}">
        </div>
        <div class="col-md-6 form-group">
            <label>Type</label>
            <select class="form-control" th:field="*{type}">
                <option value="">Choose...</option>
                <option value="Design">Design</option>
                <option value="Engineering">Engineering</option>
                <option value="Marketing">Marketing</option>
            </select>
        </div>
        <div class="col-md-6 form-group">
            <label>Color</label>
            <input type="color" class="form-control" th:field="*{color}">
        </div>
        <div class="col-md-6 form-group">
            <label>Estimated Price</label>
            <input type="number" step="any" class="form-control" th:field="*{price}">
        </div>
        <div class="col-md-6 form-group">
            <label>Launch Date</label>
            <input type="datetime-local" class="form-control" th:field="*{launchDate}">
        </div>
    </div>
    <div class="form-group">
        <label>Description</label>
        <textarea class="form-control" th:field="*{description}" rows="3"></textarea>
    </div>
</form>

CheckBox & Radio Button Fields

The th:field attribute also allows you to define checkbox and radio button input fields. Let us add a checkbox and radio buttons to the above HTML form:

<div class="col-md-6 form-group">
    <strong>Duration:</strong>
    <div class="form-check form-check-inline">
        <input class="form-check-input" type="radio" th:field="*{days}" id="option1" value="15">
        <label class="form-check-label" for="option1">15 Days</label>
    </div>
    <div class="form-check form-check-inline">
        <input class="form-check-input" type="radio" th:field="*{days}" id="option2" value="30">
        <label class="form-check-label" for="option2">30 Days</label>
    </div>
    <div class="form-check form-check-inline">
        <input class="form-check-input" type="radio" th:field="*{days}" id="option3" value="45">
        <label class="form-check-label" for="option3">45 Days</label>
    </div>
</div>
<div class="col-md-6 form-group">
    <div class="form-check">
        <input class="form-check-input" type="checkbox" th:field="*{featured}" id="featured">
        <label class="form-check-label" for="featured">
            Mark Featured
        </label>
    </div>
</div>

Showing Validation and Error Messages

Thymeleaf also allows us to show validation error messages to the user on the form and field level.

Field Errors

To check if a specific field contains an error, you can use the #fields.hasErrors() method that takes the field expression as a parameter and returns a boolean value indicating whether a validation error exists for that field or not.

Here is an example that appends the error-field class when the title field contains an error:

<input type="text" class="form-control" th:field="*{title}"
                               th:classappend="${#fields.hasErrors('title')} ? 'error-field'">

Alternatively, you can also use a special attribute th:errorclass that appends the specified CSS class to the HTML element if the field contains any error:

<input type="text" class="form-control" th:field="*{title}" th:errorclass="error-field">

You can also obtain and list all the errors associated with a specific field:

<ul>
  <li th:each="error : ${#fields.errors('title')}" th:text="${error}" />
</ul>

Another way to list all errors for a specific field is by using the th:errors attribute. It builds a list with all the errors for the specified field, separated by <br/>:

<p th:if="${#fields.hasErrors('launchDate')}" th:errors="*{launchDate}">Invalid date</p>

All Errors

You can also display all errors that occurred in the form by passing * or all as an argument to the #fields.hasErrors() method to obtain all the errors and iterate them as shown below:

<ul th:if="${#fields.hasErrors('*')}">
  <li th:each="error : ${#fields.errors('*')}" th:text="${error}">error</li>
</ul>

Global Errors

Global errors are not associated with any specific fields in the form. Usually, these errors are added programmatically from the backend side of the application.

To access global errors, you need to pass global as a parameter to the #field.hasErrors() method:

<ul th:if="${#fields.hasErrors('global')}">
  <li th:each="error : ${#fields.errors('global')}" th:text="${error}">error</li>
</ul>

Displaying Results

Finally, create a new Thymeleaf template named result.html to display the results once the form is submitted by the user:

<h2>Result</h2>
<div class="my-3">
    <p><strong>ID:</strong> <span th:text="${project.id}"></span></p>
    <p><strong>Title:</strong> <span th:text="${project.title}"></span></p>
    <p><strong>Type:</strong> <span th:text="${project.type}"></span></p>
    <p><strong>Color:</strong> <span th:text="${project.color}"></span></p>
    <p><strong>Description:</strong> <span th:text="${project.description}"></span></p>
    <p><strong>Days:</strong> <span th:text="${project.days}"></span></p>
    <p><strong>Price:</strong> <span th:text="${project.price}"></span></p>
    <p><strong>Featured:</strong> <span th:text="${project.featured} ? 'Yes' : 'No'"></span></p>
    <p><strong>Launch Date:</strong> <span th:text="${project.launchDate}"></span></p>
</div>
<a th:href="@{/create-project}">Add new project</a>

Running & Testing the Application

To start the Spring Boot application, let us first add the following main application class:

Application.java

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    
}

Now execute the following command in your terminal from the root directory of the project to start the application:

$ ./gradlew bootRun

If you use Maven, run the following command:

./mvnw spring-boot:run

Once the application is started, open http://localhost:8080/create-project in a web browser to view the HTML form. Here is what it looks like:

Spring Boot + Thymeleaf Form

When you fill all input fields and then click the "Save Project" button, you will see the following output:

Spring Boot + Thymeleaf Form Result

Source Code: Download the complete source code from GitHub available under MIT license.

Conclusion

That's all for how to use HTML forms in a Thymeleaf and Spring Boot application. We covered the complete process of creating an HTML form in Thymeleaf, from binding fields with a command object attribute, showing validation errors, handling form submission in the Spring Boot controller, to displaying the result to the user.

Thymeleaf supports all modern HTML5 input types, so you can build a complex HTML form and handle the form submission using a Spring Boot web controller.

✌️ Like this article? Follow me on Twitter and LinkedIn. You can also subscribe to RSS Feed.