1. Introduction
In this article, we are going to present Thymeleaf DataTable component embedded in a Spring Boot application. We used the Bootstrap DataTables library that allows us to add advanced interaction controls to the simple HTML tables.
If you are looking for some more information about how to configure Thymeleaf and how to create simple forms, check those links:
Spring Boot with Thymeleaf
2. Maven dependencies
Our example application will use several dependecies:
The Maven pom.xml
file will have the following structure:
Copy
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>thymeleaf-bootstrap-datatable</artifactId>
<properties>
<bootstrap.version>4.0.0-2</bootstrap.version>
<webjars-locator.version>0.30</webjars-locator.version>
<lombok.version>1.18.2</lombok.version>
</properties>
<!-- Inherit defaults from Spring Boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
</parent>
<!-- Add typical dependencies for a web application -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>${bootstrap.version}</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator</artifactId>
<version>${webjars-locator.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- Package as an executable jar -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3. Model, Web Controller, Rest Controller and Application class
We defined a single web controller called IndexController
, that simply serves index.html
page with our DataTable component. Additionally, we implement the rest controller EmployeeRestController
to handle all dynamic AJAX
requests that result from user interaction on the table element.
Model contains:
objects used for pagination, sorting and filtering DataTable: Column
,Direction
, Order
, Page
, PagingRequest
, Search
,
objects that store employee's data like names, salary, etc: Employee
, EmployeeComparators
- that one is used to sort the table by a specific column.
The Employee
class have the following structure:
Copy
package com.frontbackend.thymeleaf.bootstrap.model;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class Employee {
@JsonFormat(pattern = "yyyy/MM/dd")
@JsonProperty("start_date")
private Date startDate;
private Integer id;
private String position;
private String name;
private Double salary;
private String office;
private Integer extn;
}
Below we present model classes used for pagination.
PageRequest
is a structure that comes from DataTable component of every user interaction:
Copy
package com.frontbackend.thymeleaf.bootstrap.model.paging;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@NoArgsConstructor
public class PagingRequest {
private int start;
private int length;
private int draw;
private List<Order> order;
private List<Column> columns;
private Search search;
}
Order
class is used for sorting the results:
Copy
package com.frontbackend.thymeleaf.bootstrap.model.paging;
import lombok.*;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class Order {
private Integer column;
private Direction dir;
}
Direction
tell us about sorting order (ascending, descending):
Copy
package com.frontbackend.thymeleaf.bootstrap.model.paging;
public enum Direction {
asc,
desc;
}
Column
class is a representetion of the column in the DataTable component:
Copy
package com.frontbackend.thymeleaf.bootstrap.model.paging;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@NoArgsConstructor
public class Column {
private String data;
private String name;
private Boolean searchable;
private Boolean orderable;
private Search search;
public Column(String data) {
this.data = data;
}
}
Page
is a wrapper class that wraps the result employee list with metadata total records number:
Copy
package com.frontbackend.thymeleaf.bootstrap.model.paging;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
public class Page<T> {
public Page(List<T> data) {
this.data = data;
}
private List<T> data;
private int recordsFiltered;
private int recordsTotal;
private int draw;
}
The PageArray
object was introduced to support arrays
as a data source for DataTable component:
Copy
package com.frontbackend.thymeleaf.bootstrap.model.paging;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
public class PageArray {
private List<List<String>> data;
private int recordsFiltered;
private int recordsTotal;
private int draw;
}
Search
is used to filter the result list with the specific query:
Copy
package com.frontbackend.thymeleaf.bootstrap.model.paging;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@NoArgsConstructor
public class Search {
private String value;
private String regexp;
}
The web controller class is very simple it only servers index.html
website on the root context and /index
URI:
Copy
package com.frontbackend.thymeleaf.bootstrap.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping({ "/", "/index" })
public class IndexController {
@GetMapping
public String main() {
return "index";
}
}
EmployeeRestController
is a rest controller that will handle all requests from DataTable component:
Copy
package com.frontbackend.thymeleaf.bootstrap.controller;
import com.frontbackend.thymeleaf.bootstrap.model.paging.PageArray;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.frontbackend.thymeleaf.bootstrap.model.Employee;
import com.frontbackend.thymeleaf.bootstrap.model.paging.Page;
import com.frontbackend.thymeleaf.bootstrap.model.paging.PagingRequest;
import com.frontbackend.thymeleaf.bootstrap.service.EmployeeService;
@RestController
@RequestMapping("employees")
public class EmployeeRestController {
private final EmployeeService employeeService;
@Autowired
public EmployeeRestController(EmployeeService employeeService) {
this.employeeService = employeeService;
}
@PostMapping
public Page<Employee> list(@RequestBody PagingRequest pagingRequest) {
return employeeService.getEmployees(pagingRequest);
}
@PostMapping("/array")
public PageArray array(@RequestBody PagingRequest pagingRequest) {
return employeeService.getEmployeesArray(pagingRequest);
}
}
The main Spring Boot application class have the following structure:
Copy
package com.frontbackend.thymeleaf.bootstrap;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
The EmployeeService
is a service class with a separated logic related to parsing JSON file and preparing result data understandable by DataTable component:
Copy
package com.frontbackend.thymeleaf.bootstrap.service;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.frontbackend.thymeleaf.bootstrap.model.Employee;
import com.frontbackend.thymeleaf.bootstrap.model.EmployeeComparators;
import com.frontbackend.thymeleaf.bootstrap.model.paging.Column;
import com.frontbackend.thymeleaf.bootstrap.model.paging.Order;
import com.frontbackend.thymeleaf.bootstrap.model.paging.Page;
import com.frontbackend.thymeleaf.bootstrap.model.paging.PageArray;
import com.frontbackend.thymeleaf.bootstrap.model.paging.PagingRequest;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class EmployeeService {
private static final Comparator<Employee> EMPTY_COMPARATOR = (e1, e2) -> 0;
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
public PageArray getEmployeesArray(PagingRequest pagingRequest) {
pagingRequest.setColumns(Stream.of("name", "position", "office", "start_date", "salary")
.map(Column::new)
.collect(Collectors.toList()));
Page<Employee> employeePage = getEmployees(pagingRequest);
PageArray pageArray = new PageArray();
pageArray.setRecordsFiltered(employeePage.getRecordsFiltered());
pageArray.setRecordsTotal(employeePage.getRecordsTotal());
pageArray.setDraw(employeePage.getDraw());
pageArray.setData(employeePage.getData()
.stream()
.map(this::toStringList)
.collect(Collectors.toList()));
return pageArray;
}
private List<String> toStringList(Employee employee) {
return Arrays.asList(employee.getName(), employee.getPosition(), employee.getOffice(), sdf.format(employee.getStartDate()),
employee.getSalary()
.toString());
}
public Page<Employee> getEmployees(PagingRequest pagingRequest) {
ObjectMapper objectMapper = new ObjectMapper();
try {
List<Employee> employees = objectMapper.readValue(getClass().getClassLoader()
.getResourceAsStream("employees.json"),
new TypeReference<List<Employee>>() {
});
return getPage(employees, pagingRequest);
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return new Page<>();
}
private Page<Employee> getPage(List<Employee> employees, PagingRequest pagingRequest) {
List<Employee> filtered = employees.stream()
.sorted(sortEmployees(pagingRequest))
.filter(filterEmployees(pagingRequest))
.skip(pagingRequest.getStart())
.limit(pagingRequest.getLength())
.collect(Collectors.toList());
long count = employees.stream()
.filter(filterEmployees(pagingRequest))
.count();
Page<Employee> page = new Page<>(filtered);
page.setRecordsFiltered((int) count);
page.setRecordsTotal((int) count);
page.setDraw(pagingRequest.getDraw());
return page;
}
private Predicate<Employee> filterEmployees(PagingRequest pagingRequest) {
if (pagingRequest.getSearch() == null || StringUtils.isEmpty(pagingRequest.getSearch()
.getValue())) {
return employee -> true;
}
String value = pagingRequest.getSearch()
.getValue();
return employee -> employee.getName()
.toLowerCase()
.contains(value)
|| employee.getPosition()
.toLowerCase()
.contains(value)
|| employee.getOffice()
.toLowerCase()
.contains(value);
}
private Comparator<Employee> sortEmployees(PagingRequest pagingRequest) {
if (pagingRequest.getOrder() == null) {
return EMPTY_COMPARATOR;
}
try {
Order order = pagingRequest.getOrder()
.get(0);
int columnIndex = order.getColumn();
Column column = pagingRequest.getColumns()
.get(columnIndex);
Comparator<Employee> comparator = EmployeeComparators.getComparator(column.getData(), order.getDir());
if (comparator == null) {
return EMPTY_COMPARATOR;
}
return comparator;
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return EMPTY_COMPARATOR;
}
}
4. Templates
We have a single template index.html
with a sample DataTable component.
This index.html
file have the following structure:
Copy
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Spring Boot Thymeleaf Application - Bootstrap DataTable</title>
<link th:rel="stylesheet" th:href="@{assets/datatable/datatables.css}"/>
<link th:rel="stylesheet" th:href="@{webjars/bootstrap/4.0.0-2/css/bootstrap.min.css} "/>
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top">
<div class="container">
<a class="navbar-brand" href="/">Thymeleaf - Bootstrap DataTable</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive"
aria-controls="navbarResponsive"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
<li class="nav-item active">
<a class="nav-link" href="#">Home
<span class="sr-only">(current)</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">About</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Services</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Contact</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container">
<div class="row">
<div class="col-lg-10 mt-5 mb-5">
<h3>Datatable using list of Objects in data parameter</h3>
<table id="example" class="table table-bordered table-responsive" style="width: 100%">
<thead>
<tr>
<th>Name</th>
<th>Position</th>
<th>Office</th>
<th>Start date</th>
<th>Salary</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Name</th>
<th>Position</th>
<th>Office</th>
<th>Start date</th>
<th>Salary</th>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="row">
<div class="col-lg-10 mt-5 mb-5">
<h3>Datatable using list of Arrays in data parameter</h3>
<table id="exampleArray" class="table table-bordered table-responsive" style="width: 100%">
<thead>
<tr>
<th style="width:20%">Name</th>
<th style="width:20%">Position</th>
<th style="width:20%">Office</th>
<th style="width:20%">Start date</th>
<th style="width:20%">Salary</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Name</th>
<th>Position</th>
<th>Office</th>
<th>Start date</th>
<th>Salary</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<script th:src="@{/webjars/jquery/jquery.min.js}"></script>
<script th:src="@{/webjars/popper.js/umd/popper.min.js}"></script>
<script th:src="@{/webjars/bootstrap/js/bootstrap.min.js}"></script>
<script th:src="@{assets/datatable/datatables.js}"></script>
<script>
$('#example').DataTable({
"processing": true,
"serverSide": true,
"ajax": {
"url": "/employees",
"type": "POST",
"dataType": "json",
"contentType": "application/json",
"data": function (d) {
return JSON.stringify(d);
}
},
"columns": [
{"data": "name", "width": "20%"},
{"data": "position","width": "20%"},
{"data": "office", "width": "20%"},
{"data": "start_date", "width": "20%"},
{"data": "salary", "width": "20%"}
]
});
$('#exampleArray').DataTable({
"processing": true,
"serverSide": true,
"ajax": {
"url": "/employees/array",
"type": "POST",
"dataType": "json",
"contentType": "application/json",
"data": function (d) {
return JSON.stringify(d);
}
}
});
</script>
</body>
</html>
At the bottom of the page, we add DataTable configuration for object-based source data.
Copy
$('#example').DataTable({
"processing": true,
"serverSide": true,
"ajax": {
"url": "/employees",
"type": "POST",
"dataType": "json",
"contentType": "application/json",
"data": function (d) {
return JSON.stringify(d);
}
},
"columns": [
{"data": "name", "width": "20%"},
{"data": "position","width": "20%"},
{"data": "office", "width": "20%"},
{"data": "start_date", "width": "20%"},
{"data": "salary", "width": "20%"}
]
});
and array-based source data:
Copy
$('#exampleArray').DataTable({
"processing": true,
"serverSide": true,
"ajax": {
"url": "/employees/array",
"type": "POST",
"dataType": "json",
"contentType": "application/json",
"data": function (d) {
return JSON.stringify(d);
}
}
});
5. The output
Started application is available under http://locahost:8080
URL and presents the following functionality:
6. Conclusion
In this article, we showcased how to use Thymeleaf DataTable component in a Spring Boot application.
As usual, the code used in this example is available under our GitHub repo .
{{ 'Comments (%count%)' | trans {count:count} }}
{{ 'Comments are closed.' | trans }}