Angular custom paginator

Angular custom paginator

In this article, we'll create a custom paginator using just Angular components. Before starting, please read the drawbacks in case this is not the best approach for your project.

Drawbacks:

  • This is a custom approach, which means that you are responsible for making the necessary modifications in order to make it fit into your project.
  • This approach is decoupled from the same implementation, which means that you need to create the logic to manage the pages content. However, I'll be adding an example about it.

Let's start with the paginator implementation.

  1. First, let's create a new project:
> ng new my-app
  1. Now let's create two components:
> ng g c components/paginator
> ng g c components/paginator/page-button

The page-button component will be just for the layout of a single page button component, while the paginator component will be responsible for controlling the pagination logic.

  1. Creating the button component:

This component will be responsible for managing all the possible states of a page button, like selected, disabled, or even if it is an arrow or a dots button and not a number. Let's see how to create it:

page-button.component.html

1<button 
2  type="button"
3  class="pageButton"
4  [ngClass]="{
5    'current': current,
6    'dots': dots,
7    'right': rightArrow,
8    'left': leftArrow,
9    'disabled': disabled
10  }"
11  [disabled]="disabled"
12  (click)="onClick()"> 
13  {{ number }}
14</button>
15

page-button.component.ts

1import { Component, EventEmitter, Input, Output } from '@angular/core';
2
3@Component({
4  selector: 'app-page-button',
5  templateUrl: './page-button.component.html',
6  styleUrls: ['./page-button.component.scss']
7})
8export class PageButtonComponent {
9  @Input() current?: boolean = false;
10  @Input() dots?: boolean = false;
11  @Input() number?: number;
12  @Input() disabled?: boolean = false;
13  @Input() rightArrow?: boolean = false;
14  @Input() leftArrow?: boolean = false;
15
16  @Output() clickEvent = new EventEmitter<number>();
17
18  onClick(): void {
19    this.clickEvent.emit(this.number);
20  }
21}
22

This component is a presentational component, which means it won't have any kind of logic, It will just display information based on the input data.

You could find all the code for this component, including the scss, in here.

At the end, we could test all the possible states of the buttons, and it should look like this:

  1. Creating the paginator component

Now, let's create the paginator component, in which we'll be using the page-button component. This graph shows how the paginator component will work:

Now, let's translate this to code:

paginator.component.html

1<app-page-button 
2  [leftArrow]="true"
3  [disabled]="currentPage === 1"
4  (clickEvent)="goToPageFromArrow('left')"
5></app-page-button>
6<ng-container *ngFor="let page of pagesToShow">
7  <app-page-button 
8    [number]="page !== -1 ? page : undefined"
9    [dots]="page === -1"
10    [current]="page === currentPage"
11    (clickEvent)="goToPage($event)"
12  ></app-page-button>
13</ng-container>
14<app-page-button 
15  [rightArrow]="true"
16  [disabled]="currentPage === pages"
17  (clickEvent)="goToPageFromArrow('right')"
18></app-page-button>
19

Here, I'm also creating a little bit of logic, like controlling the disabled status for the arrows when the currentPage reaches the minimum limit for the left arrow and the maximum limit for the right arrow.

Moreover, I'm using -1 in order to know if I want to show dots. Let's review all this logic on the paginator.component.ts component.

Let's start with the @Input and @Output data:

paginator.component.ts

1  @Input() pages: number = 0;
2  @Input() currentPage: number = 0;
3  @Input() visiblePages: number = 0;
4  
5  @Output() gotToPageEvent = new EventEmitter<number>();
6

We'll receive the following data:

  • pages: An integer number of pages that represents the number of pages of your content.
  • currentPage: Remember that the logic of the pagination will depend on the component that implements the paginator. Thus, we'll be getting the current page.
  • visiblePages: The visible pages will be a number that indicates the maximum number of pages to show. If the number of pages is greater than the number of visible pages, we'll be using the dots feature.

Now, we need to define the initial data:

1fullPagesList: number[] = [];
2pagesToShow: number[] = [];
3pagesDifferential = 0;
4
5ngOnInit(): void {
6  this.pagesDifferential = Math.floor(this.visiblePages / 2);
7  this.fullPagesList = Array.from({length: this.pages}).map((_, i) => i+1);
8  this.definePagesButtons(this.currentPage);
9}
10

I'm defining some global variables in order to manage the state of the paginator. Then, on the OnInit I'm calculating the differential pages in order to know into how many parts I should split the pages.

Also, I'm creating an array using the Array.from function in order to generate an array based on a number.

1Array.from({length: 3}).map((_, i) => i+1); // ===> [1,2,3]
2

Finally, I'm calling the definePageButtons function with the current page:

1definePagesButtons(page: number): void {
2  if(this.pages > this.visiblePages) {
3    let pagesArray = [...this.fullPagesList];
4
5    pagesArray = [...pagesArray.slice(page - 1, pagesArray.length)];
6
7    if(pagesArray.length > this.visiblePages) {
8      pagesArray = [
9        ...pagesArray.splice(0, this.pagesDifferential), 
10        -1, 
11        ...pagesArray.splice(pagesArray.length - this.pagesDifferential, pagesArray.length)
12      ];
13      this.pagesToShow = pagesArray;
14    } else if(pagesArray.length === this.visiblePages) {
15      pagesArray = [
16        -1, 
17        ...pagesArray.splice(0, this.pagesDifferential), 
18        ...pagesArray.splice(pagesArray.length - this.pagesDifferential, pagesArray.length)
19      ];
20      this.pagesToShow = pagesArray;
21    }
22  } else {
23    this.pagesToShow = [...this.fullPagesList];
24  }
25}
26

This function will be responsible for defining how the data will look to the *ngFor.

ThereĀ“s an example of a possible output:

1// pages: 10,
2// visiblePages: 4,
3this.definePagesButtons(1);
4// output: [1,2,-1,9,10]
5

As I mentioned before, the -1 will render the dots variation on the page-button component.

Last, but not least, we need to define a couple of public functions in order to call the necessary process after clicking on a page button.

1goToPage(page: number): void {
2  if(!page) return;
3  this.gotToPageEvent.emit(page);
4  this.definePagesButtons(page);
5}
6
7goToPageFromArrow(arrow: 'left' | 'right'): void {
8  if(arrow === 'left') {
9    this.gotToPageEvent.emit(this.currentPage - 1);
10    this.definePagesButtons(this.currentPage - 1);
11  } else {
12    this.gotToPageEvent.emit(this.currentPage + 1);
13    this.definePagesButtons(this.currentPage + 1);
14  }    
15}
16

These functions are too basic; goToPage will be called after clicking on a specific page number. And goToPageFromArrow will be called after clicking on an arrow button. And the process is just controlling the current page variable.

You can see the entire code for this component, including the scss in here..

  1. Implementing the paginator

Now, we could implement the paginator from another component. I'l be using the app.component to call it.

app.component.html

1<app-paginator 
2  [pages]="10"
3  [currentPage]="1"
4  [visiblePages]="4">
5</app-paginator>
6

Then, the component should look like this:

As you can tell, the layout looks good. However, it is not working properly. That's because we haven't added the pagination logic to app.component.

1export class AppComponent implements OnInit {
2  title = 'paginator';
3  pages: number = 0;
4  currentPage: number = 1;
5
6  ngOnInit(): void {
7    this.pages = 10;
8  }
9
10  changePage(page: number): void {
11    if(page === this.currentPage) {
12      return;
13    }
14    this.currentPage = page;
15  }
16}
17
1<app-paginator 
2  [pages]="pages"
3  [currentPage]="currentPage"
4  [visiblePages]="4"
5  (gotToPageEvent)="changePage($event)">
6</app-paginator>
7

Now, it should work just fine:

  1. Finally, let's implement the paginator with real content management:

I'm going to test this feature using just a collection of boxes. Like this:

1<div class="container">
2  <section class="content">
3    <ng-container *ngFor="let box of contentToShow">
4      <div class="box">Box {{box}}</div>
5    </ng-container>
6  </section>
7  <app-paginator 
8    [pages]="pages"
9    [currentPage]="currentPage"
10    [visiblePages]="2"
11    (gotToPageEvent)="changePage($event)">
12  </app-paginator>
13</div>
14

I'm just iterating over a list and displaying divs with a simple design:

1export class AppComponent implements OnInit {
2  title = 'paginator';
3  pages: number = 0;
4  contentPerPage = 4;
5  currentPage: number = 1;
6  content = Array.from({length: 20}).map((_, i) => i+1);
7  contentToShow: number[] = [];
8
9  ngOnInit(): void {
10    this.pages = Math.floor(this.content.length / this.contentPerPage);
11    this.contentToShow = [...this.content].splice(0, this.contentPerPage);
12  }
13
14  changePage(page: number): void {
15    if(page === this.currentPage) {
16      return;
17    }
18    this.contentToShow = [...this.content].splice((this.contentPerPage * page) -  this.contentPerPage, this.contentPerPage);
19    this.currentPage = page;
20  }
21}
22
23

Also, I'm using simple logic to choose which part of the entire content should be displayed. With this simple code:

1this.contentToShow = [...this.content].splice((this.contentPerPage * page) -  this.contentPerPage, this.contentPerPage);
2

Then, the final app should look something like this:

Summary

  • We just created a nice paginator component using just angular components.
  • This approach can be adapted for any kind of implementation, due to the fact that the main logic can be adapted for any kind of source content. Like:
    • Static content.
    • Content from APIs that uses pagination systems from backend.
    • Implementations with observables to manage data persistence.

Final thoughts

This code is an initial purpose for a pagination system, which means the source code could change in the future in order to improve the implementation. Please, clone the repository and try to improve this code. All the information about this implementation can be found here.