Reputation: 12459
I am developing a simple application using Spring + Thymeleaf. On one of the pages I have a list of items which needs to be paginated.
Ideally I would like to only send the currPageNo
(the number of the current page) and numOfPages
(the total number of pages) variables to the view and the rest of the work would be done in there (this is a presentation issue and has nothing to do with the business logic). If, however, the cleanest solution would require me to do some computation in the controller first, I would accept it as a small evil.
I would like to get the list of pages in the following form.
<Prev | 1 | ... | 3 | 4 | 5 | 6 | 7 | ... | 15 | Next>
I was only able to come with the following solution. It works but I believe that you will agree that it is very messy and really hard to read.
Moreover, in addition to currPageNo
and numOfPages
I had to send two more variables to the view. The ideal solution would not require me to do that.
firstPageNo = Math.max(2, currPageNo - 2)
lastPageNo = Math.min(numOfPages - 1, currPageNo + 2)
The current version of my code follows.
<ul>
<li th:if="${currPageNo > 1}">
<a th:href="@{/items.html(pageNo = ${currPageNo - 1})}" href="">< Prev</a>
</li>
<li th:class="${currPageNo == 1} ? 'selected'">
<a th:href="@{/items.html(pageNo = 1)}" th:if="${currPageNo > 1}" href="">1</a>
<span th:if="${currPageNo == 1}">1</span>
</li>
<li th:if="${currPageNo >= 5}">
...
</li>
<li th:each="pageNo : ${#numbers.sequence(firstPageNo, lastPageNo)}" th:class="${currPageNo == pageNo} ? 'selected'">
<a th:href="@{/items.html(pageNo = ${pageNo})}" th:if="${pageNo != currPageNo}" th:text="${pageNo}" href="">2</a>
<span th:if="${pageNo == currPageNo}" th:text="${pageNo}">2</span>
</li>
<li th:if="${currPageNo <= (numOfPages - 4)}">
...
</li>
<li th:class="${currPageNo == numOfPages} ? 'selected'">
<a th:href="@{/items.html(pageNo = ${numOfPages})}" th:if="${currPageNo < numOfPages}" th:text="${numOfPages}" href="">10</a>
<span th:if="${currPageNo == numOfPages}" th:text="${numOfPages}">1</span>
</li>
<li th:if="${currPageNo < numOfPages}">
<a th:href="@{/items.html(pageNo = ${currPageNo + 1})}" href=""> Next ></a>
</li>
</ul>
The following list sumarizes the issues that I would like to get rid of the most. I understand that some of them are inherent to the platform but still, the list seems to be quit long and the code messy.
firstPageNo
and lastPageNo
to the view from the controller.<
instead of <
in the expressions.I also welcome any other suggestions about how to improve the quality of the code.
I understand that perhaps this question would be a better fit for the Code Review site, but, as Thymeleaf seems to be a technology with a tiny user base yet, I expect a reasonable answer rather here on Stack Overflow, which has a much greater user base (I believe).
If, however, such a question is really not welcome here, please consider moving it to the right site instead of closing it so that I get the advice I need.
Upvotes: 16
Views: 43886
Reputation: 51
Here is a pagination build completely in thymeleaf, and read page info from the org.springframework.data.domain.Page
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<!-- Pagination Bar -->
<div th:fragment="paginationbar(page)" class="mt-5">
<!-- Define the Thymeleaf variable 'page' of type 'org.springframework.data.domain.Page' -->
<!--/*@thymesVar id="page" type="org.springframework.data.domain.Page"*/-->
<nav>
<ul class="pagination justify-content-center">
<!-- Previous Page Button -->
<li class="page-item" th:classappend="${page.hasPrevious()} ? '' : 'disabled'">
<!-- If there is no previous page, display a disabled span -->
<span th:if="${not page.hasPrevious()}" class="page-link">«</span>
<!-- If there is a previous page, display a clickable link to the previous page -->
<a th:if="${page.hasPrevious()}" th:href="${@urlBuilder.replaceQueryParam('page', page.previousPageable().getPageNumber()).toUriString()}" class="page-link" th:title="#{pagination.previous}">«</a>
</li>
<!-- Display First Page Button -->
<li class="page-item" th:classappend="${page.isFirst()} ? 'disabled' : ''" th:if="${page.number > 5}">
<!-- If it's the first page, display a disabled span -->
<span th:if="${page.isFirst()}" class="page-link">1</span>
<!-- If it's not the first page, display a clickable link to the first page -->
<a th:if="${not page.isFirst()}" th:href="${@urlBuilder.replaceQueryParam('page', 0).toUriString()}" class="page-link">1</a>
</li>
<!-- Display Ellipsis (between First Page and Current Page) -->
<li th:if="${page.number > 5}" class="page-item disabled">
<span class="page-link">...</span>
</li>
<!-- Display Pages Around the Current Page (5 pages before and after) -->
<li th:each="i : ${#numbers.sequence(T(java.lang.Math).max(1, page.number - 5), T(java.lang.Math).min(page.totalPages, page.number + 5))}" th:classappend="${page.number == i - 1} ? 'active' : ''">
<!-- If it's the current page, display a span with 'active' class -->
<span th:if="${page.number == i - 1}" class="page-link">[[${i}]]</span>
<!-- If it's not the current page, display a clickable link to that page -->
<a th:if="${page.number != i - 1}" th:href="${@urlBuilder.replaceQueryParam('page', i - 1).toUriString()}" class="page-link">[[${i}]]</a>
</li>
<!-- Display Ellipsis (between Current Page and Last Page) -->
<li th:if="${page.number < page.totalPages - 6}" class="page-item disabled">
<span class="page-link">...</span>
</li>
<!-- Display Last Page Button -->
<li class="page-item" th:classappend="${page.isLast()} ? 'disabled' : ''" th:if="${page.number < page.totalPages - 5}">
<!-- If it's the last page, display a disabled span -->
<span th:if="${page.isLast()}" class="page-link">[[${page.totalPages}]]</span>
<!-- If it's not the last page, display a clickable link to the last page -->
<a th:if="${not page.isLast()}" th:href="${@urlBuilder.replaceQueryParam('page', page.totalPages - 1).toUriString()}" class="page-link">[[${page.totalPages}]]</a>
</li>
<!-- Next Page Button -->
<li class="page-item" th:classappend="${page.hasNext()} ? '' : 'disabled'">
<!-- If there is no next page, display a disabled span -->
<span th:if="${not page.hasNext()}" class="page-link">»</span>
<!-- If there is a next page, display a clickable link to the next page -->
<a th:if="${page.hasNext()}" th:href="${@urlBuilder.replaceQueryParam('page', page.nextPageable().getPageNumber()).toUriString()}" class="page-link" th:title="#{pagination.next}">»</a>
</li>
</ul>
</nav>
</div>
</body>
</html>
Upvotes: 0
Reputation: 31
The following fragment can be used in any template in the project as it uses ServletUriComponentsBuilder to figure out URL for each page, no changes in controller required.
You only need to provide page number in your URL (/?page=X) and inject Page<myData> object as template variable (here used as "pagination"). Any additional query string parameters will be automatically appended to chosen page. The max variable defined as th:with="max=3" provides information how many pages should be shown before and after selected page.
Example screenshots:
<th:block th:fragment="paginationLinks(pagination)" th:with="urlBuilder=${T(org.springframework.web.servlet.support.ServletUriComponentsBuilder)}">
<nav aria-label="page navigation" th:if="${pagination.totalPages>1}" th:with="max=3">
<ul class="pagination justify-content-center">
<li class="page-item" th:if="${pagination.hasPrevious()}">
<a class="page-link"
th:href="@{${urlBuilder.fromCurrentRequest().replaceQueryParam('page', pagination.number - 1).toUriString()}}"><i class="mdi mdi-arrow-left"></i></a>
</li>
<li class="page-item" th:each="page: ${#numbers.sequence(0, pagination.totalPages-1)}"
th:if="${pagination.number} >= ${page - max} and ${pagination.number} <= ${page + max} or ${page} == ${pagination.totalPages - 1} or ${page} == 0"
th:classappend="${page} == ${pagination.number} ? 'active'">
<span class="page-link" th:if="${pagination.number} == ${page + max} and ${page != 0}">...</span>
<a class="page-link"
th:if="${pagination.number} > ${page - max} and ${pagination.number} < ${page + max} or ${page} == ${pagination.totalPages - 1} or ${page} == 0"
th:href="@{${urlBuilder.fromCurrentRequest().replaceQueryParam('page', page).toUriString()}}" th:text="${page+1}">1</a>
<span class="page-link" th:if="${pagination.number} == ${page - max} and ${page} != ${pagination.totalPages - 1}">...</span>
</li>
<li class="page-item" th:if="${pagination.hasNext()}">
<a class="page-link"
th:href="@{${urlBuilder.fromCurrentRequest().replaceQueryParam('page', pagination.number + 1).toUriString()}}"><i class="mdi mdi-arrow-right"></i></a>
</li>
</ul>
</nav>
</th:block>
Upvotes: 0
Reputation: 331
You can you use pageable interface
@GetMapping("/show-contacts/{page}") public String showContacts(@PathVariable("page") Integer page, Model model, Principal principal) {
model.addAttribute("title", "Show Contacts");
//have to send contacts list
String name = principal.getName();
User user = this.userRepository.getUserByUserName(name);
Pageable pageable = PageRequest.of(page, 5);
Page<Contact> contacts = this.contactRepository.findContactsByUser(user.getId(), pageable);
model.addAttribute("contacts", contacts);
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", contacts.getTotalPages());
return "user/show_contacts";
}
Thymleaf
1Upvotes: 0
Reputation: 83
Here is my solution. As a result, it will look like this
@GetMapping("/myPage")
public String main(
@PageableDefault(sort = {"id"}, direction = Sort.Direction.ASC, value = 100) Pageable pageable,
Model model) {
Page<myData> page;
page = boardgameService.findAll(pageable);
int[] body;
if (page.getTotalPages() > 7) {
int totalPages = page.getTotalPages();
int pageNumber = page.getNumber()+1;
int[] head = (pageNumber > 4) ? new int[]{1, -1} : new int[]{1,2,3};
int[] bodyBefore = (pageNumber > 4 && pageNumber < totalPages - 1) ? new int[]{pageNumber-2, pageNumber-1} : new int[]{};
int[] bodyCenter = (pageNumber > 3 && pageNumber < totalPages - 2) ? new int[]{pageNumber} : new int[]{};
int[] bodyAfter = (pageNumber > 2 && pageNumber < totalPages - 3) ? new int[]{pageNumber+1, pageNumber+2} : new int[]{};
int[] tail = (pageNumber < totalPages - 3) ? new int[]{-1, totalPages} : new int[] {totalPages-2, totalPages-1, totalPages};
body = ControllerUtils.merge(head, bodyBefore, bodyCenter, bodyAfter, tail);
} else {
body = new int[page.getTotalPages()];
for (int i = 0; i < page.getTotalPages(); i++) {
body[i] = 1+i;
}
}
model.addAttribute("body", body);
model.addAttribute("page", page);
return "myPage";
}
ControllerUtils method "merge":
public static int[] merge(int[]... intarrays) {
return Arrays.stream(intarrays).flatMapToInt(Arrays::stream)
.toArray();
}
And my pager.html
<div th:fragment="pager (pagination)">
<ul class="pagination">
<th:block th:each="pageNumber : ${body}">
<li th:if="${pageNumberStat.first}" class="page-item">
<a class="page-link" th:href="${url} + '?page='+ ${page.getNumber()} + '&size=' + ${page.getSize()}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<li class="page-item active" th:if="${pageNumber} == ${page.getNumber()+1}">
<a class="page-link" th:text="${pageNumber}" href="#"></a>
</li>
<li class="page-item disabled" th:if="${pageNumber} == -1">
<a class="page-link" href="#">...</a>
</li>
<li class="page-item" th:if="${pageNumber} != -1 and ${pageNumber} != ${page.getNumber()+1}">
<a class="page-link" th:text="${pageNumber}" th:href="${url} + '?page='+ ${pageNumber} + '&size=' + ${page.getSize()}"></a>
</li>
<li th:if="${pageNumberStat.last}" class="page-item" aria-label="Next">
<a class="page-link" th:href="${url} + '?page='+ ${page.getNumber() + 2} + '&size=' + ${page.getSize()}">
<span aria-hidden="true">»</span>
</a>
</li>
</th:block>
</ul>
I almost forgot the last moment. So that indexing does not start from 0, but from 1. In MvcConfig:
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver();
resolver.setOneIndexedParameters(true);
resolvers.add(resolver);
WebMvcConfigurer.super.addArgumentResolvers(resolvers);
}
Upvotes: 2
Reputation: 1097
I have this almost ready, hope it helps....
<div class="tag-box tag-box-v7 text-justify">
<div class="text-center">
<ul class="pagination" th:with="elementsperpage=2, blocksize=10, pages=${page2th.Number}/${elementsperpage}, wholepages=${format.format(pages)},
whole=(${page2th.Number}/${blocksize})+1, wholex=${format.format(whole)}, startnlockpage=${wholepages}*${blocksize+1}, endblockpage=${wholepages}*${blocksize+1},
startpage=${wholex-1}*${blocksize}, endpage=(${wholex}*${blocksize})+1">
<li>
<a th:if="${startpage gt 0}" th:href="@{${'/viewannouncements?p='}+${startpage}}"><<</a>
<a th:if="${startpage eq 0}" href="javascript:void(0);"><<</a>
</li>
<li th:each="pageNo : ${#numbers.sequence(endpage-11, (endpage lt page2th.TotalPages)? endpage-2 : page2th.TotalPages-1)}"
th:class="${page2th.Number eq pageNo}? 'active' : ''">
<a th:if="${page2th.Number eq pageNo}" href="javascript:void(0);">
<span th:text="${pageNo + 1}"></span>
</a>
<a th:if="${not (page2th.Number eq pageNo)}" th:href="@{${'/viewannouncements?p='}+${pageNo+1}}">
<span th:text="${pageNo + 1}"></span>
</a>
</li>
<li>
<a th:if="${(endpage*elementsperpage) le (page2th.TotalElements)}" th:href="@{${'/viewannouncements?p='}+${endpage}}">>></a>
<a th:if="${(endpage*elementsperpage) le (page2th.TotalElements)}" href="javascript:void(0);"></a>
</li>
</ul>
</div>
</div>
Upvotes: 0
Reputation: 211
Similar to solution described in http://www.javacodegeeks.com/2013/03/implement-bootstrap-pagination-with-spring-data-and-thymeleaf.html
but without using wrapper around Spring Pageable
<div class="table-pagination">
<ul class="pagination">
<li th:class="${contactsPage.number eq 0} ? 'disabled' : ''">
<a th:if="${not contactsPage.firstPage}" th:href="@{${'/contacts'}(page=${contactsPage.number-1},size=${contactsPage.size})}">Previous</a>
<a th:if="${contactsPage.firstPage}" href="javascript:void(0);">Previous</a>
</li>
<li th:each="pageNo : ${#numbers.sequence(0, contactsPage.totalPages - 1)}" th:class="${contactsPage.number eq pageNo}? 'active' : ''">
<a th:if="${contactsPage.number eq pageNo}" href="javascript:void(0);">
<span th:text="${pageNo + 1}"></span>
</a>
<a th:if="${not (contactsPage.number eq pageNo)}" th:href="@{${'/contacts'}(page=${pageNo},size=${contactsPage.size})}">
<span th:text="${pageNo + 1}"></span>
</a>
</li>
<li th:class="${contactsPage.number + 1 ge contactsPage.totalPages} ? 'disabled' : ''">
<a th:if="${not contactsPage.lastPage}" th:href="@{${'/contacts'}(page=${contactsPage.number+1},size=${contactsPage.size})}">Next</a>
<a th:if="${contactsPage.lastPage}" href="javascript:void(0);">Next</a>
</li>
</ul>
</div>
Upvotes: 21
Reputation: 153
Another option would be Ben Thurley's solution. We have implemented it and it's working smoothly: http://bthurley.wordpress.com/2012/07/18/spring-mvc-with-restful-datatables/
It lacks couples of items, like the filter argument for the search, but you can easily add via the PagingCriteria object and make sure to add it on the TableParamArgumentResolver.
public class TableParamArgumentResolver implements WebArgumentResolver {
private static final String S_ECHO = "sEcho";
private static final String I_DISPLAY_START = "iDisplayStart";
private static final String I_DISPLAY_LENGTH = "iDisplayLength";
private static final String I_SORTING_COLS = "iSortingCols";
private static final String I_SORT_COLS = "iSortCol_";
private static final String S_SORT_DIR = "sSortDir_";
private static final String S_DATA_PROP = "mDataProp_";
private static final String I_DATA_SEARCH = "sSearch";
public Object resolveArgument(MethodParameter param, NativeWebRequest request)
throws Exception {
TableParam tableParamAnnotation = param.getParameterAnnotation(TableParam.class);
if (tableParamAnnotation != null) {
HttpServletRequest httpRequest = (HttpServletRequest) request.getNativeRequest();
String sEcho = httpRequest.getParameter(S_ECHO);
String sDisplayStart = httpRequest.getParameter(I_DISPLAY_START);
String sDisplayLength = httpRequest.getParameter(I_DISPLAY_LENGTH);
String sSortingCols = httpRequest.getParameter(I_SORTING_COLS);
String sSearch = httpRequest.getParameter(I_DATA_SEARCH);
Integer iEcho = Integer.parseInt(sEcho);
Integer iDisplayStart = Integer.parseInt(sDisplayStart);
Integer iDisplayLength = Integer.parseInt(sDisplayLength);
Integer iSortingCols = Integer.parseInt(sSortingCols);
List<SortField> sortFields = new ArrayList<SortField>();
for (int colCount = 0; colCount < iSortingCols; colCount++) {
String sSortCol = httpRequest.getParameter(I_SORT_COLS + colCount);
String sSortDir = httpRequest.getParameter(S_SORT_DIR + colCount);
String sColName = httpRequest.getParameter(S_DATA_PROP + sSortCol);
sortFields.add(new SortField(sColName, sSortDir));
}
PagingCriteria pc = new PagingCriteria(iDisplayStart, iDisplayLength, iEcho, sortFields, sSearch);
return pc;
}
return WebArgumentResolver.UNRESOLVED;
}
}
Upvotes: 2