Skip to content

MVC and Thymeleaf

The spring-boot-starter-web starter allows, in addition to creating the REST API, to create MVC applications with end-user views.

We can create these views using technologies such as:

  • thymeleaf
  • groovy markup templates
  • JSP

In this section we will focus on the description of thymeleaf. Furthermore, we assume that you understand the concepts described in [this] (rest.md) section.

Thymeleaf - default configuration

In order to use thymeleaf in a project, we need to add the following starter to the dependency:

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

Then, for the Spring Boot based application to be able to correctly display HTML views in context there must be beans with the following names:

  • templateResolver, which defines where the view files are located and what extension they have
  • templateEngine, which defines the technology that processes file templates representing views
  • viewResolver

The starter (spring-boot-starter-thymeleaf) added to the project, thanks to the auto-configuration mechanism, it provides implementations of beans with the above names. Default values include:

  • HTML templates should be in the resources/templates directory (this is the so-called root)
  • files should have the html extension (so we won't have to duplicate this information in the code)
  • viewResolver has an appropriate implementation (thymeleafViewResolver)

Defining controllers

Building controllers that can render a view from an HTML template is very similar to creating REST services. The controller is a separate class, marked with the annotation @Controller, which goes toWebApplicationContext.

Inside this class, we define under which paths specific views are available. We do this with annotations @RequestMapping, in which we define the path and HTTP method. However, we must remember that the only HTTP methods available in the browser are GET and POST. As in the case of REST API, this annotation can be defined directly on the class or attached to the method definition (or used in both places). It is also possible to use [abbreviated] versions (rest.md # short-writes), ie use @GetMapping and@PostMapping instead of @RequestMapping.

Moreover, these methods should return information which page to display in the next step. This information can be provided in many ways, e.g. by returning:

  • a String object that indicates the path to the view. If the default configuration is used, we can omit the file extension.
  • the ModelAndView object that returns information about the next view with its state (the so-called Model, discussed later in this section)
  • a RedirectView object that tells the end user address to redirect to

In the following examples, the String object will most often be returned. The first example shows how to display an HTML view with the appropriate message at the address http://localhost:8080/hello:

<!--welcome.html in the resources/templates directory-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Welcome Page</title>
</head>
<body>
    <p>Hello from thymeleaf view</p>
</body>
</html>
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HelloController {

  @GetMapping("/hello")
  public String showHello() {
    return "welcome";
  }
}

Passing data to views

Thymeleaf allows you to define templates of HTML views, i.e. they can additionally have places that are replaced with values that come from the backend (e.g. data from the database). In order to be able to use additional Thymeleaf functionalities we need to add the appropriate namespace to the HTML document (and the html tag):

xmlns:th="http://www.thymeleaf.org"

NOTE: The most common name for the above imported namespace is th, but it can be anything.

Define the places to replace in the HTML template as follows:

  • in the tag we indicate the attribute that should be replaced with the name. We precede this attribute with the name namespace and the character :, e.g.: <p th:text=...></p>
  • indicate the value to be replaced inside the braces and precede it with a dollar sign, e.g. ${myCustomAttributeName}

ModelMap

In order to pass data from the backend to the template, we use the ModelMap object (or the Model interface). Such an object can be injected to the method in the controller. This object behaves like map, in which the keys are [Strings] (../javaBasics/string_class.md) and the values are any objects. The key name used in the ModelMap object should match the name to be replaced on the frontend side.

An example below shows the use of this mechanism:

<!--plik welcome.html-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Welcome Page</title>
</head>
<body>
    <p th:text="${helloMsg}"></p>
</body>
</html>
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HelloController {

  private static final String MODEL_HELLO_MESSAGE_ATTRIBUTE_NAME = "helloMsg";

  @GetMapping("/hello")
  public String showHello(final ModelMap modelMap) {
    modelMap.addAttribute(MODEL_HELLO_MESSAGE_ATTRIBUTE_NAME, "hello in thymeleaf from model map");
    return "welcome";
  }
}

After entering the website http://localhost:8080/hello, we should see the text hello in thymeleaf from model map.

NOTE: The ModelMap object does not need to be returned in the method. Its attributes will be automatically passed to the template.

CSS and external libraries

When creating view templates, we want them to be readable and nice for the end user. In this case, most often we want to use [CSSy] (../HTML_CSS_JS/CSS/introduction.md) or some external libraries that will enable us to do so. One popular library that makes styling easier and faster is bootstrap. In order to use it (as well as other libraries), we can use the dependency:

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>bootstrap</artifactId>
    <version>4.5.2</version> <!--a newer version can be available-->
</dependency>

Thanks to this, in the HTML template, we can directly use the appropriate dependencies, e.g.

<link rel="stylesheet" href="/webjars/bootstrap/4.5.2/css/bootstrap.min.css">
<script src="/webjars/bootstrap/4.5.2/js/bootstrap.min.js"></script>

And then use on defined elements:

<!--the btn and btn-dark classes come from the bootstrap-->
<button class="btn btn-dark">Hello From Bootstrap</button>

NOTE: Many libraries related to frontend technologies can be downloaded as jar files. More about available libraries can be found [here] (https://www.webjars.org/).

Thymeleaf and forms

In addition to displaying information, views also allow data to be sent to the backend using forms. When the end user fills in such a form, we want to process the entered data in some way (and e.g. save it to the database). To do this, we need to define the object that this form represents. Then an instance of such an object should be added to model.

On the HTML template side, in the form tag with theth: object attribute we indicate the name of the object from the model representing the form. Additionally, in the method tag, we define the HTTP method (POST or GET) and the address to which the results will be sent. We define this address with the th: action attribute, which defines the appropriate application path. This path (as well as other thymeleaf links) should be placed in braces and preceded by the @ sign. Thanks to this, we do not have to specify the website's domain or use a relative path. Also remember that each form should have a button that send values ​​in its fields, e.g .:

<form class="user-form" method="post" th:action="@{/orders/create}" th:object="${ordersForm}">
<!--      pola formularza-->
  <button class="user-form-elem" type="submit">
    SEND DATA TO THE SERVER
  </button>
</form>

Form fields

Defining a form field is very similar to how we do it in [HTML] (../ HTML_CSS_JS/HTML/forms.md # form-fields). An additional attribute required to correctly send the information to the backend is the th: field, which indicates the name of the field in the object to which it should be assigned. Such a name should be enclosed in braces and preceded by an asterisk, e.g .:

<input type="text" th:field="*{name}">

Forms processing

When the user completes the form and sends the data to the server, they will go to the controller who:

  • maps to the appropriate path indicated in the th: action attribute
  • uses the HTTP method specified in the method attribute

In addition, the value of the form (and therefore with the fields filled in) can be downloaded:

  • directly from the ModelMap object injected into the method. This approach requires casting to a specific type (form).
  • directly from the argument of the method, using the @ModelAttribute annotation, inside which we enter the key under which the given form is located. This argument may already be of the appropriate type (i.e. form type)

NOTE: The @ModelAttribute annotation is similar to [@RequestBody] (rest.md # annotation-requestbody) for the REST API.

Example

The following example presents all the concepts described above:

<!--order.html file-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Orders</title>
</head>
<body>
    <form method="post" th:action="@{/orders/create}" th:object="${orderForm}">
        <label>
            Product name:
            <input type="text" th:field="*{productName}">
        </label>

        <label>
            Amount:
            <input type="number" th:field="*{amount}">
        </label>

        <button type="submit">SEND DATA</button>
    </form>
</body>
</html>
// form representation with an object
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderForm {
  private String productName;
  private Integer amount;
}
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/orders")
public class OrderController {

  @GetMapping("/create")
  public String showOrderForm(final ModelMap modelMap) {
    modelMap.addAttribute("orderForm", new OrderForm());
    return "orders";
  }

  @PostMapping("/create")
  public String handleNewOrder(@ModelAttribute("orderForm") final OrderForm orderForm) {
    // orderForm fields are filled with user values
    // handle new Order here
    return "redirect:/hello";
  }
}

Upon entering the address http://localhost:8080/orders/create, a form will be displayed, which, after being submitted by the user, will go to thehandleNewOrder method. This method also uses a redirect (i.e. sending a 3xx status), which we can do by returning a String starting with redirect: and then the URL to which we want to redirect the user (it can also be an "external" page) .

Validation

When using MVC and regular controllers, handling [validation] errors (validation.md) requires a few extra lines of code. We can use standard [validation annotations] (validation.md#validation-annotations) on the object that represents the form.

In order to force the validation, we use the annotation @Valid when the user submits the completed form (usually with the@ModelAttribute). However, the application does not throw an exception, but gives us access to an Errors object, which we can inject as another argument to the method. This object represents the collection of all user errors. Based on the information whether any error exists, we can display the form to the user again and information on what to correct.

In case of errors, an element can be conditionally displayed at the template level. Conditional display can be obtained by using the th: if attribute that takes as input some expression (or object) that must evaluate to the valuetrue.

In addition, Thymeleaf gives you access to some globally defined objects. Such an object is preceded by the # character and one of them is fields, which gives you the ability to:

  • check if any errors exist (#fields.hasErrors())
  • checking if a given field contains an error (#fields.hasErrors('fieldName'))
  • error content download (#fields.errors('amount'))

The following example builds on the previous one. In case the user enters a amount less than 5, they will receive the appropriate error:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Orders</title>
</head>
<body>
    <form method="post" th:action="@{/orders/create}" th:object="${orderForm}">
        <label>
            Product name:
            <input type="text" th:field="*{productName}">
        </label>

        <label>
            Amount:
            <input type="number" th:field="*{amount}">
            <ul th:if="${#fields.hasErrors('amount')}">
                <li th:each="err : ${#fields.errors('amount')}" th:text="${err}">_Input is incorrect</li>
            </ul>
        </label>

        <button type="submit">SEND DATA</button>
    </form>
</body>
</html>
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.Min;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderForm {
  private String productName;

  @Min(value = 5, message = "amount has to be at least 5")
  private Integer amount;
}
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.validation.Valid;

@Controller
@RequestMapping("/orders")
public class OrderController {

  @GetMapping("/create")
  public String showOrderForm(final ModelMap modelMap) {
    modelMap.addAttribute("orderForm", new OrderForm());
    return "orders";
  }

  @PostMapping("/create")
  public String handleNewOrder(@Valid @ModelAttribute("orderForm") final OrderForm orderForm, final Errors errors) {
    // handle new Order here
    if (errors.hasErrors()) {
      return "orders";
    }
    return "redirect:/hello";
  }
}

Error template

If any exception occurs while processing the method in the controller, the error template will be displayed by default, e.g. entering the addresshttp://localhost:8080/boom will display a page with the content Error occurred:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import pl.sdacademy.sb.exceptions.SdaException;

@Controller
public class ThrowingController {

  @GetMapping("/boom")
  public String shouldThrow() {
    throw new SdaException("Boom...");
  }
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Something bad has happened</title>
</head>
    <body>
        <p>Error occurred</p>
    </body>
</html>

Iteration

We can also pass collections to the ModelMap object, such as for example lists. In this case, on the template side, we often want to perform a certain action as many times as the size of a given collection (e.g. display a table row for each record in the list). Thymeleaf allows you to iterate through collections using the th: each attribute, and the syntax is similar to Java, e.g .:

modelMap.addAttribute("elements", List.of("one", "two", "three"));
<div class="elements">
  <p th:each="elem : ${elements}" th:text="${elem}"></p>
</div>

Nested names

Sometimes there is a need, inside a value that requires the use of braces, to use another that also requires such braces. This can happen, for example, when creating a URL that includes an identifier available in the ModelMap object. In this case, the internal value must be between two _ characters, e.g .:

<form class="user__form" method="post" th:action="@{/users/edit/__${userForm.username}__}" th:object="${userForm}">
<!--zawartość formularza-->
</form>

Messages

HTML applications typically have a lot of content on the front end. Instead of hardcoding the content in the template, it is better to move it to a separate file, and refer to the appropriate message key in the template.

Thymeleaf by default allows you to define such messages in the messages.properties file, which by default should be located directly in the resources directory. In this file we store the keys and the corresponding values. In the HTML template, we refer to the key using the # sign and clasps, e.g .:

hi.msg=Hi there!
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Welcome Page</title>
    <link rel="stylesheet" href="/webjars/bootstrap/4.5.2/css/bootstrap.min.css">
    <script src="/webjars/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</head>
<body>
    <p th:text="#{hi.msg}"></p>

</body>
</html>

This approach also allows the implementation of internationalization by means of the so-called interceptors that are outside the scope of this training.

Cheat sheet

At the very beginning, the Thymeleaf syntax can seem very complicated. Most of the items should be inside the clasps and preceded by some character. Below we present a "cheat sheet" that describes the purpose for which we use a given character:

  • $ - variable, e.g. object from backend
  • @ - URI
  • # - messages from message.properties or a global object
  • * - form field, e.g. in the th: field attribute
  • __ - use an element inside another