Back to posts
Mastering Thymeleaf with Spring MVC: A Comprehensive Guide
Erik Nguyen / December 5, 2024
Mastering Thymeleaf with Spring MVC
Thymeleaf is a modern server-side Java template engine that works seamlessly with Spring MVC. In this guide, we'll explore how to use Thymeleaf effectively to build dynamic web applications.
Setting Up Thymeleaf with Spring MVC
First, add the necessary dependencies to your pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
Configure Thymeleaf in your Spring application:
@Configuration
public class ThymeleafConfig {
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setPrefix("classpath:/templates/");
resolver.setSuffix(".html");
resolver.setTemplateMode(TemplateMode.HTML);
resolver.setCacheable(false); // Set to true in production
return resolver;
}
}
Basic Thymeleaf Syntax
Template Structure
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>My Spring MVC App</title>
</head>
<body>
<h1 th:text="${pageTitle}">Default Title</h1>
</body>
</html>
Controller Setup
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model) {
model.addAttribute("pageTitle", "Welcome to Our App");
return "home";
}
}
Working with Data
Displaying Text and Variables
<!-- Simple text display -->
<p th:text="${message}">Default message</p>
<!-- Unescaped HTML -->
<div th:utext="${htmlContent}">Default content</div>
<!-- Using with objects -->
<div>
<p th:text="${user.name}">John Doe</p>
<p th:text="${user.email}">john@example.com</p>
</div>
Iteration
<!-- List iteration -->
<ul>
<li th:each="item : ${items}" th:text="${item.name}">Item name</li>
</ul>
<!-- With status variable -->
<table>
<tr th:each="user, status : ${users}">
<td th:text="${status.count}">1</td>
<td th:text="${user.name}">John Doe</td>
</tr>
</table>
Conditional Rendering
<!-- Simple if -->
<div th:if="${not #lists.isEmpty(users)}">
<!-- User list content -->
</div>
<!-- If-else -->
<div th:if="${user.isAdmin()}" th:text="'Welcome Admin!'"></div>
<div th:unless="${user.isAdmin()}" th:text="'Welcome User!'"></div>
<!-- Switch case -->
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="'manager'">User is a manager</p>
<p th:case="*">User is a regular user</p>
</div>
Forms Handling
Form Template
<form th:action="@{/users/save}" th:object="${user}" method="post">
<div>
<label>Name:</label>
<input type="text" th:field="*{name}" />
<span th:if="${#fields.hasErrors('name')}"
th:errors="*{name}"
class="error">
</span>
</div>
<div>
<label>Email:</label>
<input type="email" th:field="*{email}" />
<span th:if="${#fields.hasErrors('email')}"
th:errors="*{email}"
class="error">
</span>
</div>
<button type="submit">Save</button>
</form>
Controller for Form
@Controller
@RequestMapping("/users")
public class UserController {
@GetMapping("/create")
public String showForm(Model model) {
model.addAttribute("user", new User());
return "users/form";
}
@PostMapping("/save")
public String saveUser(@Valid @ModelAttribute User user,
BindingResult result) {
if (result.hasErrors()) {
return "users/form";
}
userService.save(user);
return "redirect:/users";
}
}
Layout and Fragments
Creating Fragments
<!-- fragments/header.html -->
<header th:fragment="header">
<nav>
<a th:href="@{/}">Home</a>
<a th:href="@{/about}">About</a>
<a th:href="@{/contact}">Contact</a>
</nav>
</header>
<!-- fragments/footer.html -->
<footer th:fragment="footer">
<p>© 2024 Your Company</p>
</footer>
Using Fragments
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>My App</title>
</head>
<body>
<!-- Include header -->
<div th:replace="~{fragments/header :: header}"></div>
<!-- Main content -->
<main>
<h1>Welcome</h1>
<p>Main content here</p>
</main>
<!-- Include footer -->
<div th:replace="~{fragments/footer :: footer}"></div>
</body>
</html>
Advanced Features
Expression Utility Objects
<!-- Dates -->
<p th:text="${#dates.format(today, 'dd-MM-yyyy')}">31-12-2024</p>
<!-- Numbers -->
<p th:text="${#numbers.formatDecimal(price, 1, 2)}">19.99</p>
<!-- Strings -->
<p th:text="${#strings.toUpperCase(name)}">john</p>
<!-- Lists -->
<p th:text="${#lists.size(items)}">0</p>
Custom Dialects
@Component
public class CustomDialect extends AbstractProcessorDialect {
public CustomDialect() {
super("Custom Dialect", "custom", 1000);
}
@Override
public Set<IProcessor> getProcessors(String dialectPrefix) {
Set<IProcessor> processors = new HashSet<>();
processors.add(new CustomAttributeTagProcessor(dialectPrefix));
return processors;
}
}
Security Integration
<!-- Security with Thymeleaf -->
<div th:if="${#authorization.expression('hasRole(''ADMIN'')')}">
Admin content here
</div>
<form th:action="@{/logout}" method="post"
sec:authorize="isAuthenticated()">
<button type="submit">Logout</button>
</form>
Best Practices
- Use th:block for Template Logic
<th:block th:each="user : ${users}">
<div th:text="${user.name}">John Doe</div>
<div th:text="${user.email}">john@example.com</div>
</th:block>
- Leverage Expression Objects
<div th:if="${#strings.isEmpty(user.name)}">
<p>Please enter a name</p>
</div>
- Use Link URLs
<!-- Prefer this -->
<a th:href="@{/user/{id}(id=${user.id})}">View Profile</a>
<!-- Over this -->
<a th:href="'/user/' + ${user.id}">View Profile</a>
Testing Thymeleaf Views
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldRenderUserForm() throws Exception {
mockMvc.perform(get("/users/create"))
.andExpect(status().isOk())
.andExpect(view().name("users/form"))
.andExpect(model().attributeExists("user"));
}
}
Conclusion
Thymeleaf provides a powerful and flexible way to create dynamic web pages in Spring MVC applications. Its natural templating approach means templates can be viewed as static prototypes in browsers while still providing rich server-side capabilities.