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:
- org.springframework.boot:spring-boot-starter-web:2.1.5.RELEASE - Spring Boot web starter,
- org.springframework.boot:spring-boot-starter-data-jpa:2.1.5.RELEASE - Spring Boot data JPA starter,
- org.springframework.boot:spring-boot-starter-thymeleaf:2.1.5.RELEASE - Thymeleaf template engine,
- org.projectlombok:lombok:1.18.2 - generating setters/getters methods for POJO classes,
- com.h2database:h2:1.4.200 - H2 Database Engine,
- org.liquibase:liquibase-core:3.8.8 - managing and executing database changes,
- org.webjars:bootstrap:4.0.0-2 - webjar library for Bootstrap framework.
2.2. Front-End libraries
- bootstrap - a frontend framework for creating responsive websites.
Project Maven pom.xml
file has the following structure:
<?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:
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
.
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.
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:
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:
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:
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:
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:
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):
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:
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:
<!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 }}