├── .gitignore ├── README.md ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── paginationdemo │ │ ├── Application.java │ │ ├── controller │ │ └── PersonController.java │ │ ├── domain │ │ ├── Pager.java │ │ └── Person.java │ │ ├── repository │ │ └── PersonRepository.java │ │ └── service │ │ ├── PersonService.java │ │ └── impl │ │ └── PersonServiceImpl.java └── resources │ ├── application.properties │ ├── data.sql │ ├── static │ ├── css │ │ └── style.css │ └── js │ │ └── app.js │ └── templates │ └── persons.html └── test └── java └── org └── paginationdemo └── ApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .classpath 3 | .project 4 | .settings/ 5 | /.idea/ 6 | *.iml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo for pagination with Thymeleaf template engine 2 | 3 | ## How to run 4 | 5 | Run `mvn install` in root directory 6 | 7 | Run `java -jar target/spring-thymeleaf-pagination-0.0.1-SNAPSHOT.jar` 8 | 9 | Go to http://localhost:8080 10 | 11 | Special thanks to [Bruno Raljic](https://github.com/brunoraljic) for implementation of algorithm that calculates button span. -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.paginationdemo 7 | spring-thymeleaf-pagination 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | spring-thymeleaf-pagination 12 | Thymeleaf pagination demo with Spring 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.3.1.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 15 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-data-jpa 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-thymeleaf 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-web 39 | 40 | 41 | org.webjars 42 | bootstrap 43 | 3.3.7-1 44 | 45 | 46 | 47 | com.h2database 48 | h2 49 | runtime 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-test 54 | test 55 | 56 | 57 | 58 | 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-maven-plugin 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/main/java/org/paginationdemo/Application.java: -------------------------------------------------------------------------------- 1 | package org.paginationdemo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/paginationdemo/controller/PersonController.java: -------------------------------------------------------------------------------- 1 | package org.paginationdemo.controller; 2 | 3 | import org.paginationdemo.domain.Pager; 4 | import org.paginationdemo.service.PersonService; 5 | import org.springframework.data.domain.PageRequest; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.servlet.ModelAndView; 10 | 11 | import java.util.Optional; 12 | 13 | /** 14 | * @author Branislav Lazic 15 | */ 16 | @Controller 17 | public class PersonController { 18 | 19 | private static final int BUTTONS_TO_SHOW = 5; 20 | private static final int INITIAL_PAGE = 0; 21 | private static final int INITIAL_PAGE_SIZE = 5; 22 | private static final int[] PAGE_SIZES = {5, 10, 20}; 23 | 24 | private final PersonService personService; 25 | 26 | public PersonController(PersonService studentService) { 27 | this.personService = studentService; 28 | } 29 | 30 | /** 31 | * Handles all requests 32 | * 33 | * @param pageSize - the size of the page 34 | * @param page - the page number 35 | * @return model and view 36 | */ 37 | @GetMapping("/") 38 | public ModelAndView showPersonsPage(@RequestParam("pageSize") Optional pageSize, 39 | @RequestParam("page") Optional page) { 40 | var modelAndView = new ModelAndView("persons"); 41 | 42 | // Evaluate page size. If requested parameter is null, return initial 43 | // page size 44 | int evalPageSize = pageSize.orElse(INITIAL_PAGE_SIZE); 45 | // If a requested parameter is null or less than 1, 46 | // return the initial size. Otherwise, return value of 47 | // param. decreased by 1. 48 | int evalPage = page.filter(p -> p >= 1) 49 | .map(p -> p - 1) 50 | .orElse(INITIAL_PAGE); 51 | 52 | var persons = personService.findAllPageable(PageRequest.of(evalPage, evalPageSize)); 53 | var pager = new Pager(persons.getTotalPages(), persons.getNumber(), BUTTONS_TO_SHOW); 54 | 55 | modelAndView.addObject("persons", persons); 56 | modelAndView.addObject("selectedPageSize", evalPageSize); 57 | modelAndView.addObject("pageSizes", PAGE_SIZES); 58 | modelAndView.addObject("pager", pager); 59 | return modelAndView; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/org/paginationdemo/domain/Pager.java: -------------------------------------------------------------------------------- 1 | package org.paginationdemo.domain; 2 | 3 | /** 4 | * Used to calculate span of buttons which will be displayed on a page 5 | * 6 | * @author Branislav Lazic 7 | * @author Bruno Raljic 8 | */ 9 | public class Pager { 10 | 11 | private int buttonsToShow = 5; 12 | 13 | private int startPage; 14 | 15 | private int endPage; 16 | 17 | public Pager(int totalPages, int currentPage, int buttonsToShow) { 18 | 19 | setButtonsToShow(buttonsToShow); 20 | 21 | int halfPagesToShow = getButtonsToShow() / 2; 22 | 23 | if (totalPages <= getButtonsToShow()) { 24 | setStartPage(1); 25 | setEndPage(totalPages); 26 | 27 | } else if (currentPage - halfPagesToShow <= 0) { 28 | setStartPage(1); 29 | setEndPage(getButtonsToShow()); 30 | 31 | } else if (currentPage + halfPagesToShow == totalPages) { 32 | setStartPage(currentPage - halfPagesToShow); 33 | setEndPage(totalPages); 34 | 35 | } else if (currentPage + halfPagesToShow > totalPages) { 36 | setStartPage(totalPages - getButtonsToShow() + 1); 37 | setEndPage(totalPages); 38 | 39 | } else { 40 | setStartPage(currentPage - halfPagesToShow); 41 | setEndPage(currentPage + halfPagesToShow); 42 | } 43 | 44 | } 45 | 46 | public int getButtonsToShow() { 47 | return buttonsToShow; 48 | } 49 | 50 | public void setButtonsToShow(int buttonsToShow) { 51 | if (buttonsToShow % 2 != 0) { 52 | this.buttonsToShow = buttonsToShow; 53 | } else { 54 | throw new IllegalArgumentException("Must be an odd value!"); 55 | } 56 | } 57 | 58 | public int getStartPage() { 59 | return startPage; 60 | } 61 | 62 | public void setStartPage(int startPage) { 63 | this.startPage = startPage; 64 | } 65 | 66 | public int getEndPage() { 67 | return endPage; 68 | } 69 | 70 | public void setEndPage(int endPage) { 71 | this.endPage = endPage; 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | return "Pager [startPage=" + startPage + ", endPage=" + endPage + "]"; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/org/paginationdemo/domain/Person.java: -------------------------------------------------------------------------------- 1 | package org.paginationdemo.domain; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.GeneratedValue; 5 | import javax.persistence.GenerationType; 6 | import javax.persistence.Id; 7 | 8 | @Entity 9 | public class Person { 10 | 11 | @Id 12 | @GeneratedValue(strategy = GenerationType.IDENTITY) 13 | private long personId; 14 | 15 | private String firstName; 16 | 17 | private String lastName; 18 | 19 | private int age; 20 | 21 | public Person() { 22 | } 23 | 24 | public Person(String firstName, String lastName, int age) { 25 | this.firstName = firstName; 26 | this.lastName = lastName; 27 | this.age = age; 28 | } 29 | 30 | public long getPersonId() { 31 | return personId; 32 | } 33 | 34 | public void setPersonId(long studentId) { 35 | this.personId = studentId; 36 | } 37 | 38 | public String getFirstName() { 39 | return firstName; 40 | } 41 | 42 | public void setFirstName(String firstName) { 43 | this.firstName = firstName; 44 | } 45 | 46 | public String getLastName() { 47 | return lastName; 48 | } 49 | 50 | public void setLastName(String lastName) { 51 | this.lastName = lastName; 52 | } 53 | 54 | public int getAge() { 55 | return age; 56 | } 57 | 58 | public void setAge(int age) { 59 | this.age = age; 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return "Person [personId=" + personId + ", firstName=" + firstName + ", lastName=" + lastName + ", age=" + age + "]"; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/org/paginationdemo/repository/PersonRepository.java: -------------------------------------------------------------------------------- 1 | package org.paginationdemo.repository; 2 | 3 | import org.paginationdemo.domain.Person; 4 | import org.springframework.data.repository.PagingAndSortingRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface PersonRepository extends PagingAndSortingRepository { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/paginationdemo/service/PersonService.java: -------------------------------------------------------------------------------- 1 | package org.paginationdemo.service; 2 | 3 | import org.paginationdemo.domain.Person; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | 7 | public interface PersonService { 8 | 9 | /** 10 | * Finds a "page" of persons 11 | * 12 | * @param pageable 13 | * @return {@link Page} instance 14 | */ 15 | Page findAllPageable(Pageable pageable); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/paginationdemo/service/impl/PersonServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.paginationdemo.service.impl; 2 | 3 | import org.paginationdemo.domain.Person; 4 | import org.paginationdemo.repository.PersonRepository; 5 | import org.paginationdemo.service.PersonService; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Service 11 | public class PersonServiceImpl implements PersonService { 12 | 13 | private final PersonRepository personRepository; 14 | 15 | public PersonServiceImpl(PersonRepository personRepository) { 16 | this.personRepository = personRepository; 17 | } 18 | 19 | @Override 20 | public Page findAllPageable(Pageable pageable) { 21 | return personRepository.findAll(pageable); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.thymeleaf.cache=false -------------------------------------------------------------------------------- /src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | -- test data generated by http://www.generatedata.com/ 2 | insert into person 3 | (first_name, last_name, age) 4 | values 5 | ('Aiko', 'Thompson', 23), 6 | ('Dorian', 'Carpenter', 61), 7 | ('Blythe', 'Harvey', 69), 8 | ('Lunea', 'Mcconnell', 17), 9 | ('Hector', 'Hopkins', 23), 10 | ('Darius', 'Brady', 54), 11 | ('Samuel', 'Kennedy', 88), 12 | ('Allegra', 'Anderson', 32), 13 | ('Mollie', 'Logan', 43), 14 | ('Belle', 'Francis', 65), 15 | ('Ivan', 'Nguyen', 55), 16 | ('Alden', 'Christensen', 22), 17 | ('Selma', 'Hendrix', 81), 18 | ('Pascale', 'Padilla', 44), 19 | ('Brody', 'Rosales', 44), 20 | ('Tucker', 'Simmons', 95), 21 | ('Zelenia', 'Slater', 63), 22 | ('Skyler', 'Bush', 17), 23 | ('Zephr', 'Alexander', 52), 24 | ('Myra', 'Mccarthy', 53), 25 | ('Ross', 'Hebert', 88), 26 | ('Craig', 'Fox', 54), 27 | ('Isaiah', 'Rich', 92), 28 | ('Myra', 'Small', 56), 29 | ('Sigourney', 'Briggs', 68), 30 | ('Christian', 'Pollard', 20), 31 | ('Aidan', 'Ramsey', 91), 32 | ('Anne', 'Johnston', 78), 33 | ('Sybil', 'Lara', 82), 34 | ('Astra', 'Burgess', 33), 35 | ('Harlan', 'Short', 76), 36 | ('Wyoming', 'Bruce', 44), 37 | ('Martena', 'Hewitt', 52), 38 | ('Gavin', 'Middleton', 99), 39 | ('Angelica', 'Mcfarland', 94), 40 | ('Rama', 'Bauer', 76), 41 | ('Ishmael', 'Lyons', 33), 42 | ('Laith', 'Todd', 31), 43 | ('Finn', 'Schultz', 59), 44 | ('Brock', 'Schroeder', 37), 45 | ('Britanney', 'Trujillo', 57), 46 | ('Evelyn', 'Mcneil', 90), 47 | ('Jada', 'Ray', 19), 48 | ('Abel', 'Watkins', 23), 49 | ('Leonard', 'Martin', 10), 50 | ('Karleigh', 'Velez', 26), 51 | ('Shoshana', 'Joyner', 49), 52 | ('Bryar', 'Bush', 83), 53 | ('Hayfa', 'Foster', 28), 54 | ('Shoshana', 'Giles', 66), 55 | ('Amos', 'Owen', 46), 56 | ('Erin', 'Slater', 36), 57 | ('Kalia', 'Mason', 73), 58 | ('Amber', 'Livingston', 62), 59 | ('Ralph', 'Mayo', 86), 60 | ('Brenden', 'Evans', 27), 61 | ('Noble', 'Mcclure', 75), 62 | ('Hiram', 'Holland', 20), 63 | ('Zachary', 'Diaz', 24), 64 | ('Nigel', 'Santos', 100), 65 | ('Dominic', 'Ingram', 60), 66 | ('Aline', 'Caldwell', 35), 67 | ('Mara', 'Clarke', 65), 68 | ('Cora', 'Tyson', 47), 69 | ('Avram', 'Compton', 87), 70 | ('Bruno', 'Tanner', 95), 71 | ('Roth', 'Sharpe', 18), 72 | ('Zelenia', 'Nicholson', 84), 73 | ('Kitra', 'Olson', 17), 74 | ('Ina', 'Knox', 14), 75 | ('Quin', 'Durham', 46), 76 | ('Noelani', 'Cruz', 32), 77 | ('Kiayada', 'Kerr', 20), 78 | ('Harrison', 'Maldonado', 18), 79 | ('Clark', 'Baird', 81), 80 | ('Nina', 'Roy', 20), 81 | ('Barry', 'William', 73), 82 | ('Samuel', 'Frye', 92), 83 | ('Kato', 'French', 90), 84 | ('Ursa', 'Heath', 31), 85 | ('Hollee', 'Fields', 79), 86 | ('Fulton', 'Pollard', 94), 87 | ('Brody', 'Fleming', 10), 88 | ('Brock', 'Garrison', 23), 89 | ('Lani', 'Fields', 20), 90 | ('Sybill', 'Hickman', 59), 91 | ('Todd', 'Goodwin', 14), 92 | ('Ezekiel', 'Solomon', 11), 93 | ('Karly', 'Juarez', 100), 94 | ('Portia', 'Grant', 31), 95 | ('Wang', 'Sherman', 52), 96 | ('Justine', 'Perkins', 25), 97 | ('Ryan', 'Stevenson', 85), 98 | ('Rogan', 'Johns', 50), 99 | ('Carson', 'West', 48), 100 | ('Ulysses', 'Garrison', 87), 101 | ('Chiquita', 'Kramer', 48), 102 | ('Dahlia', 'Conner', 73), 103 | ('Sara', 'Becker', 71), 104 | ('Plato', 'Craig', 17); -------------------------------------------------------------------------------- /src/main/resources/static/css/style.css: -------------------------------------------------------------------------------- 1 | .pagination-container { 2 | text-align: right; 3 | padding-right: 0; 4 | } 5 | 6 | .page-select { 7 | padding-left: 0; 8 | } 9 | 10 | .disabled { 11 | pointer-events: none; 12 | opacity: 0.5; 13 | } 14 | 15 | .pointer-disabled { 16 | pointer-events: none; 17 | } -------------------------------------------------------------------------------- /src/main/resources/static/js/app.js: -------------------------------------------------------------------------------- 1 | $(document).ready(() => { 2 | changePageAndSize(); 3 | }); 4 | 5 | changePageAndSize = () => { 6 | $('#pageSizeSelect').change(evt => { 7 | window.location.replace(`/?pageSize=${evt.target.value}&page=1`); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/templates/persons.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Persons 7 | 8 | 9 |
10 |
11 |

Persons

12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
First nameLast nameAge
26 |
27 |
28 |
29 | 33 |
34 |
35 |
    36 |
  • 37 | « 38 |
  • 39 |
  • 40 | 41 |
  • 42 |
  • 44 | 46 |
  • 47 |
  • 48 | 50 |
  • 51 |
  • 52 | » 54 |
  • 55 |
56 |
57 |
58 |
59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/test/java/org/paginationdemo/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package org.paginationdemo; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class ApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | --------------------------------------------------------------------------------