In previous post, we’ve known how to build Spring Boot Thymeleaf example. Today I will continue to make Thymeleaf Pagination and Sorting (by column header) with Spring Data and Bootstrap.
Related Post:
– Spring Boot Thymeleaf CRUD example
– Spring Boot Token based Authentication with Spring Security & JWT
– Spring Boot Sort/Order by multiple Columns
– Spring Boot @ControllerAdvice & @ExceptionHandler example
– Spring Boot Pagination and Sorting Rest API example
– Spring Boot Unit Test for JPA Repository
– Spring Boot Unit Test for Rest Controller
– Docker Compose: Spring Boot and MySQL example
Contents
- Thymeleaf Pagination and Sorting example
- Spring Data PagingAndSortingRepository
- Spring Data Sort and Order
- Bring Pagination and Sorting together
- Create Thymeleaf Pagination & Sorting Project
- Data Model
- Repository for Pagination and Sorting
- Controller with Pagination & Sorting
- Import Bootstrap
- Thymeleaf Pagination & Sorting Template
- Run the Spring Boot Thymepleaf Pagination and Sorting example
- Conclusion
- Source Code
- Further Reading
Thymeleaf Pagination and Sorting example
Assume that we have tutorials table in database like this:
Our Spring Boot Application will handle requests for Pagination and Sorting by Ascending or Descending. Here are some url samples (with/without filter):
/api/tutorials
pagination [page=0, size=6] (default) and sorting by [id, ascending] (default)/api/tutorials?sort=title,asc
pagination [page=0, size=6] (default) and sorting by [title, ascending]/api/tutorials?page=2&size=6&sort=title,desc
pagination [page=2, size=6] and sorting by [title, descending]/api/tutorials?keyword=data&page=1&size=6&sort=title,asc
pagination & filter by title containing ‘data’, sorting by [title, ascending]
Retrieve Tutorials with default Page (1), Size (6) and sorting by [id, ascending]:
Change Page and Size (Items per Page) and sorting by [id, ascending] (default):
Click on a table header (Level) to make Pagination and Sorting by Level column:
Continue to click on that table header (Level) for Sorting in Descending order:
Pagination and Sorting with Filtering:
Spring Data PagingAndSortingRepository
To help us deal with this situation, Spring Data JPA provides way to implement pagination and sorting with PagingAndSortingRepository.
PagingAndSortingRepository
extends CrudRepository to provide additional methods to retrieve entities using the sorting abstraction. So you can add a special Sort parameter to your query method.
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort);
}
findAll(Sort sort)
: returns a Iterable
of entities meeting the sorting condition provided by Sort
object.
You can also define more derived and custom query methods with additional Sort
parameter. For example, the following method returns List of Tutorials which title contains a given string:
List<Tutorial> findByTitleContaining(String title, Sort sort);
You can find more supported keywords inside method names here.
Let’s continue to explore Sort
class.
Spring Data Sort and Order
The Sort class provides sorting options for database queries with more flexibility in choosing single/multiple sort columns and directions (ascending/descending).
For example, we use by()
, descending()
, and()
methods to create Sort
object and pass it to Repository.findAll()
:
// order by 'level' column - ascending
List<Tutorial> tutorials =
tutorialRepository.findAll(Sort.by("level"));
// order by 'level' column, descending
List<Tutorial> tutorials =
tutorialRepository.findAll(Sort.by("level").descending());
// order by 'level' column - descending, then order by 'title' - ascending
List<Tutorial> tutorials =
tutorialRepository.findAll(Sort.by("level").descending().and(Sort.by("title")));
We can also create a new Sort
object with List of Order
objects.
List<Order> orders = new ArrayList<Order>();
Order order1 = new Order(Sort.Direction.DESC, "level");
orders.add(order1);
Order order2 = new Order(Sort.Direction.ASC, "title");
orders.add(order2);
List<Tutorial> tutorials = tutorialRepository.findAll(Sort.by(orders));
Bring Pagination and Sorting together
What if we want to do both sorting and paging the data?
CrudRepository
also provides additional methods to retrieve entities using the pagination/sorting abstraction.
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Page<T> findAll(Pageable pageable);
}
findAll(Pageable pageable)
: returns a Page
of entities meeting the paging condition provided by Pageable
object.
Spring Data also supports many useful Query Creation from method names that we’re gonna use to filter result in this example such as:
Page<Tutorial> findByPublished(boolean published, Pageable pageable);
Page<Tutorial> findByTitleContaining(String title, Pageable pageable);
You can find more supported keywords inside method names here.
For example: JPA Repository query example
To sort multiple fields with paging, please visit the tutorial:
Spring Data JPA Sort/Order by multiple Columns | Spring Boot
Let’s notice the Pageable parameter in Repository methods above. Spring Data infrastructure will recognizes this parameter automatically to apply pagination and sorting to database.
The Pageable
interface contains the information about the requested page such as the size, the number of the page, or sort information with Sort
object.
public interface Pageable {
int getPageNumber();
int getPageSize();
long getOffset();
Sort getSort();
Pageable next();
Pageable previousOrFirst();
Pageable first();
boolean hasPrevious();
...
}
So when we want to make paging and sorting (with or without filter) in the results, we just add Pageable
to the definition of the method as a parameter.
Page<Tutorial> findAll(Pageable pageable);
Page<Tutorial> findByPublished(boolean published, Pageable pageable);
Page<Tutorial> findByTitleContaining(String title, Pageable pageable);
This is how we create Pageable
objects using PageRequest class which implements Pageable
interface:
Pageable paging = PageRequest.of(page, size, sort);
page
: zero-based page index, must NOT be negative.size
: number of items in a page to be returned, must be greater than 0.sort
: theSort
object.
You can find more details about Pagination and Filter in this post:
Spring Boot Pagination & Filter example
Create Thymeleaf Pagination & Sorting Project
You can follow step by step, or get source code in this post:
Spring Boot Thymeleaf CRUD example
The Spring Project contains structure that we only need to add some changes to make the pagination & sorting work well.
Or you can get the new Github source code (including paging and sorting) at the end of this tutorial.
Data Model
This is the Tutorial entity that we’re gonna work:
model/Tutorial.java
package com.bezkoder.spring.thymeleaf.pagingsorting.entity;
import javax.persistence.*;
@Entity
@Table(name = "tutorials")
public class Tutorial {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@Column(length = 128, nullable = false)
private String title;
@Column(length = 256)
private String description;
@Column(nullable = false)
private int level;
@Column
private boolean published;
public Tutorial() {
}
public Tutorial(String title, String description, int level, boolean published) {
this.title = title;
this.description = description;
this.level = level;
this.published = published;
}
// getters and setters
}
Repository for Pagination and Sorting
Early in this tutorial, we know PagingAndSortingRepository
, but in this example, for keeping the continuity and taking advantage Spring Data JPA, we continue to use JpaRepository which extends PagingAndSortingRepository
interface.
repository/TutorialRepository.java
package com.bezkoder.spring.thymeleaf.pagingsorting.repository;
import javax.transaction.Transactional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import com.bezkoder.spring.thymeleaf.pagingsorting.entity.Tutorial;
@Repository
@Transactional
public interface TutorialRepository extends JpaRepository<Tutorial, Integer> {
Page<Tutorial> findByTitleContainingIgnoreCase(String keyword, Pageable pageable);
// ...
}
In the code above, we use add pageable
parameter with Spring Query Creation to find all Tutorials which title containing input string.
More Derived queries at:
JPA Repository query example in Spring Boot
Custom query with @Query
annotation:
Spring JPA @Query example: Custom query in Spring Boot
Controller with Pagination & Sorting
Generally, in the HTTP request URLs, paging & sorting parameters are optional. So we should provide default values to make pagination & sorting work even when UI View does not specify these parameters.
To get sort request parameters, we use @RequestParam String[] sort
with defaultValue="id,asc"
. We also need to convert "asc"
/"desc"
into Sort.Direction.ASC
/Sort.Direction.DES
for working with Sort.Order
class.
controller/TutorialController.java
package com.bezkoder.spring.thymeleaf.pagingsorting.controller;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.bezkoder.spring.thymeleaf.pagingsorting.entity.Tutorial;
import com.bezkoder.spring.thymeleaf.pagingsorting.repository.TutorialRepository;
@Controller
public class TutorialController {
@Autowired
private TutorialRepository tutorialRepository;
@GetMapping("/tutorials")
public String getAll(Model model, @RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "6") int size,
@RequestParam(defaultValue = "id,asc") String[] sort) {
try {
List<Tutorial> tutorials = new ArrayList<Tutorial>();
String sortField = sort[0];
String sortDirection = sort[1];
Direction direction = sortDirection.equals("desc") ? Sort.Direction.DESC : Sort.Direction.ASC;
Order order = new Order(direction, sortField);
Pageable pageable = PageRequest.of(page - 1, size, Sort.by(order));
Page<Tutorial> pageTuts;
if (keyword == null) {
pageTuts = tutorialRepository.findAll(pageable);
} else {
pageTuts = tutorialRepository.findByTitleContainingIgnoreCase(keyword, pageable);
model.addAttribute("keyword", keyword);
}
tutorials = pageTuts.getContent();
model.addAttribute("tutorials", tutorials);
model.addAttribute("currentPage", pageTuts.getNumber() + 1);
model.addAttribute("totalItems", pageTuts.getTotalElements());
model.addAttribute("totalPages", pageTuts.getTotalPages());
model.addAttribute("pageSize", size);
model.addAttribute("sortField", sortField);
model.addAttribute("sortDirection", sortDirection);
model.addAttribute("reverseSortDirection", sortDirection.equals("asc") ? "desc" : "asc");
} catch (Exception e) {
model.addAttribute("message", e.getMessage());
}
return "tutorials";
}
// other CRUD methods
}
In the code above, we accept paging parameters using @RequestParam
annotation for page
, size
, sort
. By default, 6
Tutorials will be fetched from database in page index 0
(page=1 on the UI View), order by id
(descending).
Next, we create a Pageable
object with page
, size
, sort
.
Then check if the title
parameter exists or not.
- If it is null, we call Repository
findAll(pageable)
withpageable
is thePageable
object above. - If Client sends request with
title
, usefindByTitleContainingIgnoreCase(title, pageable)
.
Both methods return a Page
object. We call:
getContent()
to retrieve the List of items in the page.getNumber()
for current Page.getTotalElements()
for total items stored in database.getTotalPages()
for number of total pages.
We also need to return some model attributes such as: pageSize
, sortField
, sortDirection
, reverseSortDirection
.
Import Bootstrap
Open pom.xml and add these dependencies:
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>4.6.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
Then open the HTML file that contain pagination bar, import Thymeleaf fragments, Bootstrap, jQuery and Font Awesome:
tutorials.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0" />
<title>BezKoder - Spring Boot Thymeleaf Pagination and Sorting example</title>
<link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/css/bootstrap.min.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/css/style.css}" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<script type="text/javascript" th:src="@{/webjars/jquery/jquery.min.js}"></script>
<script type="text/javascript" th:src="@{/webjars/bootstrap/js/bootstrap.min.js}"></script>
</head>
<body>
<div th:replace="fragments/header :: header"></div>
-- list, pagination and sorting--
<div th:replace="fragments/footer :: footer"></div>
</body>
</html>
Thymeleaf Pagination & Sorting Template
We will use Thymeleaf Fragments (th:fragment
) to reuse common part for the page link and table sort by column header.
Let’s write HTML code for them.
fragments/paging.html
<a th:fragment="paging(pageNum, label, tooltip)" class="page-link"
th:href="@{'/tutorials?' + ${keyword!=null && keyword!=''? 'keyword=' + keyword + '&' : ''}
+ 'page=' + ${pageNum} + '&size=' + ${pageSize}
+ ${sortField!=null ? '&sort=' + sortField + ',' + sortDirection : ''}}"
th:title="${tooltip}" rel="tooltip">
[[${label}]]
</a>
fragments/sorting.html
<th scope="col" th:fragment="sorting(field, label)">
<a th:href="@{'/tutorials?' + ${keyword!=null && keyword!=''? 'keyword=' + keyword + '&' : ''}
+ 'page=' + ${currentPage} + '&size=' + ${pageSize}
+ ${sortField!=null ? '&sort=' + field + ',' + (sortField == field ? reverseSortDirection : sortDirection) : ''}}">
[[${label}]] </a>
<span th:if="${sortField == field}"
th:class="${sortDirection == 'asc' ? 'fas fa-arrow-down-short-wide' : 'fas fa-arrow-down-wide-short'}"></span>
</th>
Now we modify Thymeleaf template tutorials.html, which displays the list of Tutorials with pagination and sorting based on a model attributes: tutorials
, pageSize
, sortField
, sortDirection
, reverseSortDirection
that are returned from TutorialController
class.
tutorials.html
<div th:if="${tutorials.size() > 0}">
<table class="table table-hover table-responsive-xl">
<thead class="thead-light">
<tr>
<th th:replace="fragments/sorting :: sorting('id','Id')"></th>
<th th:replace="fragments/sorting :: sorting('title','Title')"></th>
<th th:replace="fragments/sorting :: sorting('description','Description')"></th>
<th th:replace="fragments/sorting :: sorting('level','Level')"></th>
<th>Published</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="tutorial : ${tutorials}">
<th scope="row">[[${tutorial.id}]]</th>
<td>[[${tutorial.title}]]</td>
<td>[[${tutorial.description}]]</td>
<td>[[${tutorial.level}]]</td>
<td>
<a th:if="${tutorial.published == true}" class="fa-regular fa-square-check"
th:href="@{'/tutorials/' + ${tutorial.id} + '/published/false'}" title="Disable this tutorial"></a>
<a th:if="${tutorial.published == false}" class="fa-regular fa-square icon-dark"
th:href="@{'/tutorials/' + ${tutorial.id} + '/published/true'}" title="Enable this tutorial"></a>
</td>
<td>
<a th:href="@{'/tutorials/' + ${tutorial.id}}" title="Edit this tutorial"
class="fa-regular fa-pen-to-square icon-dark"></a>
<a th:href="@{'/tutorials/delete/' + ${tutorial.id}}" th:tutorialTitle="${tutorial.title}" id="btnDelete"
title="Delete this tutorial" class="fa-regular fa-trash-can icon-dark btn-delete"></a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="" th:unless="${tutorials.size() > 0}">
<span>No tutorials found!</span>
</div>
<nav aria-label="Pagination" th:if="${totalPages > 0}">
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${currentPage == 1} ? 'disabled'">
<a th:replace="fragments/paging :: paging(1, '<<', 'First Page')"></a>
</li>
<li class="page-item font-weight-bold" th:classappend="${currentPage == 1} ? 'disabled'">
<a th:replace="fragments/paging :: paging(${currentPage - 1}, 'Prev', 'Previous Page')"></a>
</li>
<li class="page-item disabled" th:if="${currentPage - 2 > 1}">
<a class="page-link" href="#">...</a>
</li>
<li class="page-item" th:classappend="${page == currentPage} ? 'active'"
th:each="page : ${#numbers.sequence(currentPage > 2 ? currentPage - 2 : 1, currentPage + 2 < totalPages ? currentPage + 2 : totalPages)}">
<a th:replace="fragments/paging :: paging(${page}, ${page}, 'Page ' + ${page})"></a>
</li>
<li class="page-item disabled" th:if="${currentPage + 2 < totalPages}">
<a class="page-link" href="#">...</a>
</li>
<li class="page-item font-weight-bold" th:classappend="${currentPage == totalPages} ? 'disabled'">
<a th:replace="fragments/paging :: paging(${currentPage + 1},'Next', 'Next Page')"></a>
</li>
<li class="page-item" th:classappend="${currentPage == totalPages} ? 'disabled'">
<a th:replace="fragments/paging :: paging(${totalPages}, '>>', 'Last Page')"></a>
</li>
</ul>
</nav>
Then we continue to modify Search Form with input Keyword and Page Size:
tutorials.html
<div>
<form th:action="@{/tutorials}" id="searchForm">
<div class="row d-flex">
<div class="col-md-6 mt-2">
<div class="search">
<i class="fa fa-search"></i>
<input id="keyword" type="search" name="keyword" th:value="${keyword}" required class="form-control"
placeholder="Enter keyword">
<button type="submit" class="btn btn-secondary">Search</button>
</div>
</div>
<div class="col-md-3 input-group mt-2">
<div class="input-group-prepend">
<label class="input-group-text" for="pageSize">Items per page:</label>
</div>
<select form="searchForm" name="size" th:value="${pageSize}" onchange="changePageSize()" class="size-select"
id="pageSize">
<option th:each="s : ${ {3, 6, 9} }" th:value="${s}" th:text="${s}" th:selected="${s == pageSize}"></option>
</select>
</div>
<div class="col-md-1 mt-2">
<button id="btnClear" class="btn btn-info">Clear</button>
</div>
</div>
</form>
</div>
<!-- sortable Table and Pagination Bar -->
<script type="text/javascript">
$(document).ready(function () {
// ...
$("#btnClear").on("click", function (e) {
e.preventDefault();
$("#keyword").text("");
window.location = "[[@{/tutorials}]]";
});
});
function changePageSize() {
$("#searchForm").submit();
}
</script>
Run the Spring Boot Thymepleaf Pagination and Sorting example
Run Spring Boot application with command: mvn spring-boot:run
.
Conclusion
In this post, we have learned how to make Thymeleaf pagination and sorting table by column header in Spring Boot example with Bootstrap and Spring Data JPA.
We also see that JpaRepository
supports a great way to make server side pagination and sorting with filter methods without need of boilerplate code.
More Derived queries at:
JPA Repository query example in Spring Boot
Custom query with @Query
annotation:
Spring JPA @Query example: Custom query in Spring Boot
To sort/order by multiple fields:
Spring Data JPA Sort/Order by multiple Columns
Handle Exception for this Rest APIs is necessary:
– Spring Boot @ControllerAdvice & @ExceptionHandler example
– @RestControllerAdvice example in Spring Boot
Or way to write Unit Test:
– Spring Boot Unit Test for JPA Repository
– Spring Boot Unit Test for Rest Controller
Happy learning! See you again.
Source Code
You can find the complete source code for this tutorial on Github.
Further Reading
– Spring Boot Thymeleaf CRUD example
– Spring Boot Pagination and Sorting REST API example
Fullstack CRUD App:
– Vue + Spring Boot example
– Angular 8 + Spring Boot example
– Angular 10 + Spring Boot example
– Angular 11 + Spring Boot example
– Angular 12 + Spring Boot example
– Angular 13 + Spring Boot example
– Angular 14 + Spring Boot example
– React + Spring Boot example
Fullstack Pagination:
– Spring Boot + React: Pagination example
– Spring Boot + Angular: Pagination example
More Practice:
– Spring Boot with Spring Security & JWT Authentication
– Spring Boot Rest XML example
– Spring Boot Multipart File upload example
– Spring Boot Sort/Order by multiple Columns
Associations:
– Spring Boot One To One example with JPA, Hibernate
– Spring Boot One To Many example with JPA, Hibernate
– Spring Boot Many to Many example with JPA, Hibernate