DEV Community

Michal M.
Michal M.

Posted on • Originally published at michalmuszynski.com

Pagination component in Angular

Pagination is essential when it comes to displaying a lot of data. Usually, we prefer to divide our content into smaller parts instead of showing for example a long list of entries. There's a lot of libraries that offer fully-featured components that are ready to use. I prefer to build my own tools. Mostly because I don't want to load a whole npm package if I need just a few (or one) elements from it. In this tutorial, I'd like to show how to implement a reusable pagination component using Angular.

For the purpose of this post I've created a separate project with a github repository. The live project is available here. The project is created using ng new your-project-name command from Angular CLI.

This component allows to

  • change pages by one,
  • jump to the next and last one,
  • type the desired page number by hand,
  • trigger the content change when the page changes.

Fetching the data

First, I decided to use external API to get data. For the testing purpose this resource is very reliable.

All posts should be fetched in the parent component for the pagination. In our application, it is in the main App component.
Here's the component with the explanation.

app.component.ts

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

interface IData {
  useId: number;
  id: number;
  title: string;
  body: string;
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  fetchedData: IData[] = [];
  displayedData: IData[] = [];
  itemsPerPage: number = 10;
  allPages: number;

  constructor(private http: HttpClient) {
    this.fetchData();
  }

  fetchData(): void {
    const dataConfig$ = this.http.get('https://jsonplaceholder.typicode.com/posts');
    dataConfig$.subscribe((data: any) => {
        this.fetchedData = data;
        this.onPageChange();
        this.allPages = Math.ceil(this.fetchedData.length / this.itemsPerPage);
      }
    );
  }

  onPageChange(page: number = 1): void {
    const startItem = (page - 1) * this.itemsPerPage;
    const endItem = page * this.itemsPerPage;
    this.displayedData = this.fetchedData.slice(startItem, endItem);
  }
}
Enter fullscreen mode Exit fullscreen mode

In the fetchData method all posts are loaded using the native Angular HttpClient module. Also here, we have to specify how many items per page we would like to show. In this example, I put a hardcoded value, but it's possible to make it dynamic. There's just a need to create a select element so the user can choose the number of items per page.

Display data

The onePageChange method is fired every time it receives the event from the pagination component (child component). I will explain child-parent communication in the further part of the article. This part of the code is responsible for splitting the data collection into smaller parts. The displayedData is the part that will be shown in the application. I call this method after fetching the JSON so the data is split just after receiving it.
In the template I pass the displayedData into the data-list component which is responsible only for displaying the data.

app.component.html

<div class="main__container">
  <h1>Angular Pagination</h1>
  <app-data-list [postsData]="displayedData"></app-data-list>
  <app-pagination [allPagesNumber]="allPages" (changePage)="onPageChange($event)"></app-pagination>
</div>
Enter fullscreen mode Exit fullscreen mode

As mentioned above, the DataList component only consumes data and displays the input.

data-list.component.ts

import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'app-data-list',
  template: `
    <div class="list__container">
      <div class="list_data">
        <p *ngFor="let item of postsData">
          {{ item.id }} - {{ item.title }}
        </p>
      </div>
    </div>
  `,
  styleUrls: ['./data-list.component.scss'],
})
export class DataListComponent implements OnInit {
  @Input() postsData: any = [];

  ngOnInit(): void {}
}
Enter fullscreen mode Exit fullscreen mode

Pagination handler

Now, let's take a look at the Pagination component.

pagination.component.ts

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-pagination',
  templateUrl: './pagination.component.html',
  styleUrls: ['./pagination.component.scss']
})
export class PaginationComponent implements OnInit {
  @Input() itemsPerPage: number;
  @Input() itemsNumber: number;
  @Input() allPagesNumber: number;
  @Output() changePage: EventEmitter<number> = new EventEmitter<number>();
  private _currentPage: number = 1;

  constructor() { }

  ngOnInit(): void {}

  get currentPage(): number {
    return this._currentPage;
  }

  set currentPage(page) {
    this._currentPage = page;
    this.changePage.emit(this.currentPage);
  }

  onSetPage(event): void {
    this.currentPage = event.target.value;
  }

  onFirstPage(): void {
    this.currentPage = 1;
  }

  onLastPage(): void {
    this.currentPage = this.allPagesNumber;
  }

  onNextPage(): void {
    this.currentPage += 1;
  }

  onPreviousPage(): void {
    this.currentPage -= 1;
  }
}
Enter fullscreen mode Exit fullscreen mode

For each page change action I've created a separate method. The most important part of this component is the currentPage property which is emitted to the parent App component. Also, I've used the getter and the setter here. Thanks to it, I can call the currentPage setter in every page handler method to change the value. Every time the currentPage value is changed, the event is emitted to the parent component. That's the purpose of the native Angular EventEmitter object.

The template of the pagination is pretty simple

pagination.component.html

<div class="pagination__container">
  <div class="pagination__button pagination__page-first"
       [ngClass]="currentPage === 1 && 'pagination__button--disabled'"
       (click)="onFirstPage()"
  >
    First
  </div>
  <div class="pagination__button pagination__page-previous"
       [ngClass]="currentPage === 1 && 'pagination__button--disabled'"
       (click)="onPreviousPage()"
  >
    Previous
  </div>
  <div class="pagination__page-active">
    <input class="pagination__current-page" (input)="onSetPage($event)" [value]="currentPage"> / <span>{{allPagesNumber}}</span>
  </div>
  <div class="pagination__button pagination__page-next"
       [ngClass]="currentPage === allPagesNumber && 'pagination__button--disabled'"
       (click)="onNextPage()"
  >
    Next
  </div>
  <div class="pagination__button pagination__page-last"
       [ngClass]="currentPage === allPagesNumber && 'pagination__button--disabled'"
       (click)="onLastPage()"
  >
    Last
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Each pagination element needs an appropriate click handler and ngClass. The pagination__button--disabled class is needed to prevent the selection of pages that are out of the paging range. This class only disables the pointer-events property. The pagination component allows changing the page by typing the page number manually in the input. It works, however you can type a value that is out of range of the pagination in this example (for example -2, 12). It might be prevented for example with the attribute directive. That's an interesting topic to cover, but in this article, I'll leave it as it is.

Parent-child communication

The data flow in this direction is crucial for the pagination functionality in this example. In the parent component, we fetch the data and serve it through the nested components. The only information that App component needs in this case, is the currentPage value.

app.component.html

<div class="main__container">
  <h1>Angular Pagination</h1>
  <app-data-list [postsData]="displayedData"></app-data-list>
  <app-pagination [allPagesNumber]="allPages" (changePage)="onPageChange($event)"></app-pagination>
</div>
Enter fullscreen mode Exit fullscreen mode

The currentPage value is stored in Pagination component. Here we add a (changePage) Output from the Pagination and assign the onPageChange method from the App component. So the value from the child component is sent to the parent through the Output which is an EventEmitter.

Conclusion

There are surely many ways to create your own pagination component. The one I've created is simple and might be extended by other functionalities. However, it fulfills its role. Creating such reusable components is also a great opportunity to enhance your skills and learn to create more organized and structured code.

Top comments (0)