When you develop an API, you usually need to respond to requests with data in JSON or XML format generated by the serializer. Working with a human user requires a more intuitive interface and design. You should use HTML with CSS and JavaScript styles.
However, the content of the page often depends heavily on the context, for example, the username. Template engines allow you to substitute context elements in the appropriate places on the page. We will talk about one of these template engines, Thymeleaf, below.
Configuration
Thymeleaf is an XML/XHTML/HTML5 template engine able to apply a set of transformations to template files to display data and/or text produced by your applications.
To use Thymeleaf, you need to include the spring-boot-starter-thymeleaf artifact:
Gradle
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>You can configure the ServletContextTemplateResolver. This class is used to index templates as loader resources so that you can load these files and use them for the template engine.
For example, the code snippet below enables the resolver to look up *.html templates in the resources/templates folder:
@Bean
@Description("Thymeleaf Template Resolver")
public ServletContextTemplateResolver templateResolver() {
ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver();
templateResolver.setPrefix("templates/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode("HTML5");
return templateResolver;
}
@Bean
@Description("Thymeleaf Template Engine")
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
templateEngine.setTemplateEngineMessageSource(messageSource());
return templateEngine;
}The ViewResolver interface in Spring MVC maps the view names returned by a controller to actual view objects. ThymeleafViewResolver implements the ViewResolver interface, and it’s used to determine which Thymeleaf views to render, given a view name.
The final step in the integration is to add the ThymeleafViewResolver as a bean:
@Bean
@Description("Thymeleaf View Resolver")
public ThymeleafViewResolver viewResolver() {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
viewResolver.setOrder(1);
return viewResolver;
}Using templates
Let's say we want to make a to-do list. To do this, we will need a user and a list of their tasks, which we will create using data classes:
Java
public class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Task {
private String name;
private boolean done;
public Task(String name) {
this.name = name;
this.done = false;
}
public Task(String name, boolean done) {
this.name = name;
this.done = done;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isDone() {
return done;
}
public void setDone(boolean done) {
this.done = done;
}
}We created the user, but we need a page to welcome them. We will create a so-called HTML template, a page, in some places of which we will substitute the necessary information depending on the context. The template engine substitutes the context to the places indicated by the prefix th. For example, Thymeleaf substitutes the value of a variable named context into <p> tag: <p th:text="'${context}"></p>
Now we create a file resources/templates/index.html , in which there is a greeting for the user:
<html lang="en", xmlns:th="http://www.thymeleaf.org">
<body>
<h1 th:text="'Welcome, ' + ${user.name}"></h1>
</body>
</html>The final touch is adding an endpoint:
@GetMapping("/index")
public ThymeleafContent getIndex() {
Map<String, Object> model = new HashMap<>();
model.put("user", new User("John Doe"));
return new ThymeleafContent("index.html", model);
}Iterate over the list
Thymeleaf provides many tools for context management: variables, loops, conditions, arithmetic, and so on. We will study the most commonly used ones.
We have already learned how to work with the user, now we need to learn how to display his tasks. To display lists, Thymeleaf provides the keyword th:each. For example, if we pass listOf("one", "two", "three") to the template engine, it will be expanded into the HTML code of three blocks:
<div th:each="el : ${list}">
<p th:text="${el}"></p>
</div>Produces such HTML code:
<div>
<p>One</p>
<p>Two</p>
<p>Three</p>
</div>Thus, we can use the keyword th:each to list all the cases. Let's update the file index.html:
<html lang="en", xmlns:th="http://www.thymeleaf.org">
<body>
<h1 th:text="'Welcome, ' + ${user.name}"></h1>
<hr>
<div th:each="task : ${tasks}">
<h4 th:text="${task.name}"></h4>
<p th:text="'Done: ' + ${task.done}"></p>
<hr>
</div>
</body>
</html>And update the endpoint handler:
@GetMapping("/index")
public ThymeleafContent getIndex() {
Map<String, Object> model = new HashMap<>();
model.put("user", new User("John Doe"));
List<Task> tasks = IntStream.range(0, 3)
.mapToObj(i -> new Task("Task for today #" + i))
.collect(Collectors.toList());
model.put("tasks", tasks);
return new ThymeleafContent("index.html", model);
}Now, our page will look like this:
Referencing
It's time to complicate our application a bit. Tasks can also store IDs and descriptions. Also, let's now move our user and task entities to the global scope so that other endpoints can also have access to them:
public class Task {
private int id;
private String name;
private String description;
private boolean done;
public Task(int id, String name, String description, boolean done) {
this.id = id;
this.name = name;
this.description = description;
this.done = done;
}
// Getters and Setters
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean isDone() {
return done;
}
public void setDone(boolean done) {
this.done = done;
}
}
User user = new User("John Doe");
List<Task> tasks = new ArrayList<>();
for (int id = 0; id < 3; id++) {
tasks.add(new Task(id, "Task for today", "Very long description for task", id % 2 == 0));
}
// Printing tasks
for (Task task : tasks) {
System.out.println("Task ID: " + task.getId() + ", Name: " + task.getName() + ", Description: " + task.getDescription() + ", Done: " + task.isDone());
}We want not only to be able to view the task list but also to be able to get detailed information about the task using its ID. In addition, it is impractical to display a description of the task in the list, since it can be very long. To do this, we will create an endpoint for getting information about the task by its ID:
@GetMapping("/index")
public ThymeleafContent getIndex() {
Map<String, Object> model = new HashMap<>();
model.put("user", user);
model.put("tasks", tasks);
return new ThymeleafContent("index.html", model);
}
@GetMapping("/task/{id}")
public ThymeleafContent getTask(@PathVariable String id) {
int taskId = Integer.parseInt(id);
if (taskId < 0 || taskId >= tasks.size()) {
throw new RuntimeException("Invalid id");
}
Task task = tasks.get(taskId);
Map<String, Object> model = new HashMap<>();
model.put("task", task);
return new ThymeleafContent("task.html", model);
}For displaying information on the task ID, we will create resources/templates/task.html:
<html lang="en", xmlns:th="http://www.thymeleaf.org">
<body>
<h1 th:text="'ID' + ${task.id} + ' ' + ${task.name}"></h1>
<p th:text="${task.description}"></p>
<p th:text="'Done: ' + ${task.done}"></p>
</body>
</html>Since the application now works with two endpoints, it would be nice to add routing. To do this, we will use the keyword th:href, which is used to work with hyperlinks. Tag <a th:href="@{a/b/c}"></a> will create a link to the a/b/c resource relative to the path of the page on which it is located. If you add a leading slash <a th:href="@{/a/b/c}"></a>, then the path will be constructed relative to the root of the host.
Moreover, this keyword is able to construct links depending on the context. Let's say we create a link to a product whose ID is specified in the URL: <a th:href="@{good/{id}(id=5)}"></a>. This code will create the following HTML: <a href="good/5"></a>. Well, in the case, and if the path parameter also depends on the context, then we can get it like any other variable: <a th:href="@{good/{id}(id=${good.id})}"></a>
Given the above, let's update index.html:
<html lang="en", xmlns:th="http://www.thymeleaf.org">
<body>
<h1 th:text="'Welcome, ' + ${user.name}"></h1>
<hr>
<div th:each="task : ${tasks}">
<h4 th:text="'ID' + ${task.id} + ' ' + ${task.name}"></h4>
<p th:text="'Done: ' + ${task.done}"></p>
<a th:href="@{task/{id}(id=${task.id})}">click</a>
<hr>
</div>
</body>
</html>We can make sure that the links lead to the relevant pages:
Conditional Evaluation
Displaying the value of a boolean variable to the user is not very intuitive. Let's write YES or NO in the execution field instead. To do this, we use the ternary operator: <p th:text="${task.done} ? 'YES' : 'NO'"></p>. If the task completes, we will get the following HTML: <p>YES</p>otherwise <p>NO</p> will be generated.
It's not good to mix all the tasks in one heap. It is worth dividing the tasks into two blocks: done, and in the process. Simply put, we need to iterate through the list twice and display only those tasks that correspond to the block. The keyword th:if is perfect for these purposes.
The content placed inside the block with the condition will only be displayed if the condition is true. For example, if we pass listOf(Element(ready=true, text="first"), Element(ready=false, text="second")):
<div th:each="el : ${list}">
<div th:if="${el.ready}" >
<p th:text="${el.text}"></p>
</div>
</div>Such code will generate the following HTML:
<div>
<div>
<p>First</p>
</div>
</div>Now we can update index.html to get the division of tasks by status:
<html lang="en", xmlns:th="http://www.thymeleaf.org">
<body>
<h1 th:text="'Welcome, ' + ${user.name}"></h1>
<div>
<h2>In progress:</h2>
<hr>
<div th:each="task : ${tasks}">
<div th:if="${task.done}" >
<h4 th:text="${task.name}"></h4>
<p th:text="${task.done} ? 'YES' : 'NO'"></p>
<hr>
</div>
</div>
</div>
<div>
<h2>Done:</h2>
<hr>
<div th:each="task : ${tasks}">
<div th:if="${not task.done}" >
<h4 th:text="${task.name}"></h4>
<p th:text="${task.done} ? 'YES' : 'NO'"></p>
<hr>
</div>
</div>
</div>
</body>
</html>As you can see, we also used the not keyword in the condition as a logical negation operator. The result is now like this:
Switch-case
Let's add urgency status for each task:
public enum Status {
NORMAL, WARNING, URGENT;
}
public class Task {
private int id;
private String name;
private String description;
private Status status;
private boolean done;
public Task(int id, String name, String description, Status status, boolean done) {
this.id = id;
this.name = name;
this.description = description;
this.status = status;
this.done = done;
}
// Getters and Setters
}
List<Task> tasks = new ArrayList<>();
for (int id = 0; id < 3; id++) {
tasks.add(new Task(id, "Task for today", "Very long description for task", Status.values()[id], id % 2 == 0));
}
// task0 is NORMAL, task1 is WARNING, task2 is URGENT
System.out.println("Tasks created: ");
for (Task task : tasks) {
System.out.println("Task id: " + task.getId() + ", Status: " + task.getStatus());
}Depending on the urgency, we will display different comments on the task. To do this, Thymeleaf provides a switch-case construct. Since we are comparing with constants from the enum, we should compare name() with string:
<div th:switch="${task.status.name()}">
<p th:case="'NORMAL'">No need to hurry</p>
<p th:case="'WARNING'">Try not to delay</p>
<p th:case="'URGENT'">Complete this task first</p>
</div>Using the code above, we will display only the part of the HTML that meets the condition. For example, if the task has a WARNING urgency, then displays only <p>Try not to delay</p>:
Conclusion
In this topic, we learned how to create user-friendly pages using the Thymeleaf template engine. Not only can we place context in appropriate places on the HTML page, but we can also use loops, conditions and, other tools.