How to Implement Server-side Pagination with Angular and Spring Boot in 3 Steps

In the previous article,  we have implemented a CRUD application. As part of the Read operation, we need to list all the available products from the database. If there are 100s of records in the database, then it is not advisable to fetch all of them at once and display them in the frontend. If we do so, then the performance of both frontend and backend will be impacted since all the fetched records will be placed in memory. The loading & rendering time also will be high based on the volume of data. Hence, instead of fetching all the data at once, we are gonna implement pagination in which we will fetch records page by page.

What You’ll Build

Angular Frontend

List Page

Angular Spring Boot Pagination

Spring Boot Backend

Implement the /api/products REST endpoint to return the paginated data.

What You’ll Need

Run the following checklist before you begin the implementation:

Angular Client Implementation

Install Angular Material

We are gonna use Angular Material to implement the pagination on the frontend. So lets install the angular material dependency with the following command

ng add @angular/material

It will ask you to

  • Select a material theme -> I have chosen Deep Purple/Amber theme since it suits well with the existing look & feel of the application
  • Set up global Angular Material typography styles -> Enter ‘N’ to skip it since we already have bootstrap style in place
  • Set up browser animations for Angular Material -> Enter ‘Y’ to set up browser animation. Importing the BrowserAnimationsModule into your application enables Angular’s animation system. Declining this will disable most of Angular Material’s animations.

Output

Skipping installation: Package already installed
? Choose a prebuilt theme name, or "custom" for a custom theme: (Use arrow keys)

> Indigo/Pink        [ Preview: https://material.angular.io?theme=indigo-pink ]

? Choose a prebuilt theme name, or "custom" for a custom theme:
  Indigo/Pink        [ Preview: https://material.angular.io?theme=indigo-pink ]

> Deep Purple/Amber  [ Preview: https://material.angular.io?theme=deeppurple-amb
? Choose a prebuilt theme name, or "custom" for a custom theme: Deep Purple/Amber  [ Preview: https://material.angular.io?theme=deeppurple-amber ]
? Set up global Angular Material typography styles? No
? Set up browser animations for Angular Material? Yes
UPDATE package.json (1325 bytes)
√ Packages installed successfully.
Two or more projects are using identical roots. Unable to determine project usin
g current working directory. Using default workspace project instead.
UPDATE src/app/app.module.ts (1996 bytes)
UPDATE angular.json (6815 bytes)
UPDATE src/index.html (1016 bytes)
UPDATE src/styles.css (182 bytes)

Modify Product List Component

list.component.ts

This component does the following:

  • Declares a totalElements field to hold the total no. of records available in the database.
  • The ngOnInit() method calls this.getProducts() method with page number “0” and size “5” as request parameters.
  • The getProducts() method calls the productService.getAll() method and assigns the data and total no. of records from the response to the products and totalElements fields respectively. In case of an error, the error message will be displayed in the page.
  • When the user interacts with the paginator, a PageEvent will be fired that can be used to update any associated data view. So, it binds the PageEvent to the nextPage() method. This method calls this.getProducts() method with page number and size from the PageEvent.
import { Component, OnInit } from '@angular/core';
import { ProductService } from '../_services/product.service';
import { Product } from './product';
import { PageEvent } from '@angular/material/paginator';
    
@Component({templateUrl: './list.component.html'})
export class ProductListComponent implements OnInit {
     
	products: Product[] = [];
	totalElements: number = 0;
   
	constructor(public productService: ProductService) {
	}
    
	ngOnInit(): void {
		this.getProducts({ page: "0", size: "5" });
	}
	 
	private getProducts(request) {
		this.productService.getAll(request)
		.subscribe(data => {
			this.products = data['content'];
			this.totalElements = data['totalElements'];
		}
		, error => {
			console.log(error.error.message);
		}
		);
	}
	 
	nextPage(event: PageEvent) {
		const request = {};
		request['page'] = event.pageIndex.toString();
		request['size'] = event.pageSize.toString();
		this.getProducts(request);
	}
	  
	deleteProduct(id:number){
		this.productService.delete(id)
		.subscribe(data => {
			this.products = this.products.filter(item => item.id !== id);
			console.log('Product deleted successfully!');
		}
		, error => {
			console.log(error.error.message);
		}
		);
	}
}

list.component.html

We are using the mat-paginator component from Angular Material for pagination. Each paginator instance requires:

  • The number of items per page (default set to 50)
  • The total number of items being paged
Page size options

The paginator displays a dropdown of page sizes for the user to choose from. The options for this dropdown can be set via pageSizeOptions. The current pageSize will always appear in the dropdown, even if it is not included in pageSizeOptions.

So we just need to include the mat-paginator with the required options as highlighted below.

<div class="container">
	<h5 class="text-center mt-3">PRODUCTS</h5>
	<div class="d-flex my-2">
		<a href="#" routerLink="/products/add" class="btn btn-outline-success ms-auto">Create New Product</a>
	</div>
	<div class="table-responsive">
		<table class="table table-bordered p-0 m-0">
			<thead>
				<tr>
					<th scope="col">#</th>
					<th scope="col">Name</th>
					<th scope="col">Description</th>
					<th scope="col">Version</th>
					<th scope="col">Edition</th>
					<th scope="col">Action</th>
				</tr>
			</thead>
			<tbody>
				<tr scope="row" *ngFor="let product of products">
					<td>{{ product.id }}</td>
					<td>{{ product.name }}</td>
					<td>{{ product.description}}</td>
					<td>{{ product.version }}</td>
					<td>{{ product.edition }}</td>
					<td><a href="#" [routerLink]="['/products/', product.id, 'view']" class="btn btn-info m-1">View</a> <a href="#"
							[routerLink]="['/products/', product.id, 'edit']" class="btn btn-primary m-1">Edit</a>
						<button type="button" (click)="deleteProduct(product.id)" class="btn btn-danger m-1">Delete</button></td>
				</tr>
				<tr scope="row" *ngIf="products.length === 0">
					<td colspan="6" class="text-center">No record found</td>
				</tr>
			</tbody>
		</table>
		<mat-paginator [pageSizeOptions]="[5, 10, 25, 100]" [pageSize]="5" [length]="totalElements" (page)="nextPage($event)"> </mat-paginator>
	</div>
</div>

Define Module

app.module.ts

Import and add MatPaginatorModule in the module declarations

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { MatPaginatorModule } from '@angular/material/paginator';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { BoardAdminComponent } from './board-admin/board-admin.component';
import { BoardModeratorComponent } from './board-moderator/board-moderator.component';
import { BoardUserComponent } from './board-user/board-user.component';
import { TotpComponent } from './totp/totp.component';
import { OrderComponent } from './order/order.component';
import { TokenComponent } from './register/token.component';
import { ProductListComponent } from './products/list.component';
import { ProductViewComponent } from './products/view.component';
import { ProductAddEditComponent } from './products/add-edit.component';

import { authInterceptorProviders } from './_helpers/auth.interceptor';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
    RegisterComponent,
    HomeComponent,
    ProfileComponent,
    BoardAdminComponent,
    BoardModeratorComponent,
    BoardUserComponent,
    TotpComponent,
    OrderComponent,
    TokenComponent,
	ProductListComponent,
    ProductViewComponent,
    ProductAddEditComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
	ReactiveFormsModule,
    HttpClientModule,
    MatPaginatorModule,
    BrowserAnimationsModule
  ],
  providers: [authInterceptorProviders],
  bootstrap: [AppComponent]
})
export class AppModule { }

Spring Boot Backend Implementation

In the previous tutorial, we have already implemented the /api/products REST endpoint to return the paginated data with the help of PagingAndSortingRepository interface provided by Spring Data JPA. Now, we are just gonna see the relevant code here.

The URL of the REST API with the paging parameters will be as shown below. If pageIndex and size is not present, then it will default to 0 and 3 respectively.

/api/products?page=1&size=2

Output

{
  "content": [
    {
      "id": 3,
      "deleted": false,
      "name": "Product 3",
      "version": "3.0",
      "edition": "2017",
      "description": "Product 3"
    },
    {
      "id": 4,
      "deleted": false,
      "name": "Product 4",
      "version": "4.0",
      "edition": "2018",
      "description": "Product 4"
    }
  ],
  "pageable": {
    "sort": {
      "unsorted": true,
      "sorted": false,
      "empty": true
    },
    "offset": 2,
    "pageNumber": 1,
    "pageSize": 2,
    "paged": true,
    "unpaged": false
  },
  "totalPages": 3,
  "totalElements": 6,
  "last": false,
  "size": 2,
  "number": 1,
  "sort": {
    "unsorted": true,
    "sorted": false,
    "empty": true
  },
  "numberOfElements": 2,
  "first": false,
  "empty": false
}

Spring Data Repository

ProductRepository.java

This repository extendPagingAndSortingRepository in order to get the findAll(Pageable pageable) and findAll(Sort sort) methods for paging and sorting.

The findAll(Pageable pageable) method by default returns a Page<T> object. A Page<T> instance, in addition to having the list of Products, also knows about the total number of available pages by triggering an additional count query.

package com.javachinna.repo;

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;

import com.javachinna.model.Product;

@Repository
public interface ProductRepository extends PagingAndSortingRepository<Product, Long> {

}

Product Service

ProductService.java

package com.javachinna.service;

import com.javachinna.dto.ProductRequest;
import com.javachinna.model.Product;

public interface ProductService extends Service<Product, ProductRequest> {

}

ProductServiceImpl.java

@Service
public class ProductServiceImpl implements ProductService {

	@Autowired
	private ProductRepository productRepository;
	
	@Override
	public Page<Product> findAll(Pageable pageable) {
		return productRepository.findAll(pageable);
	}
}

Product Controller

ProductController.java

The /api/products endpoint accepts the page number and page size as request parameters with default values 0 & 3 respectively. By passing in the requested page number and the page size, we will create a PageRequest object, which is an implementation of the Pageable interface and pass it to the repository method.

@RestController
@RequestMapping(path = "/api/products")
@RequiredArgsConstructor
public class ProductController {

	private final ProductService productService;

	@GetMapping
	public Page<Product> getProductList(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "3") int size) {
		Pageable paging = PageRequest.of(page, size);
		return productService.findAll(paging);
	}
}

Run Spring Boot App with Maven

You can run the application with mvn clean spring-boot:run and the REST API services can be accessed via http://localhost:8080

Run the Angular App

You can run this App with the below command and hit the URL http://localhost:8081/ in browser

ng serve --port 8081

Source Code

https://github.com/JavaChinna/angular-spring-boot-pagination

Conclusion

That’s all folks. In this article, we have implemented server-side pagination with Spring Data JPA and Angular Material in our CRUD application.

Thank you for reading.

Leave a Reply