Bootstrap AJAX Spring Boot Pagination

Introduction

In this tutorial I will use Spring Data JPA Pageable to build REST API for pagination in Spring Boot applications. Here, I am going to show you how to build pagination using Bootstrap jQuery – AJAX with Spring Boot applications. Using jQuery – AJAX, you don’t need to load or fetch all records from server side, and you are going to fetch data only for the page which you want to display on the UI (User Interface).

Spring Data JPA Pageable is mainly useful when you have lots of data in backend and want to break into several pages while displaying on the UI (User Interface) instead of displaying all data in a single scrollable page. This pagination layout will give users better experience when they want to see less amount of data in a page without having to scroll down at bottom.

The Spring Data JPA provides a useful Pageable interface that provides a quite number of methods for building pagination in the UI side. Even you can sort the data returned by the JPA query using the methods provided in this interface API.

Related Posts:

I am going to build a pagination system that will have First, Previous, numeric paginations, Next and Last links. The number of links to be shown for the pagination is 10. You can change as per your requirements. The first, previous links will not be shown when you are on the page number one or first page. The next and last pagination links will not be shown when you are on the last page of the pagination.

pagination using pageable

In this example, I am not going to show you how to use Pageable interface to return paginated data. In the client side or UI side you can then easily build the pagination links to break the data into different pages. I will build a REST API to fetch data from MySQL database using Spring Data JPA.

Exposing REST API makes the application decoupled and the application can easily be integrated with other applications. So, on client side you can use any UI technology, such as, React, Angular, plain JavaScript, jQuery, Bootstrap, etc.

Prerequisites

Java 11/16 (1.8+), Maven 3.8.2, Spring Boot 2.5.6, MySQL 8.0.26, Bootstrap 4/5, jQuery 3.4.1 – 3.6.0

Backend – Spring Boot Application

Project Setup

Create a maven-based project in your favorite IDE or tool. The name of the project is spring-data-jpa-pageable. Even you can use the following pom.xml file to build your project.

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.roytuts</groupId>
	<artifactId>spring-data-jpa-pageable</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.source>11</maven.compiler.source>
		<maven.compiler.target>11</maven.compiler.target>
	</properties>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.5.6</version>
	</parent>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

		<!-- <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency> -->

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

Entity Class

A simple entity class that stores data into the table.

@Entity
@Table
public class Note {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Integer id;

	@Column
	private String note;
               // getters and setters
}

I have not specified table name in the annotation @Table, so the entity class name and table name will be same. I have also not specified column name in the annotation @Column, so the table’s column name and class’s attribute name are same. If you want to have different names then you have to specify the name.

Data JPA Repository Interface

Spring Data JPA framework provides number of built-in functions for performing basic CRUD operations when you extend JpaRepository or CrudRepository. I am also going to take advantage of one of the built-in functions to fetch paginated data.

public interface NoteRepository extends JpaRepository<Note, Integer> {

}

Service Class

Service class is the class where you generally process your business logic. In this class I am also fetching the paginated records from table by passing additional argument Pageable to the findAll() method.

@Service
public class NoteService {

	@Autowired
	private NoteRepository noteRepository;

	public Page<NoteDto> notes(Pageable pageable) {
		Page<Note> notes = noteRepository.findAll(pageable);

		Page<NoteDto> pages = notes.map(entity -> {
			NoteDto dto = EntityDtoConverter.entityToDto(entity);
			return dto;
		});

		return pages;
	}

}

In the above class, notice that I am returning the Page type instead of returning any list or collection of notes. Also, I am converting entity instance to DTO instance. It is not a good idea to return the entity class to the client side, which may expose your unintended data.

Entity DTO Converter Class

The following class that converts entity to DTO or DTO to entity class will be used as a decoupler between layers.

public final class EntityDtoConverter {

	private EntityDtoConverter() {
	}

	public static NoteDto entityToDto(Note note) {
		NoteDto noteDto = new NoteDto();

		noteDto.setId(note.getId());
		noteDto.setNote(note.getNote());

		return noteDto;
	}

	public static Note dtoToEntity(NoteDto noteDto) {
		Note note = new Note();

		note.setId(noteDto.getId());
		note.setNote(noteDto.getNote());

		return note;
	}

}

REST Controller

The following REST controller has one API exposed that will return data to the consumer.

@RestController
@CrossOrigin(origins = "*")
public class NoteRestController {

	@Autowired
	private NoteService noteService;

	@GetMapping("/notes")
	public Page<NoteDto> notes(Pageable pageable) {
		return noteService.notes(pageable);
	}

}

In the above REST API method, I have passed Pageable as an argument instead of taking page number, limit of records to be fetched from server, etc. as inputs. The Pageable interface will take care when you input the correct parameters from your client side requests.

Data Initializer

Let’s say you want to store hundreds or thousands of records in the table for testing purpose, but how would you insert so many records. Manually inserting so many records will be a too difficult task.

So, I have created an initializer class to insert 1000 thousand records in note table.

@Component
public class NoteInitializer {

	@Autowired
	private NoteRepository noteRepository;

	@PostConstruct
	private void loadNotes() {
		for (int i = 0; i < 1000; i++) {
			Note note = new Note();
			note.setNote("Note " + i);

			noteRepository.save(note);
		}
	}

}

The class has been defined as a Spring bean and I have specified @PostContruct annotation over the loadNotes() method so that it will be called during the bean initialization on application start up and data will be inserted into the note table. You can also use batch insertion mechanism instead of inserting one record at a time.

application.properties

The following datasource configuration is done in the src/main/resources/application.properties file:

spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/roytuts
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.show-sql = true
spring.jpa.properties.hibernate.format_sql=true

logging.level.org.hibernate.type.descriptor.sql=trace

spring.jpa.hibernate.ddl-auto = create
#spring.jpa.hibernate.ddl-auto = none

I have set the following properties so that Hibernate will create the note table in MySQL server during application start up.

spring.jpa.hibernate.ddl-auto=create

I am logging SQL statement and formatting them for better reading in the console using the following key/value pairs:

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.type.descriptor.sql=trace

Spring Boot Main Class

A class with a main method having class level annotation @SpringBootApplication will deploy the application into embedded Tomcat server.

@SpringBootApplication
@EntityScan(basePackages = "com.roytuts.spring.data.jpa.pageable.entity")
@EnableJpaRepositories(basePackages = "com.roytuts.spring.data.jpa.pageable.repository")
public class PageableApp {

	public static void main(String[] args) {
		SpringApplication.run(PageableApp.class, args);
	}

}

I have also let Spring container know in which packages my entity class and repository interface are available.

Testing the Spring Boot Pagination Application

Make sure your application is deployed successfully in embedded Tomcat server. The default port is 8080 at localhost.

Now when you access the URL http://localhost:8080/notes in the browser or REST client (for example, Postman), then you will see the following output:

bootstrap ajax spring boot pagination

Text version of the above output image:

{
    "content": [
        {
            "id": 1,
            "note": "Note 0"
        },
        {
            "id": 2,
            "note": "Note 1"
        },
        {
            "id": 3,
            "note": "Note 2"
        },
        {
            "id": 4,
            "note": "Note 3"
        },
        {
            "id": 5,
            "note": "Note 4"
        },
        {
            "id": 6,
            "note": "Note 5"
        },
        {
            "id": 7,
            "note": "Note 6"
        },
        {
            "id": 8,
            "note": "Note 7"
        },
        {
            "id": 9,
            "note": "Note 8"
        },
        {
            "id": 10,
            "note": "Note 9"
        },
        {
            "id": 11,
            "note": "Note 10"
        },
        {
            "id": 12,
            "note": "Note 11"
        },
        {
            "id": 13,
            "note": "Note 12"
        },
        {
            "id": 14,
            "note": "Note 13"
        },
        {
            "id": 15,
            "note": "Note 14"
        },
        {
            "id": 16,
            "note": "Note 15"
        },
        {
            "id": 17,
            "note": "Note 16"
        },
        {
            "id": 18,
            "note": "Note 17"
        },
        {
            "id": 19,
            "note": "Note 18"
        },
        {
            "id": 20,
            "note": "Note 19"
        }
    ],
    "pageable": {
        "sort": {
            "empty": true,
            "sorted": false,
            "unsorted": true
        },
        "offset": 0,
        "pageNumber": 0,
        "pageSize": 20,
        "paged": true,
        "unpaged": false
    },
    "last": false,
    "totalPages": 50,
    "totalElements": 1000,
    "number": 0,
    "size": 20,
    "sort": {
        "empty": true,
        "sorted": false,
        "unsorted": true
    },
    "first": true,
    "numberOfElements": 20,
    "empty": false
}

In the above output response, you will find two main keys – content and pageable. The content has the records coming from the database. The pageable has the values for pagination and sorting.

I am not doing any sorting, so unsorted becomes true.

For the pagination, you will find current page number (pageNumber), offset (offset), page size (pageSize), total pages (totalPages), total records (totalElements), etc.

The pageNumber indicates current page number which you see, the offset is from which position the records are to be fetched, pageSize indicates how many records you want to fetch, totalPages indicates total records (totalElements) available in the table divided by pageSize, totalElements indicates total number of available records in the note table.

You can change the page size and from which page number you want to fetch records. Let’s say you want to fetch record from page number 0 (start page is 0) and you want to fetch 5 records at a time for a page, then you can query using the following URL:

http://localhost:8080/notes?page=0&size=5

The output you will get from the above URL is:

bootstrap ajax pagination

Text version of the above output image:

{
    "content": [
        {
            "id": 1,
            "note": "Note 0"
        },
        {
            "id": 2,
            "note": "Note 1"
        },
        {
            "id": 3,
            "note": "Note 2"
        },
        {
            "id": 4,
            "note": "Note 3"
        },
        {
            "id": 5,
            "note": "Note 4"
        }
    ],
    "pageable": {
        "sort": {
            "empty": true,
            "sorted": false,
            "unsorted": true
        },
        "offset": 0,
        "pageNumber": 0,
        "pageSize": 5,
        "paged": true,
        "unpaged": false
    },
    "last": false,
    "totalPages": 200,
    "totalElements": 1000,
    "number": 0,
    "size": 5,
    "sort": {
        "empty": true,
        "sorted": false,
        "unsorted": true
    },
    "first": true,
    "numberOfElements": 5,
    "empty": false
}

Now you see the number of pages (totalPages) has been 200, because I am fetching 5 records instead of 20 records for each page.

Let’s say you want to fetch record for page 5: http://localhost:8080/notes?page=5&size=5

{
    "content": [
        {
            "id": 26,
            "note": "Note 25"
        },
        {
            "id": 27,
            "note": "Note 26"
        },
        {
            "id": 28,
            "note": "Note 27"
        },
        {
            "id": 29,
            "note": "Note 28"
        },
        {
            "id": 30,
            "note": "Note 29"
        }
    ],
    "pageable": {
        "sort": {
            "empty": true,
            "sorted": false,
            "unsorted": true
        },
        "offset": 25,
        "pageNumber": 5,
        "pageSize": 5,
        "paged": true,
        "unpaged": false
    },
    "last": false,
    "totalPages": 200,
    "totalElements": 1000,
    "number": 5,
    "size": 5,
    "sort": {
        "empty": true,
        "sorted": false,
        "unsorted": true
    },
    "first": false,
    "numberOfElements": 5,
    "empty": false
}

So, in the above output, you got records from page number 5.

Also notice that I have not used any query parameter in the REST API method but I am passing query parameter for fetching data for appropriate pages with limit.

Front End – Bootstrap AJAX jQuery

You can use either bootstrap 4 or bootstrap 5 according to your requirements, but the same code for pagination implementations will work with both versions of bootstrap.

Project Directory

As a first step I will create a project directory called bootstrap-ajax-springboot-pagination. This is the project root directory that will contain your whole source code required for the application.

I may not mention the project root directory in subsequent sections and I will assume that I am talking with respect to the project root directory.

Bootstrap jQuery AJAX Pagination

Now I will create HTML page to display data and JavaScript file to write jQuery code for implementing pagination using AJAX.

HTML Page

Here I am going to write the HTML page that has the following content. The following code is written into a file index.html.

<!DOCTYPE html>
<html lang="en">
<head>
	<title>Bootstrap Ajax SpringBoot Pagination</title>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1">

	<!--<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">-->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.2/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
</head>
<body>
	<div class="container">
		<div class="row">
			<div class="col-sm-8" style="background-color: #e6f9ff; margin: 10px; padding: 10px; border-radius: 5px; margin: auto;">
				<div class="alert alert-warning">
					<h3>Bootstrap + Ajax + SpringBoot Pagination</h3>
				</div>
	
				<table id="noteTable" class="table table-hover table-sm">
					<thead class="thead-dark">
						<tr>
							<th>Id</th>
							<th>Note</th>
						</tr>
					</thead>
					<tbody>
					</tbody>
				</table>
				
				<ul class="pagination justify-content-center" style="margin:20px 0; cursor: pointer;">
				</ul>
			</div>
		</div>
	</div>
	
	<!--<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>-->
	<script src="https://code.jquery.com/jquery-3.6.0.min.js" crossorigin="anonymous"></script>
	<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>-->
	<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.10.2/umd/popper.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
	<!--<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>-->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.2/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
	<script src="table-pagination.js"></script>
</body>
</html>

In the above HTML file, I have put only HTML table header and table body will be dynamically populated using jQuery code once data fetched from the server side.

I have also kept a placeholder for pagination using <ul> tag.

jQuery Pagination

Finally, I am going to load the records from server using AJAX into the table and build the pagination using jQuery. The following code is written into a file table-pagination.js.

$(document).ready(function() {
	let totalPages = 1;
	
	function fetchNotes(startPage) {
		//console.log('startPage: ' +startPage);
		/**
		 * get data from Backend's REST API
		 */
	    $.ajax({
	        type : "GET",
	        url : "http://localhost:8080/notes",
	        data: { 
	            page: startPage, 
	            size: 10
	        },
	        success: function(response){
	          $('#noteTable tbody').empty();
	          // add table rows
	          $.each(response.content, (i, note) => {
	            let noteRow = '<tr>' +
	      	  						'<td>' + note.id + '</td>' +
			                		'<td>' + note.note + '</td>' +
			                   '</tr>';
	            $('#noteTable tbody').append(noteRow);
	          });

	          if ($('ul.pagination li').length - 2 != response.totalPages){
	          	  // build pagination list at the first time loading
	        	  $('ul.pagination').empty();
	              buildPagination(response);
	          }
	        },
	        error : function(e) {
	          alert("ERROR: ", e);
	          console.log("ERROR: ", e);
	        }
	    });
	}
	
	function buildPagination(response) {
		totalPages = response.totalPages;

		var pageNumber = response.pageable.pageNumber;
		
		var numLinks = 10;
		
		// print 'previous' link only if not on page one
		var first = '';
		var prev = '';
		if (pageNumber > 0) {
			if(pageNumber !== 0) {
				first = '<li class="page-item"><a class="page-link">« First</a></li>';
			}
			prev = '<li class="page-item"><a class="page-link">‹ Prev</a></li>';
		} else {
			prev = ''; // on the page one, don't show 'previous' link
			first = ''; // nor 'first page' link
		}
		
		// print 'next' link only if not on the last page
		var next = '';
		var last = '';
		if (pageNumber < totalPages) {
			if(pageNumber !== totalPages - 1) {
				next = '<li class="page-item"><a class="page-link">Next ›</a></li>';				
				last = '<li class="page-item"><a class="page-link">Last »</a></li>';
			}
		} else {
			next = ''; // on the last page, don't show 'next' link
			last = ''; // nor 'last page' link
		}
		
		var start = pageNumber - (pageNumber % numLinks) + 1;
		var end = start + numLinks - 1;
		end = Math.min(totalPages, end);
		var pagingLink = '';
		
		for (var i = start; i <= end; i++) {
			if (i == pageNumber + 1) {
				pagingLink += '<li class="page-item active"><a class="page-link"> ' + i + ' </a></li>'; // no need to create a link to current page
			} else {
				pagingLink += '<li class="page-item"><a class="page-link"> ' + i + ' </a></li>';
			}
		}
		
		// return the page navigation link
		pagingLink = first + prev + pagingLink + next + last;
		
		$("ul.pagination").append(pagingLink);
	}
	
	$(document).on("click", "ul.pagination li a", function() {
        var data = $(this).attr('data');
		let val = $(this).text();
		console.log('val: ' + val);

		// click on the NEXT tag
		if(val.toUpperCase() === "« FIRST") {
			let currentActive = $("li.active");
			fetchNotes(0);
			$("li.active").removeClass("active");
	  		// add .active to next-pagination li
	  		currentActive.next().addClass("active");
		} else if(val.toUpperCase() === "LAST »") {
			fetchNotes(totalPages - 1);
			$("li.active").removeClass("active");
	  		// add .active to next-pagination li
	  		currentActive.next().addClass("active");
		} else if(val.toUpperCase() === "NEXT ›") {
	  		let activeValue = parseInt($("ul.pagination li.active").text());
	  		if(activeValue < totalPages){
	  			let currentActive = $("li.active");
				startPage = activeValue;
				fetchNotes(startPage);
	  			// remove .active class for the old li tag
	  			$("li.active").removeClass("active");
	  			// add .active to next-pagination li
	  			currentActive.next().addClass("active");
	  		}
	  	} else if(val.toUpperCase() === "‹ PREV") {
	  		let activeValue = parseInt($("ul.pagination li.active").text());
	  		if(activeValue > 1) {
	  			// get the previous page
				startPage = activeValue - 2;
				fetchNotes(startPage);
	  			let currentActive = $("li.active");
	  			currentActive.removeClass("active");
	  			// add .active to previous-pagination li
	  			currentActive.prev().addClass("active");
	  		}
	  	} else {
			startPage = parseInt(val - 1);
			fetchNotes(startPage);
	  		// add focus to the li tag
	  		$("li.active").removeClass("active");
	  		$(this).parent().addClass("active");
			//$(this).addClass("active");
	  	}
    });
	
	(function(){
    	// get first-page at initial time
    	fetchNotes(0);
    })();
});

The function fetchNotes() takes one parameter to decide which page to display.

On success function of $.ajax({…}), I am adding row data to the table and thereafter I am building the pagination using the function buildPagination().

I am showing 10 links on the page for the pagination to function along with First, Prev, Next and Last links.
Testing Bootstrap AJAX Pagination

Make sure your Spring Boot Pagination application is running.

Now you can open your index.html file to navigate through pagination links and verify the data.

Your home page will give you the following data as shown in the below image:

spring boot ajax bootstrap pagination

In the above image, notice the active page is highlighted and First and Prev links are not displayed.

Navigating to second page or any other page except the last page will display First and Prev links:

spring boot pagination

Navigating to the last page similarly will not display the Next and Last links for paginations:

spring boot pageable

Hope you got idea how to build pagination using Spring Boot jQuery AJAX Bootstrap.

Source Code

Backend – Spring Boot

Frontend – Bootstrap AJAX

Leave a Reply

Your email address will not be published. Required fields are marked *