Thymeleaf Pagination and Sorting example in Spring Boot

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 Repositiory
Spring Boot Unit Test for Rest Controller
Docker Compose: Spring Boot and MySQL example


Thymeleaf Pagination and Sorting example

Assume that we have tutorials table in database like this:

thymeleaf-pagination-sorting-example-database

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]:

thymeleaf-pagination-sorting-example-default

Change Page and Size (Items per Page) and sorting by [id, ascending] (default):

thymeleaf-pagination-sorting-example

Click on a table header (Level) to make Pagination and Sorting by Level column:

thymeleaf-pagination-sorting-example-

Continue to click on that table header (Level) for Sorting in Descending order:

thymeleaf-sorting-descending-order-example

Pagination and Sorting with Filtering:

thymeleaf-pagination-sorting-filtering-example

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: the Sort 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.

thymeleaf-pagination-sorting-example-spring-boot-project

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) with pageable is the Pageable object above.
  • If Client sends request with title, use findByTitleContainingIgnoreCase(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 Repositiory
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