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.
- First, let's create a new project:
> ng new my-app
- 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.
- 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:
- 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..
- 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:
- 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.