Thymeleaf Pagination in Spring Boot with Bootstrap

In previous post, we’ve known how to build Spring Boot Thymeleaf example. Today I will continue to make Thymeleaf Pagination and Filter with Spring Data and Bootstrap.

Related Post:
– Rest API: Spring Boot Pagination & Filter example
Spring Boot Thymeleaf CRUD example
Thymeleaf Pagination and Sorting example
Spring Boot Sort/Order by multiple Columns | Spring Data JPA
Spring Boot @ControllerAdvice & @ExceptionHandler example
Spring Boot Pagination and Sorting example
Spring Boot Unit Test for JPA Repositiory
Spring Boot Unit Test for Rest Controller

More Practice:
Spring Boot Token based Authentication with Spring Security & JWT
– With MongoDB: Spring Boot MongoDB Pagination & Filter example with Spring Data


Spring Boot Thymeleaf Pagination example overview

One of the most important things to make a website friendly is the response time, and pagination comes for this reason. For example, this bezkoder.com website has hundreds of tutorials, and we don’t want to see all of them at once. Paging means displaying a small number of all, by a page.

Assume that we have tutorials table in database like this:

thymeleaf-pagination-spring-boot-database

Here are some url samples for pagination (with/without filter):

  • /api/tutorials?page=1&size=5
  • /api/tutorials?size=5: using default value for page
  • /api/tutorials?keyword=data&page=1&size=3: pagination & filter by title containing ‘data’

Read Tutorials with default page (1) and page size (3):

thymeleaf-pagination-spring-boot-default

Change page to 3:

thymeleaf-pagination-spring-boot-page-change

Click on Next, the page navigation will display like following:

thymeleaf-pagination-spring-boot-page-next

Click on >> for last page:

thymeleaf-pagination-spring-boot-page-last

You can change the page size:

thymeleaf-pagination-spring-boot-size-change

Or you can make pagination with filter/search by input keyword:

thymeleaf-pagination-spring-boot-filter

Pagination and Filter with Spring Data JPA

To help us deal with this situation, Spring Data JPA provides way to implement pagination with PagingAndSortingRepository.

PagingAndSortingRepository extends CrudRepository to provide additional methods to retrieve entities using the pagination 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

Spring Data Page

Let’s look at the Page object.

Page is a sub-interface of Slice with a couple of additional methods. It contains total amount of elements and total pages of the entire list.

public interface Page<T> extends Slice<T> {
  static <T> Page<T> empty();
  static <T> Page<T> empty(Pageable pageable);
  long getTotalElements();
  int getTotalPages();
  <U> Page<U> map(Function<? super T,? extends U> converter);
}

If the number of items increases, the performance could be affected, it’s the time you should think about Slice.

A Slice object knows less information than a Page, for example, whether the next one or previous one is available or not, or this slice is the first/last one. You can use it when you don’t need the total number of items and total pages.

public interface Slice<T> extends Streamable<T> {
  int getNumber();
  int getSize();
  int getNumberOfElements();
  List<T> getContent();
  boolean hasContent();
  Sort getSort();
  boolean isFirst();
  boolean isLast();
  boolean hasNext();
  boolean hasPrevious();
  ...
}

Spring Data Pageable

Now we’re gonna see 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 and the number of the page.

public interface Pageable {
  int getPageNumber();
  int getPageSize();
  long getOffset();
  Sort getSort();
  Pageable next();
  Pageable previousOrFirst();
  Pageable first();
  boolean hasPrevious();
  ...
}

So when we want to get pagination (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);
  • page: zero-based page index, must NOT be negative.
  • size: number of items in a page to be returned, must be greater than 0.

Create Thymeleaf Pagination 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 work well.

thymeleaf-pagination-spring-boot-project

Or you can get the new Github source code at the end of this tutorial.

Data Model

We have Tutorial entity like this:

entity/Tutorial.java

package com.bezkoder.spring.thymeleaf.pagination.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 Filter

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.pagination.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.pagination.entity.Tutorial;

@Repository
@Transactional
public interface TutorialRepository extends JpaRepository<Tutorial, Integer> {

  Page<Tutorial> findByTitleContainingIgnoreCase(String keyword, Pageable pageable);

  // ...
}

In the code above, we add pageable parameter with Spring Query Creation to find all Tutorials which title containing input string (case-insensitive).

You can find more supported keywords example inside method names at:
JPA Repository query example in Spring Boot

Custom query with @Query annotation:
Spring JPA @Query example: Custom query in Spring Boot

Controller for Pagination

Generally, in the HTTP request URLs, paging parameters are optional. So if our Rest API supports server side pagination, we should provide default values to make paging work even when Client does not specify these parameters.

controller/TutorialController.java

package com.bezkoder.spring.thymeleaf.pagination.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.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.pagination.entity.Tutorial;
import com.bezkoder.spring.thymeleaf.pagination.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 = "3") int size) {
    try {
      List<Tutorial> tutorials = new ArrayList<Tutorial>();
      Pageable paging = PageRequest.of(page - 1, size);

      Page<Tutorial> pageTuts;
      if (keyword == null) {
        pageTuts = tutorialRepository.findAll(paging);
      } else {
        pageTuts = tutorialRepository.findByTitleContainingIgnoreCase(keyword, paging);
        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);
    } 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. By default, 3 Tutorials will be fetched from database in page index 0 (1 for client side).

Next, we create a Pageable object with page & size.
Then check if the keyword parameter exists or not.

  • If it is null, we call Repository findAll(paging) with paging is the Pageable object above.
  • If Client sends request with keyword, use findByTitleContainingIgnoreCase(title, paging).

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.

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 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 and pagination --

  <div th:replace="fragments/footer :: footer"></div>
</body>

</html>

Thymeleaf Pagination Template

We will use Thymeleaf Fragments (th:fragment) to reuse common part for the page link.
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}}"
  th:title="${tooltip}" rel="tooltip">
  [[${label}]]
</a>

Now we modify Thymeleaf template tutorials.html, which displays the list of Tutorials with pagination based on a model attribute tutorials 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 scope="col">Id</th>
        <th scope="col">Title</th>
        <th scope="col">Description</th>
        <th scope="col">Level</th>
        <th scope="col">Published</th>
        <th scope="col">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 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>

<!-- 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 example

Run Spring Boot application with command: mvn spring-boot:run.

Conclusion

In this post, we have learned how to create Thymeleaf pagination for result 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 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 | Spring Boot

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
– Rest API: Spring Boot Pagination & Filter example
Thymeleaf Pagination and Sorting 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 Pagination and Sorting example

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