Spring Boot Angular 7/8/9 Server Side Pagination Tutorial


Recently I had a to help a team add pagination in their application. The application was built using:

  1. Spring Boot 2.x
  2. Angular 7
  3. Spring Data JPA

In this blog I will quickly show how to add pagination to a Spring Boot 2.2.6 and Angular 9.1.1 application.

To quickly build the application we will use the spring-boot-maven-angular-starter project I wrote a couple of years. It is updated to the latest version of Spring Boot and Angular.

Let’s start by cloning the Github repository.

$ git clone git@github.com:shekhargulati/spring-boot-maven-angular-starter.git

You don’t have to type $. It signifies command prompt.

The code is divided into two folders.

  1. backend: This contains Java code of the application.
  2. ui: This contains source code for the Angular based frontend.

In this tutorial, we will build a simple todo application as an example.

Building the Todo application backend

Open the code in your favourite editor/IDE. I use IntelliJ Idea.

We will quickly build the backend of a simple Todo application. As we will use in-memory relational database H2 for storing the data we will add spring-boot-starter-data-jpa and h2 dependency in the backend/pom.xml in the dependencies section.

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

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

Now, we will write our entity class Todo

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "todos")
public class Todo {

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

  private String task;

  private boolean done;

  Todo() {
  }

  public Todo(String task) {
    this.task = task;
  }

  public void done() {
    this.done = true;
  }

  public void undone() {
    this.done = false;
  }

  public Long getId() {
    return id;
  }

  public String getTask() {
    return task;
  }

  public boolean isDone() {
    return done;
  }
}

The code shown above:

  1. Create a Todo entity class. We annotated it with JPA annotations.
  2. Todo has three fields id, task, and done that will be persisted.
  3. The class has two methods done and undone that provides behavior to marking a todo done or undone.

Next, we will create the repository interface TodoRepository

import com.shekhargulati.app.domain.Todo;
import org.springframework.data.repository.PagingAndSortingRepository;

public interface TodoRepository extends PagingAndSortingRepository<Todo, Long> {
}

TodoRepository interface extends PagingAndSortingRepository interface. This provides implementations for findAll methods that support pagination.

Next, we will create our REST API controller TodoResource

import static java.util.stream.Collectors.toList;

@RestController
@RequestMapping(path = "/todos")
public class TodoResource {

  private final TodoRepository todoRepository;

  @Autowired
  public TodoResource(TodoRepository todoRepository) {
    this.todoRepository = todoRepository;
  }

  @PostMapping
  public ResponseEntity<Void> create(@RequestBody TodoRequest todoRequest) {
    Todo saved = this.todoRepository.save(
      new Todo(todoRequest.getTask())
    );
    return ResponseEntity.status(HttpStatus.CREATED).build();
  }

  @PostMapping(path = "/bulk")
  public ResponseEntity<Void> bulkCreate() {
    for (int i = 1; i <= 100; i++) {
      todoRepository.save(new Todo("Do task " + i));
    }
    return ResponseEntity.status(HttpStatus.CREATED).build();
  }

  @PutMapping(path = "/{id}/done")
  public ResponseEntity<Void> done(@PathVariable("id") Long todoId) {
    Optional<Todo> found = this.todoRepository.findById(todoId);
    Todo task = found.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    task.done();
    todoRepository.save(task);
    return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
  }

  @PutMapping(path = "/{id}/undone")
  public ResponseEntity<Void> undone(@PathVariable("id") Long todoId) {
    Optional<Todo> found = this.todoRepository.findById(todoId);
    Todo task = found.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    task.undone();
    todoRepository.save(task);
    return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
  }

  @GetMapping
  public Page<TodoResponse> list(@RequestParam(name = "page", defaultValue = "0") int page,
                                 @RequestParam(name = "size", defaultValue = "10") int size) {
    PageRequest pageRequest = PageRequest.of(page, size);
    Page<Todo> pageResult = todoRepository.findAll(pageRequest);
    List<TodoResponse> todos = pageResult
      .stream()
      .map(TodoResponse::new)
      .collect(toList());

    return new PageImpl<>(todos, pageRequest, pageResult.getTotalElements());

  }
}

In the code shown above:

  1. We have five operations on the /todos endpoint.

  2. The POST methods creates the todo, two PATCHs are used to change task state from done to undone and vice-versa.

  3. Finally we have GET list method that return data in paginated manner. If user does not specify pagination parameters then defaults are assumed else we use what is provided in the request. We fetch the page data from the database using the findAll(Pageable) method on the repository interface. Once we have page of the data we transform the domain class to a DTO and return a new Page object.

Building the Angular frontend

We will build the simple Angular frontend for the listing tasks to showcase pagination. In Angular, you can add pagination in two ways:

  1. Using Angular Material DataTable
  2. Using normal table

Let’s start by adding Angular material dependency. In the ui folder run the following command.

$ ng add @angular/material

We will update our AppComponent to render two links that we can use to view both implementations.

Change the app.component.html to following.

<h1>Spring Boot Angular Pagination Example App</h1>
<nav>
  <ul>
    <li><a routerLink="/todos1" routerLinkActive="active">DataTable Pagination Example</a></li>
    <li><a routerLink="/todos2" routerLinkActive="active">Normal Table Example</a></li>
  </ul>
</nav>
<router-outlet></router-outlet>

Next, we will change the app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

}

We wil configure our app to use Angular Material components mainly MatTable and MatPaginator. To do that we replace app.module.ts with the following.

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

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {HttpClientModule} from '@angular/common/http';
import { MatTableModule } from '@angular/material/table';
import {  MatPaginatorModule } from '@angular/material/paginator';
import { Todo2Component } from './todo2/todo2.component';
import { Todo1Component } from './todo1/todo1.component';


@NgModule({
  declarations: [
    AppComponent,
    Todo1Component,
    Todo2Component
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    HttpClientModule,
    MatTableModule,
    MatPaginatorModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Next, we will generate two components todo1 and todo2

$ ng generate component todo1
$ ng generate component todo2

Now we will update app-routing.module.ts to do correct routing.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Todo2Component } from './todo2/todo2.component';
import { Todo1Component } from './todo1/todo1.component';


const routes: Routes = [
  {path : 'todos1', component: Todo1Component},
  {path : 'todos2', component: Todo2Component}
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

One last thing you should change is configure base API URL in the environment.ts

export const environment = {
  production: false,
  apiUrl: "http://localhost:8080/api"
};

If you run the frontend now you should two links that will take you to Todo1Component and Todo2Component.

01-angular-todo-routing

Pagination Solution 1: Pagination using Angular Material DataTable

In the first solution we will add pagination to Angular Material Data table using MatPaginator.

We will start by creating the table in todo1.component.html

<table mat-table [dataSource]="todoDatasource" class="mat-elevation-z8">

    <!--- Note that these columns can be defined in any order.
          The actual rendered columns are set as a property on the row definition" -->

    <ng-container matColumnDef="id">
      <th mat-header-cell *matHeaderCellDef> Id </th>
      <td mat-cell *matCellDef="let element"> {{element.id}} </td>
    </ng-container>

    <!-- Name Column -->
    <ng-container matColumnDef="task">
      <th mat-header-cell *matHeaderCellDef> Task </th>
      <td mat-cell *matCellDef="let element"> {{element.task}} </td>
    </ng-container>

    <!-- Weight Column -->
    <ng-container matColumnDef="done">
      <th mat-header-cell *matHeaderCellDef> Done </th>
      <td mat-cell *matCellDef="let element"> {{element.done}} </td>
    </ng-container>

    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  </table>
  <mat-paginator [pageSizeOptions]="[10, 25, 100]" [pageSize]="10"></mat-paginator>

The above uses todoDatasource that will be responsible for fetching the data from the REST API

Next, we will update our todo1.component.ts to fetch the data and manage pagination.

import { Component, ViewChild } from '@angular/core';
import { TodoDataSource } from '../todo.datasource';
import { MatPaginator } from '@angular/material/paginator';
import { TodoService } from '../todo.service';
import { tap } from 'rxjs/operators';

@Component({
  selector: 'app-todo2',
  templateUrl: './todo1.component.html',
  styleUrls: ['./todo1.component.css']
})
export class Todo1Component {

  displayedColumns = ['id', 'task', 'done'];
  todoDatasource: TodoDataSource;
  @ViewChild(MatPaginator) paginator: MatPaginator;

  constructor(private todoService: TodoService) { }

  ngOnInit() {
    this.todoDatasource = new TodoDataSource(this.todoService);
    this.todoDatasource.loadTodos();
  }

  ngAfterViewInit() {
    this.todoDatasource.counter$
      .pipe(
        tap((count) => {
          this.paginator.length = count;
        })
      )
      .subscribe();

    this.paginator.page
      .pipe(
        tap(() => this.loadTodos())
      )
      .subscribe();
  }

  loadTodos() {
    this.todoDatasource.loadTodos(this.paginator.pageIndex, this.paginator.pageSize);
  }


}

The class above uses TodoDataSource that will call the todo.service.ts to fetch list of the todo items page by page.

import { DataSource } from '@angular/cdk/table';
import { TodoListResponse, Todo } from './todo';
import { CollectionViewer } from '@angular/cdk/collections';
import { Observable, BehaviorSubject, of } from "rxjs";
import { TodoService } from './todo.service';
import { catchError, finalize } from "rxjs/operators";

export class TodoDataSource implements DataSource<Todo>{

    private todoSubject = new BehaviorSubject<Todo[]>([]);
    private loadingSubject = new BehaviorSubject<boolean>(false);
    private countSubject = new BehaviorSubject<number>(0);
    public counter$ = this.countSubject.asObservable();

    constructor(private todoService: TodoService) { }

    connect(collectionViewer: CollectionViewer): Observable<Todo[]> {
        return this.todoSubject.asObservable();
    }

    disconnect(collectionViewer: CollectionViewer): void {
        this.todoSubject.complete();
        this.loadingSubject.complete();
        this.countSubject.complete();
    }

    loadTodos(pageNumber = 0, pageSize = 10) {
        this.loadingSubject.next(true);
        this.todoService.listTodos({ page: pageNumber, size: pageSize })
            .pipe(
                catchError(() => of([])),
                finalize(() => this.loadingSubject.next(false))
            )
            .subscribe((result: TodoListResponse) => {
                this.todoSubject.next(result.content);
                this.countSubject.next(result.totalElements);
            }
            );
    }

}

The todo.service.ts is shown below.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class TodoService {

  constructor(private http: HttpClient) { }

  listTodos(request) {
    const endpoint = environment.apiUrl + "/todos";
    const params = request;
    return this.http.get(endpoint, { params });
  }
}

The TodoListResponse.ts is shown below.

export interface TodoListResponse {
    content: Todo[];
    totalElements: number;
}
export interface Todo{
    id: number;
    task: string;
    done: boolean
}

If you run the app now you will see data fetched from the backend. Please make sure backend is up and running.

02-datatable-pagination

When you click next you will see page by page data fetched from the backend.

03-pagination-network-calls

You can also read about the detailed explanation in this post by Angular University.

Pagination Solution 2: Using normal table

We can use MatPaginator with the normal table as well.

Update the todo2.component.html with the following code.

<div>
    <table class="mat-table">
        <thead>
            <tr class="mat-header-row">
                <th class='mat-header-cell'>Id</th>
                <th class='mat-header-cell'>Task</th>
                <th class='mat-header-cell'>Done</th>
            </tr>
        </thead>
        <tbody>
            <tr class="mat-row" *ngFor="let todo of todos">
                <td class="mat-cell">{{todo.id}}</td>
                <td class="mat-cell">{{todo.task}}</td>
                <td class="mat-cell">{{todo.done}}</td>
            </tr>
        </tbody>
    </table>
    <mat-paginator [pageSizeOptions]="[10, 25, 100]" [pageSize]="10" [length]="totalElements"
        (page)="nextPage($event)">

    </mat-paginator>
</div>

As you can see in the above code we used normal table with only material style. We used mat-paginator as we used in the first solution. But this time we are relying on page event of MatPaginator instead.

The todo2.component.ts that takes care of calling the TodoService is shown below.

import { Component, OnInit } from '@angular/core';
import { TodoService } from '../todo.service';
import { Todo } from '../todo';
import { PageEvent } from '@angular/material/paginator';

@Component({
  selector: 'app-todo2',
  templateUrl: './todo2.component.html',
  styleUrls: ['./todo2.component.css']
})
export class Todo2Component implements OnInit {

  totalElements: number = 0;
  todos: Todo[] = [];
  loading: boolean;

  constructor(private todoService: TodoService) { }

  ngOnInit(): void {
    this.getTodos({ page: "0", size: "10" });
  }

  private getTodos(request) {
    this.loading = true;
    this.todoService.listTodos(request)
      .subscribe(data => {
        this.todos = data['content'];
        this.totalElements = data['totalElements'];
        this.loading = false;
      }, error => {
        this.loading = false;
      });
  }

  nextPage(event: PageEvent) {
    const request = {};
    request['page'] = event.pageIndex.toString();
    request['size'] = event.pageSize.toString();
    this.getTodos(request);
  }


}

Now if go to the UI you will see todos2 route also working.

04-table-pagination

Conclusion

In this post I wanted to show how you can quickly add pagination in an Angular Spring Boot application. I suggest you don’t consider pagination late in the application as it becomes very difficult to add in the later stages of the project. Add pagination from the day 1. You will thank me later.

The complete code is available on Github: boot-angular-pagination-example-app

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s