Feb 21, 2013
Implement Bootstrap Pagination With SpringData And Thymeleaf
Published by Yuan Ji on Feb 21, 2013 at 5:50:00 AM | 5 Comments
Some of my thoughts and experiences.
Twitter Bootstrap has a very nice pagination UI, and here I will show you how to implement it with Spring Data Web pagination function and Thymeleaf conditional evaluation features.
Simple pagination inspired by Rdio, great for apps and search results. The large block is hard to miss, easily scalable, and provides large click areas.
The original source code to display pagination from the Bootstrap document is very simple:
<div class="pagination">
<ul>
<li><a href="#">Prev</a></li>
<li><a href="#">1</a></li>
<li><a href="#">2</a></li>
<li><a href="#">3</a></li>
<li><a href="#">4</a></li>
<li><a href="#">5</a></li>
<li><a href="#">Next</a></li>
</ul>
</div>
You can see this is just a mock up code, and to make it display page numbers dynamically with correct hyperlink URL, I need to do many changes to my existing code. So let's start from the bottom up: change domain layer first, then application service layer, followed by presentation layer, and finally the configuration to glue them together.
The only change in domain layer is at BlogPostRepository. Before it had a method to
retrieve a list of published BlogPost sorted by publishedTime:
public interface BlogPostRepository extends MongoRepository<BlogPost, String>{
...
List<BlogPost> findByPublishedIsTrueOrderByPublishedTimeDesc();
...
}
Now we need to get the paginated result list. With
Spring Data
Page,
we will return the Page<BlogPost> instead of List<BlogPost>, and pass the
Pageable
parameter:
public interface BlogPostRepository extends MongoRepository<BlogPost, String>{
...
Page<BlogPost> findByPublishedIsTrueOrderByPublishedTimeDesc(Pageable pageable);
...
}
The applicaiton service layer change is also very simple, just using the new function
from BlogPostRepository:
public interface BlogService {
...
Page<BlogPost> getAllPublishedPosts(Pageable pageable);
...
}
public class BlogServiceImpl implements BlogService {
...
private final BlogPostRepository blogPostRepository;
...
@Override
public Page<BlogPost> getAllPublishedPosts(Pageable pageable) {
Page<BlogPost> blogList = blogPostRepository.findByPublishedIsTrueOrderByPublishedTimeDesc(pageable);
return blogList;
}
...
}
The Spring Data Page interface has many nice functions to get current page number, get total pages, etc. But it still lacks ways to let me display only partial page ranges of total pagination. So I created an adapter class to wrap the Spring Data Page interface with additional features.
public class PageWrapper<T> {
public static final int MAX_PAGE_ITEM_DISPLAY = 5;
private Page<T> page;
private List<PageItem> items;
private int currentNumber;
private String url;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public PageWrapper(Page<T> page, String url){
this.page = page;
this.url = url;
items = new ArrayList<PageItem>();
currentNumber = page.getNumber() + 1; //start from 1 to match page.page
int start, size;
if (page.getTotalPages() <= MAX_PAGE_ITEM_DISPLAY){
start = 1;
size = page.getTotalPages();
} else {
if (currentNumber <= MAX_PAGE_ITEM_DISPLAY - MAX_PAGE_ITEM_DISPLAY/2){
start = 1;
size = MAX_PAGE_ITEM_DISPLAY;
} else if (currentNumber >= page.getTotalPages() - MAX_PAGE_ITEM_DISPLAY/2){
start = page.getTotalPages() - MAX_PAGE_ITEM_DISPLAY + 1;
size = MAX_PAGE_ITEM_DISPLAY;
} else {
start = currentNumber - MAX_PAGE_ITEM_DISPLAY/2;
size = MAX_PAGE_ITEM_DISPLAY;
}
}
for (int i = 0; i<size; i++){
items.add(new PageItem(start+i, (start+i)==currentNumber));
}
}
public List<PageItem> getItems(){
return items;
}
public int getNumber(){
return currentNumber;
}
public List<T> getContent(){
return page.getContent();
}
public int getSize(){
return page.getSize();
}
public int getTotalPages(){
return page.getTotalPages();
}
public boolean isFirstPage(){
return page.isFirstPage();
}
public boolean isLastPage(){
return page.isLastPage();
}
public boolean isHasPreviousPage(){
return page.hasPreviousPage();
}
public boolean isHasNextPage(){
return page.hasNextPage();
}
public class PageItem {
private int number;
private boolean current;
public PageItem(int number, boolean current){
this.number = number;
this.current = current;
}
public int getNumber(){
return this.number;
}
public boolean isCurrent(){
return this.current;
}
}
}
With this PageWrapper, we can wrap Page<BlogPost>
returned from BlogService
and put it into the SpringMVC UI model. See the controller code for the blog page:
@Controller
public class BlogController
...
@RequestMapping(value = "/blog", method = RequestMethod.GET)
public String blog(Model uiModel, Pageable pageable) {
PageWrapper<BlogPost> page = new PageWrapper<BlogPost>
(blogService.getAllPublishedPosts(pageable), "/blog");
uiModel.addAttribute("page", page);
return "blog";
}
...
}
The Pageable is passed in from
PageableArgumentResolver,
which I will explain later. Another trick is I also pass the view URL to PageWrapper,
and it can be used to construct Thymeleaf hyperlinks in pagination bar.
Since my PageWrapper is very generic, I created an html fragment for pagination bar,
so I can use it anywhere in my application pages when pagination needed. This fragment html
uses Thymeleaf th:if to dynamically
switch between static text or hyperlink based on whether the link is disabled or not. And it uses
th:href to construct URL with correct page number and page size.
<!-- Pagination Bar -->
<div th:fragment="paginationbar">
<div class="pagination pagination-centered">
<ul>
<li th:class="${page.firstPage}? 'disabled' : ''">
<span th:if="${page.firstPage}">← First</span>
<a th:if="${not page.firstPage}" th:href="@{${page.url}(page.page=1,page.size=${page.size})}">← First</a>
</li>
<li th:class="${page.hasPreviousPage}? '' : 'disabled'">
<span th:if="${not page.hasPreviousPage}">«</span>
<a th:if="${page.hasPreviousPage}" th:href="@{${page.url}(page.page=${page.number-1},page.size=${page.size})}" title="Go to previous page">«</a>
</li>
<li th:each="item : ${page.items}" th:class="${item.current}? 'active' : ''">
<span th:if="${item.current}" th:text="${item.number}">1</span>
<a th:if="${not item.current}" th:href="@{${page.url}(page.page=${item.number},page.size=${page.size})}"><span th:text="${item.number}">1</span></a>
</li>
<li th:class="${page.hasNextPage}? '' : 'disabled'">
<span th:if="${not page.hasNextPage}">»</span>
<a th:if="${page.hasNextPage}" th:href="@{${page.url}(page.page=${page.number+1},page.size=${page.size})}" title="Go to next page">»</a>
</li>
<li th:class="${page.lastPage}? 'disabled' : ''">
<span th:if="${page.lastPage}">Last →</span>
<a th:if="${not page.lastPage}" th:href="@{${page.url}(page.page=${page.totalPages},page.size=${page.size})}">Last →</a>
</li>
</ul>
</div>
</div>
The last step is to put them all together. Fortunately I did some research before I updated my code.
There is a very good blog post
Pagination with Spring MVC, Spring Data and Java Config by
Doug Haber. In his blog, Doug mentioned several gotchas, especially
when the Pageable parameter needs some configuration magic:
In order for Spring to know how to convert the parameter to a Pageable object you need to configure a HandlerMethodArgumentResolver. Spring Data provides a PageableArgumentResolver but it uses the old ArgumentResolver interface instead of the new (Spring 3.1) HandlerMethodArgumentResolver interface. The XML config can handle this discrepancy for us, but since we're using Java Config we have to do it a little more manually. Luckily this can be easily resolved if you know the right magic incantation... Doug Haber
With Doug's help, I added this argument resolver to my WebConfig class:
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.jiwhiz.blog.web")
public class WebConfig extends WebMvcConfigurerAdapter {
...
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
PageableArgumentResolver resolver = new PageableArgumentResolver();
resolver.setFallbackPagable(new PageRequest(1, 5));
argumentResolvers.add(new ServletWebArgumentResolverAdapter(resolver));
}
...
}
After all those changes, my Blog list will have a pagination bar at the top and bottom of the page, and it will always have at most 5 page numbers, with current page number in the middle and disabled. The pagination bar also has First and Previous links at the beginning, Next and Last links at the end. I also used it in my admin pages, for the user list and comment list, and it works very well.
Hi Oliver, thank you for your feedback.
The reason I use PageWrapper is to provide a list of page items (maximum 5 items) with page number, like 1 to 5, or 21 to 25, with current page number in the middle. So for example, currentNumber is 12, I want to display 10, 11, 12, 13, 14 in the pagination bar, with page 12 shown as current and disabled. In the PageWrapper constructor, you can see the algorithm to create list of PageItem objects. And I can use Thymeleaf to iterate through page items and build hyperlink URL with PageItem's number value.
This article was published at Java Code Geeks.
Proud to be part of Java Code Geeks Community.
hi Yuan, great example, Is there any to have the source downloadable. I know its your blog.. Thanks - le tme know.
Hi Chris,
Actually I just pushed the source code of my blog app into GitHub jiwhiz/JiwhizBlogWeb yesterday. I want to share with my friends first, then add more documents and open to public. You are welcome to be the early user of my open source project :)
You must sign in to post your comment.
Very nice blog post, Yuan!
Can you elaborate what you're actually shooting for with the
PageWrapper? The Page essentially is the snippet of data that you requested. Is it that you essentially want to be able to the item index in the context of the entire result? E.g. requesting new PageRequest(2, 10) to know that you've retrieved the 31st to the 40th element? What is currentNumber semantically?