Thymeleaf re-usable pagination component- Spring MVC/Bootstrap

In this blog, we will discuss how we can create a reusable pagination component using Thymeleaf and Spring MVC/Data.

Goal is to create a pagination UI similar to this:

 

Controller 

Spring resolves the size and page parameter from URL : /users?size=20&page=1 to Pageable object at the Controller methods. We are returning Page<> instead of simple list. Page contains all the information that we need to create the pagination UI as above.
@GetMapping("/articles")
public String userHome(Model model, Pageable pageable) {
Page<Article> page= articleService.findAllByStatus(Status.PUBLISHED, pageable);
model.addAttribute("articles", page);
return "home-page";
}

@GetMapping("/users")
public String userHome(Model model, Pageable pageable) {
Page<User> page= userRepo.findAllByStatus(Status.ACTIVE, pageable);
model.addAttribute("users", page);
return "user-list";
}

 

Pagination at home-page.html   

<th:block th:each="article : ${articles}">
<!-- display article in a table row or card/box -->
</th:block>

<!--pager at bottom -->
<th:block th:replace="_fragments/_utils :: pagination(${articles})">
</th:block>

Pagination at user-list.html

<th:block th:each="user : ${users}">
<!-- display user in a table row or card/box -->
</th:block>
<!--pager at bottom -->
<th:block th:replace="_fragments/_utils :: pagination(${users})">
</th:block>

Reusable Fragment Definition at _fragments/_utils.html

Note that the pages are 0 based meaning page=0 equals the first page of data. You can make it to 1 based using the steps described here: https://ganeshtiwaridotcomdotnp.blogspot.com/2020/09/spring-pageable-start-index-with-one.html. You might also need to update the indices below to to handle that.

<th:block th:fragment="pagination(pagedObject)">

<!-- works with org.springframework.data.domain.Page<pagedObject>, -->

<div th:if="${pagedObject.getTotalPages() != 1}" class="form-group col-md-11 pagination-centered">
<ul class="pagination">
<!-- page number start with 0, totalPages returns actual number of pages -->
<li th:classappend="${pagedObject.getNumber() == 0} ? disabled" class="page-item">
<a class="page-link" th:href="@{''(size=${pagedObject.getSize()}, page=0)}">&laquo;</a>
</li>
<li th:classappend="${pagedObject.getNumber() == 0} ? disabled" class="page-item">
<a class="page-link"
th:href="@{''(size=${pagedObject.getSize()}, page=${pagedObject.getNumber() -1})}">&larr;</a>
</li>
<li th:classappend="${pagedObject.getNumber() == (page )} ? 'active pointer-disabled'"
th:each="page : ${#numbers.sequence(0, pagedObject.getTotalPages() -1)}" class="page-item">
<a class="page-link" th:href="@{''(size=${pagedObject.getSize()}, page=${page})}"
th:text="${page + 1}"></a>
</li>
<li th:classappend="${pagedObject.getNumber() + 1 == pagedObject.getTotalPages()} ? disabled"
class="page-item">
<a class="page-link"
th:href="@{''(size=${pagedObject.getSize()}, page=${pagedObject.getNumber() + 1})}">&rarr;</a>
</li>
<li th:classappend="${pagedObject.getNumber() + 1 == pagedObject.getTotalPages()} ? disabled"
class="page-item">
<a class="page-link"
th:href="@{''(size=${pagedObject.getSize()}, page=${pagedObject.getTotalPages()})}">&raquo;</a>
</li>
</ul>

<p class="text-muted small">
Showing page <span th:text="${pagedObject.getNumber() +1}"></span> of <span
th:text="${pagedObject.getTotalPages()}"></span>.
Total: <span th:text="${pagedObject.getTotalElements()}"></span>.

</p>
</div>


<div th:if="${pagedObject.getTotalElements() ==0}">
<div class="alert alert-warning" role="alert">
<span>No items available.</span>
</div>
</div>


</th:block>

Reference: https://github.com/gtiwari333/spring-boot-web-application-seed/blob/master/main-app/src/main/resources/templates/_fragments/_utils.html

Thymeleaf pass parameter to fragment

Making reusable fragment/UI Components in Thymeleaf

Thymeleaf supports creation of 'function like' mechanism and pass parameters to make the UI components reusable.

In this example we are going to create a reusable alert component using thymeleaf:

The fragment definition is similar to how a function/method is defined:

File: _fragments/_utils.html

<div th:fragment="alertBox(message,type)">
//do stuff with message and type
</div>

Let's call with some parameters in another file:

<div th:replace="_fragments/_utils::alertBox ('A message','success')">
</div>

The parameter order is not important if you do like this:

<div th:replace="_fragments/_utils::alertBox (type = 'success', message='A message' )">
</div>

A complete example:

<div th:fragment="alertBox (message,type)">
<div
th:classappend="|
${type == 'success' ? 'alert-success': ''}
${type == 'info' ? 'alert-info': ''}
${type == 'danger' ? 'alert-danger': ''} |"
class="alert alert-dismissible">
<a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
<span th:text="${message}"></span>
</div>
</div>


Calling it:


<div th:replace="_fragments/_utils :: alertBox ('Hello this is a success!','success')">
</div>

<div th:replace="_fragments/_utils :: alertBox ('Danger! danger! .......','danger')">
</div>


<div th:replace="_fragments/_utils :: alertBox ( type = 'success', message='Success message' )">
</div>


Output:







Spring pageable - start index with one

By default Spring uses 0 indexed page meaning a page number of 0 in the request equals the first page.

You can customize this to start from 1 in following by creating a bean of PageableHandlerMethodArgumentResolverCustomizer and setting the setOneIndexedParameters to true.

You can read  more about the method argument resolver and other customizations to pagable in my earlier blog post:  https://ganeshtiwaridotcomdotnp.blogspot.com/2020/09/spring-data-pagination-set-max-page.html


Configures whether to expose and assume 1-based page number indexes in the request parameters. When its true, a page number of 1 in the request will be considered the first page.

@Bean
public PageableHandlerMethodArgumentResolverCustomizer paginationCustomizer() {
return pageableResolver -> {
pageableResolver.setOneIndexedParameters(true); //default is false, starts with 0
};
}

Now you can have your pagination url to start with page=1 eg: /users?size=20&page=1.