Views

MVC Application using Spring Boot and Thymeleaf

MVC Application using Spring Boot and Thymeleaf
Page content

In this article, I will be developing a Model-View-Controller (MVC) application using Spring Boot, H2 Database and Thymeleaf (Java Template Engine).

Spring Boot Framework will serve as back-end(created in Microservice Using Spring Boot, Hibernate and H2 Database, same code will be reused with few modifications).

I will be using the same Relational Database known as H2 Database, for persisting(storing) the data, used in the above mentioned project.

Thymeleaf (Java Template Engine) will be the User Interface (UI) for this application.

If you have no time to read this article, but want to try the code for yourself, GitHub location is provided here. Go ahead and clone the code.


Overview

We will build a MVC application using Spring Boot, H2 Database and Thymeleaf (Java Template Engine) for a Todo Task Application in that:

  • Each Todo Task has id, title, description, creation date, due date, status and comments.
  • User can Create New Task, Update Existing Task, View All Tasks as List, View a Task and Delete a Task.

What is Thymeleaf?

Thymeleaf is a Java Template Engine for processing and creating HTML5, XML, JavaScript, CSS and Text. It is able to apply a set of transformations to template files in order to display data and/or text produced by your applications.

The library is extremely extensible and its natural templating capability ensures templates can be prototyped without a backend – which makes development very fast when compared with other popular template engines such as JSP.

Thymeleaf has a good integration with Spring MVC Framework, making it first choice library for Frontend Development in Java and Spring Technologies.


Prerequisites

There are some prerequisites that are required for creating the Microservice Spring Boot Application.

Familiarity with Technology and Frameworks

It is assumed that you have prior basic knowledge or familiarity with Java Technology, HTML, Thymeleaf, Spring, Spring Boot, Hibernate Frameworks, working with RDBMS databases and basic SQL commands, because I will not be covering the basics of these in this article.

If you are not familiar, then it is advised to get the basic knowledge of these before continuing.

JDK 1.8+

Download the latest version of the Java JDK (1.17 is the latest LTS version, at the time of writing this article) from here. Click on the downloaded .exe and complete the installation.

Since it is an Oracle Proprietary product you will need to sign-in/create Oracle Account, before downloading.

Integrated Development Environment (IDE) for Code Development

You can use any IDE of your choice. I will be using the IntelliJ IDEA Community Edition.

If you wish to use the IntelliJ IDEA Community Edition, download the latest version from here.

Click on the downloaded .exe and complete the installation.

Webserver– Tomcat is embedded in the Spring Boot application and can be used as Webserver for running this project.

Project Management Tool (Maven)– Maven is embedded in the Spring Boot application and can be used for building and managing the Spring Boot project.


Adding Thymeleaf Template Engine to Spring Boot Project

We will be updating the Microservice Application already created in Microservice Using Spring Boot, Hibernate and H2 Database. We will make the necessary changes to add Thymeleaf (Java Template Engine) , as a View Layer to this application.

Update pom.xml

  • Add the following Thymeleaf (Java Template Engine) dependencies
1<dependency>
2    <groupId>org.springframework.boot</groupId>
3    <artifactId>spring-boot-starter-thymeleaf</artifactId>
4</dependency>

Create Fragment Templates

Before creating endpoints, let us first understand one important feature of the Thymeleaf- Fragment Templates, also known as Frames.

Default location for storing all the Thymeleaf templates is: /src/main/resources/templates

Fragment templates provides consistency across the pages to have same header, footer contents and also maintain similar feel to the pages.

There are two ways of creating fragment templates in Thymeleaf

Using the layout and layout:decorator Dialect Directive

Define the header, footer and body section in the main template and replace only the body section in the subsequent html files, using the Thymeleaf layout and layout:decorator dialect directive.

Let us see an example:

In order to get this to work, we need to have a below Maven dependency in the pom.xml:

1<dependency>
2	<groupId>nz.net.ultraq.thymeleaf</groupId>
3	<artifactId>thymeleaf-layout-dialect</artifactId>
4</dependency>

Create a BaseLayout.html file in the location: /src/main/resources/templates/Layouts

 1<!DOCTYPE html>
 2<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
 3    <head>
 4        <meta charset="UTF-8"/>
 5        <title>Base Layout</title>
 6    </head>
 7    <body>
 8        <header th:fragment="custom-header">This is a custom header</header>
 9        <section layout:fragment="custom-body-content">This is the main body section</section>
10        <footer th:fragment="custom-footer">This is a custom footer</footer>
11    </body>
12</html>

This creates a Main Fragment page, on which body section will get replaced.

Create a home.html file in the location: /src/main/resources/templates

 1<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" 
 2    layout:decorator="Layouts/BaseLayout">
 3    <head>
 4        <title layout:title-pattern="$DECORATOR_TITLE | $CONTENT_TITLE">Dashboard</title>
 5    </head>
 6    <body>
 7        <section layout:fragment="custom-body-content">
 8            <div class="container-fluid">
 9                This the home page for the example
10            </div>
11        </section>
12    </body>
13</html>

Contents inside the <section layout:fragment="custom-body-content"> attribute in the home.html file will replace the contents in the BaseLayout.html file with the same <section> attribute during the runtime, since we have mapping for the layout using the Thymeleaf directive.

Using the fragment replace directive

Define the header, footer and body section in the main template and add the header and footer sections in the subsequent html files using the Thymeleaf replace directive.

Let us see an example.

Create a header.html file in the location: /src/main/resources/templates/fragments

 1<html xmlns:th="http://www.thymeleaf.org">
 2    <head>
 3        <div th:fragment="header-css">
 4            This is header-css fragment            
 5        </div>
 6    </head>
 7    <body>
 8        <div th:fragment="header">
 9            This is header fragment
10        </div>
11    </body>
12</html>

Similarly create a footer.html file and place it in the location: /src/main/resources/templates/fragments

 1<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
 2    <head>
 3    </head>
 4    <body>
 5        <div th:fragment="footer">
 6            <footer>
 7                This is footer fragment
 8            </footer>
 9        </div>
10    </body>
11</html>

Create a home.html file in the location: /src/main/resources/templates/ and use these header and footer fragments.

 1<html xmlns:th="http://www.thymeleaf.org">
 2    <head>
 3        <title>Spring Boot Thymeleaf + Spring Security</title>
 4        <div th:replace="fragments/header :: header-css"/>
 5    </head>
 6    <body>
 7        <div th:replace="fragments/header :: header"/>
 8        <div class="container">
 9            This is body content
10        </div>
11        <div th:replace="fragments/footer :: footer"/>
12    </body>
13</html>

As you can see in the above code, header and footer sections are inserted into the page using the replace attribute from the Thymeleaf library.

We have seen 2 different methods of creating and using the fragment templates in the Thymeleaf. You can create and use either or none of them, based on your requirement.


Create Thymeleaf Application

For our example we will be using the Using the fragment replace directive explained above.

Final Project Structure

Final Project Structure would look something like given below:

project-structure.jpeg
Project Structure

Let us create header.html and footer.html files in their respective locations, as explained earlier.

  • Create header.html file in the location: /src/main/resources/templates/fragments
 1<html xmlns:th="http://www.thymeleaf.org">
 2    <head>
 3        <div th:fragment="header-css">
 4            <!-- this is header-css -->
 5            <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
 6                integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
 7                crossorigin="anonymous">
 8        </div>
 9    </head>
10<body>
11    <div th:fragment="header">
12        <!-- this is header -->
13        <nav class="navbar navbar-inverse">
14            <div class="container">
15               <div id="navbar" class="collapse navbar-collapse">
16                    <ul class="nav navbar-nav">
17                        <li class="active"><a th:href="@{/}">Home</a></li>
18                    </ul>
19                </div>
20            </div>
21        </nav>
22    </div>
23</body>
24</html>
  • Create a footer.html file and place it in location: /src/main/resources/templates/fragments
 1<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
 2<head>
 3</head>
 4<body>
 5<div th:fragment="footer">
 6    <div class="container" align="center">
 7        <footer>
 8            <!-- this is footer -->
 9            2017-<script>document.write(new Date().getFullYear())</script>
10        </footer>
11    </div>
12</div>
13</body>
14</html>

Create required html files

  1. Add /src/main/resources/templates/index.html
 1<html lang="en" xmlns:th="http://www.thymeleaf.org">
 2<head>
 3    <meta charset="UTF-8">
 4    <title>To do Tracker</title>
 5    <div th:replace="fragments/header :: header-css"></div>
 6</head>
 7<body>
 8    <div th:replace="fragments/header :: header"></div>
 9    <div class="container" align="center">
10        <a th:href="@{/singleItemView/edit/0}"><button class="btn btn-success">CREATE</button></a>
11        <br/>
12        <table class='table-striped' border='1' align="center">
13            <thead>
14            <tr>
15                <th>Title</th>
16                <th>Description</th>
17                <th>Due Date</th>
18                <th>Status</th>
19                <th>No. Of Comments</th>
20                <th>View</th>
21                <th>Edit</th>
22                <th>Delete</th>
23            </tr>
24            </thead>
25            <tbody>
26                <tr th:each="tasks : ${tasksItemList}">
27                    <td th:text="${tasks.title}"></td>
28                    <td th:text="${tasks.description}"></td>
29                    <td th:text="${tasks.dueDate}"></td>
30                    <td th:text="${tasks.status}"></td>
31                    <td align="center" th:text="${tasks.todoTaskCommentsSet.size()}"></td>
32                    <td>
33                        <a th:href="@{/singleItemView/view/__${tasks.systemTasksId}__}"><button class="btn btn-success">VIEW</button></a>
34                    </td>
35                    <td>
36                        <a th:href="@{/singleItemView/edit/__${tasks.systemTasksId}__}"><button class="btn btn-success">EDIT</button></a>
37                    </td>
38                    <td>
39                        <a th:href="@{/deleteById/__${tasks.systemTasksId}__}"><button class="btn btn-warning">DELETE</button></a>
40                    </td>
41                </tr>
42            </tbody>
43        </table>
44    </div>
45    <br/>
46    <div th:replace="fragments/footer :: footer"></div>
47</body>
48</html>

Highlighted lines(5,8,46) are using the th:replace directive dialect of the Thymeleaf to have the header and footer from the fragments templates, inserted into this template file.

  1. Add /src/main/resources/templates/singleItemPage.html
  1<html lang="en" xmlns:th="http://www.thymeleaf.org">
  2<head>
  3    <meta charset="UTF-8">
  4    <title>To do Tracker</title>
  5    <div th:replace="fragments/header :: header-css"></div>
  6    <script type="text/javascript">
  7        function addNewComments(){
  8            var lTable = document.getElementById("commentsTable");
  9            lTable.style.display = (lTable.style.display == "table") ? "none" : "table";
 10        }
 11    </script>
 12</head>
 13<body>
 14    <div th:replace="fragments/header :: header"></div>
 15    <div class="container" align="center">
 16        <form method="post" th:action="@{/createOrUpdate}" th:object="${tasksItem}">
 17        <table class='table-striped' border='1'>
 18            <tbody>
 19                <input type="hidden" name="systemTasksId" id="systemTasksId" th:value="*{systemTasksId}"/>
 20                <tr>
 21                    <th>Title</th>
 22                    <td>
 23                        <span th:if="${action == 'view'}" th:text="${tasksItem.title}"></span>
 24                        <span th:unless="${action == 'view'}">
 25                            <input type="text" name='title' id='title' th:value='*{title}' size="35"/>
 26                        </span>
 27                    </td>
 28                </tr><tr>
 29                    <th>Description</th>
 30                    <td>
 31                        <span th:if="${action == 'view'}" th:text="${tasksItem.description}"></span>
 32                        <span th:unless="${action == 'view'}">
 33                            <textarea name='description' id='description' rows="3" cols="38" th:value='*{description}' th:text="${tasksItem.description}"></textarea>
 34                        </span>
 35                    </td>
 36                </tr><tr th:if="${tasksItem.systemTasksId > 0}">
 37                    <th>Creation Date</th>
 38                    <td>
 39                        <span th:text="${#temporals.format(tasksItem.creationDate, 'dd-MM-yyyy')}"></span>
 40                    </td>
 41                </tr><tr>
 42                    <th>Due Date</th>
 43                    <td>
 44                        <span th:if="${action == 'view'}" th:text="${#temporals.format(tasksItem.dueDate, 'dd-MM-yyyy')}"></span>
 45                        <span th:unless="${action == 'view'}">
 46                            <input type="date" th:field="*{dueDate}" th:value="*{dueDate}"/>
 47                        </span>
 48                    </td>
 49                </tr><tr>
 50                    <th>Status</th>
 51                    <td>
 52                        <span th:if="${action == 'view'}"th:text='${tasksItem.status}'></span>
 53                        <span th:unless="${action == 'view'}">
 54                            <select th:field="*{status}">
 55                            <option value=''>--Select Status--</option>
 56                            <option th:each="status : ${todoStatus}" th:value="${status}" th:text="${status}">
 57                            </option>
 58                        </select>
 59                        </span>
 60                    </td>
 61                </tr>
 62                <tr th:if="${tasksItem.systemTasksId > 0}">
 63                    <th>Comments</th>
 64                    <td>
 65                        <table class='table-bordered'>
 66                            <thead th:if="${tasksItem.todoTaskCommentsSet.size() > 0}">
 67                            <tr>
 68                                <th>Creation Date</th>
 69                                <th>Description</th>
 70                            </tr>
 71                            </thead>
 72                            <tbody>
 73                                <tr th:each="comments : ${tasksItem.todoTaskCommentsSet}"
 74                                 th:if="${tasksItem.todoTaskCommentsSet.size() > 0}">
 75                                    <td>
 76                                        <span th:text="${#temporals.format(comments.creationDate, 'dd-MM-yyyy')}"></span>
 77                                    </td>
 78                                    <td th:text="${comments.taskComments}"></td>
 79                                </tr>
 80                                <tr>
 81                                    <td colspan="2" th:if="${action == 'edit' and tasksItem.systemTasksId > 0}" th:object="${todoTaskComments}">
 82                                        <input type="button" class="btn btn-success" onclick=addNewComments()
 83                                         value="Add New Comments" />
 84                                        <table class='table-bordered' id="commentsTable" style="display:none;">
 85                                                <tr>
 86                                                    <th>Description</th>
 87                                                    <td>
 88                                                        <textarea th:field="*{taskComments}" rows="3" cols="38"></textarea>
 89                                                    </td>
 90                                                </tr>
 91                                        </table>
 92                                    </td>
 93                                </tr>
 94                            </tbody>
 95                        </table>
 96                    </td>
 97                </tr>
 98                <tr th:if="${action == 'edit'}">
 99                    <td colspan="2" align="center">
100                        <button class="btn btn-success">Submit</button>
101                    </td>
102                </tr>
103            </tbody>
104        </table>
105        </form>
106    </div>
107    <br/>
108    <div th:replace="fragments/footer :: footer"></div>
109</body>
110</html>

Update the existing Controller = controller.TodoController

  • Remove all the RequestMappings(like @GetMapping, @PostMapping, @PutMapping, @DeleteMapping) from this class, since it is serving as only Intermediary Controller class.
 1@RestController
 2public class TodoController implements TodoApplicationConstants {
 3
 4    @Autowired
 5    private TodoService todoService;
 6
 7    public ResponseEntity<List<Tasks>> getAllTasks() {
 8        try {
 9            List<Tasks> tasksList = todoService.findAll();
10            return new ResponseEntity<>(tasksList, HttpStatus.OK);
11        } catch (Exception ex) {
12            return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
13        }
14    }
15
16    public ResponseEntity<Tasks> getTasksById(@PathVariable("id") long id) {
17        Optional<Tasks> task = todoService.findById(id);
18        if (task.isPresent()) {
19            return new ResponseEntity<>(task.get(), HttpStatus.OK);
20        } else {
21            return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
22        }
23    }
24
25    public ResponseEntity<HttpStatus> deleteTaskById(@PathVariable("id") long id) {
26        try {
27            if(todoService.deleteById(id))
28                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
29            else
30                return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
31        } catch (Exception ex) {
32            return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
33        }
34    }
35
36    public ResponseEntity<Tasks> updateTaskItem(@RequestBody Tasks task) {
37        task = todoService.createOrUpdate(task);
38        return new ResponseEntity<>(task, HttpStatus.OK);
39    }
40
41    public ResponseEntity<Tasks> createTodoTask(@RequestBody Tasks task) {
42        task = todoService.createOrUpdate(task);
43        return new ResponseEntity<>(task, HttpStatus.CREATED);
44    }
45
46    public ResponseEntity<List<String>> getTaskStatus() {
47        List<String> taskStatusList = todoService.getTodoStatusAsList();
48        return new ResponseEntity<>(taskStatusList, HttpStatus.OK);
49    }
50}

Create a new Controller Class = controller.TodoViewController

  • This is the Controller Class that will be receiving the requests from Thymeleaf template html files and hence it will have all the RequestMappings(like @GetMapping, @PostMapping, @PutMapping, @DeleteMapping).
 1@Controller
 2public class TodoViewController implements TodoApplicationConstants {
 3
 4    @Autowired
 5    private TodoController todoController;
 6
 7    @GetMapping
 8    public String gotoHome(ModelMap model) {
 9        model.put("tasksItemList", todoController.getAllTasks().getBody());
10        return "index";
11    }
12
13    @GetMapping("/singleItemView/{action}/{todoId}")
14    public String goToSingleItemPage(ModelMap model, @PathVariable("action") String action,
15                                           @PathVariable("todoId") Long todoId){
16        Tasks tasksItem = Tasks.builder().title("").description("").dueDate(null).status("").build();
17        if(todoId > 0) {
18            tasksItem = todoController.getTasksById(todoId).getBody();
19        }
20        model.put("tasksItem", tasksItem);
21        model.put("todoTaskComments", new TodoTaskComments());
22        model.put("action",action);
23        model.put("todoStatus",todoController.getTaskStatus().getBody());
24        return "singleItemPage";
25    }
26
27    @PostMapping(value = "/createOrUpdate", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
28    public String sendToCreate(Tasks tasksItem, TodoTaskComments todoTaskComments) {
29        if (tasksItem.getSystemTasksId() != null && tasksItem.getSystemTasksId() > 0 ) {
30            Set<TodoTaskComments> todoTaskCommentsSet = new HashSet<>();
31            todoTaskCommentsSet.add(todoTaskComments);
32            tasksItem.setTodoTaskCommentsSet(todoTaskCommentsSet);
33
34            todoController.updateTaskItem(tasksItem);
35        } else {
36            Tasks response = todoController.createTodoTask(tasksItem).getBody();
37        }
38        return "redirect:/";
39    }
40
41    @GetMapping("/deleteById/{id}")
42    public String sendToDeleteById(@PathVariable long id) {
43        todoController.deleteTaskById(id);
44        return "redirect:/";
45    }
46}

Let us look at some of the Highlights:

  • Line# 1: We are using @Controller annotation.
    • Usually for Rest Client APIs, we use @RestController.
    • However, in order to inform the Thymeleaf (Java Template Engine) to redirect the requests to Thymeleaf templates, we need to use @Controller.
    • If we use @RestController, since it is a combination of @Controller and @ResponseBody, whatever we have in the return statement, will be displayed on the UI.
  • Line# 10: Method returns the String value “index”.
    • Because we are using @Controller, Thymeleaf (Java Template Engine) will identify it as a call to the Thymeleaf template and tries to find the template with the name index.html in /src/main/resources/templates.
    • Since we have the file index.html at that location, it will render that file.

Running the Thymeleaf Spring Boot Project

With these changes, we are ready to run the project.

Run the server using the Configuration settings as shown here.

When we start the project and go to browser and type http://localhost:8080/todo-app/tasks, we will see the index.html in action.

todo-tracker-home-page.jpeg
Todo Tracker Home Page

  • When you click on Create Button

    todo-tracker-create-page.jpeg
    Todo Tracker Create Page

  • Enter the values and Click on the Submit Button in the Create Page

    todo-tracker-home-page-2.jpeg
    Todo Tracker Home Page After Create

  • Click on View Button to go to the below page

    todo-tracker-view-page.jpeg
    Todo Tracker View Page

  • On the Home page, click on the Edit Button to get the Edit Page, where you can update the existing contents or add new comment.

    todo-tracker-edit-page.jpeg
    Todo Tracker Edit Page

  • On the Home page, click on Delete Button and delete the task.

    todo-tracker-home-page.jpeg
    Todo Tracker Home Page After Delete


Conclusion

With this setup, we have come to an end of this article and we have seen how we can developing a Model-View-Controller (MVC) Application using Spring Boot, H2 Database and Thymeleaf (Java Template Engine).

Complete code for this project can be found at GitHub here. Go ahead and clone it.

Instructions on how to clone the code repository and run the project are provided on the GitHub project page.