1. Introduction
In this article, we are going to present Thymeleaf Pagination component based on the Bootstrap framework and Spring JPA. Dividing database results into pages is a common functionality used in many applications. This tutorial will show how to use the Thymeleaf template to create a nice pagination component.
For more useful informations about Thymeleaf and Bootstrap tables check the following links:
Thymeleaf Features
DataTable component with Thymeleaf
2. Dependencies
2.1. Maven dependencies
The POC (proof of concept) application was created as a Maven project using the following dependencies:
2.2. Front-End libraries
bootstrap - a frontend framework for creating responsive websites.
Project Maven pom.xml
file has 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-pagination</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>
<liquibase-core.version>3.8.8</liquibase-core.version>
<h2.version>1.4.200</h2.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.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>${liquibase-core.version}</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>${bootstrap.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.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, View, Controller layers
The sample application using H2 Database and Liquibase for managing database changes.
So, first we need to configure these libraries in our application.properties
file:
Copy
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1
spring.datasource.username=h2
spring.datasource.password=pass
spring.liquibase.change-log=classpath:/db-scripts/db-changelog-master.xml
In the database layer, we create a single entity Post
with fields like title
and body
.
Copy
package com.frontbackend.thymeleaf.bootstrap.posts.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity(name = "POSTS")
@NoArgsConstructor
@Setter
@Getter
public class Post {
@Id
private Long id;
private String title;
private String body;
}
From the Data Access Object (PostDAO
), that extends JpaRepository
, we will use the findAll
method that takes the Pageable
parameter. Pageable
class stores all information we need to limit and sort the result records.
Copy
package com.frontbackend.thymeleaf.bootstrap.posts.control.dao;
import org.springframework.data.jpa.repository.JpaRepository;
import com.frontbackend.thymeleaf.bootstrap.posts.entity.Post;
public interface PostDAO extends JpaRepository<Post, Long> {
}
PostService
class is responsible for preparing PageRequest
class and for wrapping the results with Paged
instance:
Copy
package com.frontbackend.thymeleaf.bootstrap.posts.control.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import com.frontbackend.thymeleaf.bootstrap.posts.control.dao.PostDAO;
import com.frontbackend.thymeleaf.bootstrap.posts.entity.Post;
import com.frontbackend.thymeleaf.bootstrap.posts.entity.paging.Paged;
import com.frontbackend.thymeleaf.bootstrap.posts.entity.paging.Paging;
@Service
public class PostService {
private final PostDAO postDAO;
@Autowired
public PostService(PostDAO postDAO) {
this.postDAO = postDAO;
}
public Paged<Post> getPage(int pageNumber, int size) {
PageRequest request = PageRequest.of(pageNumber - 1, size, new Sort(Sort.Direction.ASC, "id"));
Page<Post> postPage = postDAO.findAll(request);
return new Paged<>(postPage, Paging.of(postPage.getTotalPages(), pageNumber, size));
}
}
Paged
, PageItem
, PageItemType
and Paging
are utility classes used for preparing pagination component.
The Paged
class has the following structure:
Copy
package com.frontbackend.thymeleaf.bootstrap.posts.entity.paging;
import org.springframework.data.domain.Page;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Paged<T> {
private Page<T> page;
private Paging paging;
}
PageItem
has the following structure:
Copy
package com.frontbackend.thymeleaf.bootstrap.posts.entity.paging;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageItem {
private PageItemType pageItemType;
private int index;
private boolean active;
}
PageItemType
is an enum that holds two values:
Copy
package com.frontbackend.thymeleaf.bootstrap.posts.entity.paging;
public enum PageItemType {
DOTS,
PAGE
}
Paging
class is responsible for calculating how pages and dots will be displayed in the pagination component:
Copy
package com.frontbackend.thymeleaf.bootstrap.posts.entity.paging;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Paging {
private static final int PAGINATION_STEP = 3;
private boolean nextEnabled;
private boolean prevEnabled;
private int pageSize;
private int pageNumber;
private List<PageItem> items = new ArrayList<>();
public void addPageItems(int from, int to, int pageNumber) {
for (int i = from; i < to; i++) {
items.add(PageItem.builder()
.active(pageNumber != i)
.index(i)
.pageItemType(PageItemType.PAGE)
.build());
}
}
public void last(int pageSize) {
items.add(PageItem.builder()
.active(false)
.pageItemType(PageItemType.DOTS)
.build());
items.add(PageItem.builder()
.active(true)
.index(pageSize)
.pageItemType(PageItemType.PAGE)
.build());
}
public void first(int pageNumber) {
items.add(PageItem.builder()
.active(pageNumber != 1)
.index(1)
.pageItemType(PageItemType.PAGE)
.build());
items.add(PageItem.builder()
.active(false)
.pageItemType(PageItemType.DOTS)
.build());
}
public static Paging of(int totalPages, int pageNumber, int pageSize) {
Paging paging = new Paging();
paging.setPageSize(pageSize);
paging.setNextEnabled(pageNumber != totalPages);
paging.setPrevEnabled(pageNumber != 1);
paging.setPageNumber(pageNumber);
if (totalPages < PAGINATION_STEP * 2 + 6) {
paging.addPageItems(1, totalPages + 1, pageNumber);
} else if (pageNumber < PAGINATION_STEP * 2 + 1) {
paging.addPageItems(1, PAGINATION_STEP * 2 + 4, pageNumber);
paging.last(totalPages);
} else if (pageNumber > totalPages - PAGINATION_STEP * 2) {
paging.first(pageNumber);
paging.addPageItems(totalPages - PAGINATION_STEP * 2 - 2, totalPages + 1, pageNumber);
} else {
paging.first(pageNumber);
paging.addPageItems(pageNumber - PAGINATION_STEP, pageNumber + PAGINATION_STEP + 1, pageNumber);
paging.last(totalPages);
}
return paging;
}
}
GET requests are handled by the PostController
class. To change the current page and page size we need to add two request parameters:
pageNumber - current page number (default first page),
size - page size (default 5 rows):
Copy
package com.frontbackend.thymeleaf.bootstrap.posts.boundary;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.frontbackend.thymeleaf.bootstrap.posts.control.service.PostService;
@Controller
@RequestMapping("/")
public class PostController {
private final PostService postService;
@Autowired
public PostController(PostService postService) {
this.postService = postService;
}
@GetMapping
public String posts(@RequestParam(value = "pageNumber", required = false, defaultValue = "1") int pageNumber,
@RequestParam(value = "size", required = false, defaultValue = "5") int size, Model model) {
model.addAttribute("posts", postService.getPage(pageNumber, size));
return "posts";
}
}
The Application
is the Java class with the main method that starts the Spring Boot application server:
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);
}
}
4. Thymeleaf template
The presentation layer contains single Thymeleaf template posts.html
:
The posts.html
view has 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 Pagination</title>
<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 Pagination</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">
<table id="posts" class="table table-bordered table-responsive-sm">
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Body</th>
</tr>
</thead>
<tbody>
<tr th:each="post : ${posts.page}">
<td th:text="${post.id}">id</td>
<td th:text="${post.title}">title</td>
<td th:text="${post.body}">body</td>
</tr>
</tbody>
</table>
<nav aria-label="Page navigation" class="paging">
<ul class="pagination" th:if="${posts.page.totalPages > 1}">
<li class="page-item" th:classappend="${!posts.paging.isPrevEnabled()? 'disabled' : ''}">
<a class="page-link" th:href="@{'/?pageNumber=' + ${posts.paging.pageNumber - 1}}"
tabindex="-1">Previous</a>
</li>
<th:block th:each="item : ${posts.paging.getItems()}">
<li class="page-item" th:classappend="${item.index == posts.paging.pageNumber? 'active' : ''}"
th:if="${item.pageItemType.name() == 'PAGE'}">
<a class="page-link" th:href="@{'/?pageNumber=' + ${item.index}}"
th:text="${item.index}"></a>
</li>
<li class="page-item disabled" th:if="${item.pageItemType.name() == 'DOTS'}">
<a class="page-link" href="#">...</a>
</li>
</th:block>
<li class="page-item" th:classappend="${!posts.paging.isNextEnabled()? 'disabled' : ''}">
<a class="page-link" th:href="@{'/?pageNumber=' + ${posts.paging.pageNumber + 1}}">Next</a>
</li>
</ul>
</nav>
</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>
</body>
</html>
The pagination component is stored in the nav tag: <nav aria-label="Page navigation" class="paging">...</nav>
. This code could be easily moved to fragments but for the clarity of the sample application we keep it with table in a single file.
5. The output
The running application is available under http://locahost:8080
URL and presents the following functionality:
6. Conclusion
In this article, we presented how to build Thymeleaf Pagination component based on Spring JPA and Bootstrap framework. We used the Spring Boot application with H2 Memory Database , JPA , and Liquibase for creating database structure and fill it with sample 100 records.
As usual, the code used in this article is available under our GitHub repository .
{{ 'Comments (%count%)' | trans {count:count} }}
{{ 'Comments are closed.' | trans }}