Blog Post‎ > ‎

Angular 2: Implement Pagination UI Component

posted Dec 25, 2016, 9:47 AM by Julian Zhu   [ updated Jan 21, 2017, 6:27 AM ]

Demo: 


GitHub (with just basic source code for this UI Pagination Component)


Background

  • I need a simple (yes, simple is beautiful!) pagination UI component for my Angular 2 project. 
  • I tried couple of open source ones from the Internet, but it is either too complicated or not working at all. 

I decided to implement my own pagination UI component. 

Design/Implementation Principle: Easy to Understand & Use, Flexible to Customize, and open source. 

UI Example: How does it look? 

The styling is using Bootstrap default css - nothing special. By default, it uses bootstrap from the Angular 2 application level. 
Of course, you could customize it easily by defining your own style in the css file. 





See how the pagination scale changes accordingly. 

Key Features:

  • With the buttons liking to the very first, very last page. 
  • Always showing the first page [1], and the last page [whatever the total - 1] 
  • Always showing the current page, and surrounding pages
  • Hide page indices that are too far away from the current page, and only show the "1"s, such as 21, 31, 41, ... and so on. 
Of course, you can easily customize the logic above. 

Now Let's dive into the Angular 2 Code: 

We create a component folder name "ui" for our pagination component implementation: 

[ui/] 
- pagination.component.ts
- pagination.component.html
- pagination.component.css


We will ignore .css for now - you could define and apply any css style you want. 

Let's focus on the two core implementation files: 

pagination.component.ts

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

// Menu Component
@Component({
  moduleId: module.id,
  selector: "my-pagination",
  templateUrl: 'pagination.component.html',
  styleUrls: [ 'pagination.component.css' ]
})

export class PaginationComponent implements OnInit {

  @Input() total: number = 0;   // total pages
  @Input() page: number = 0;    // current/selected page
  @Input() size: number = 0;    // # of elements per page
  @Input() keyword: string = '';    // search data keyword

  @Output()
  pageChanged: EventEmitter<{keyword: string, page: number, size: number}> = new EventEmitter();

  pages: Array<any> = new Array();

  constructor() { }

  ngOnInit(): void {
    this.pages = new Array(this.total);
    for(let pg = 0; pg < this.total; pg ++) {
      this.pages.push({page: pg, current: (pg == this.page)});
    }
  }

  // when user select a page
 // We emit an event and notify the parent component to handle refresh/loading data
  selectPage(page: number, event?:MouseEvent) {
      if (event) {
          event.preventDefault();
          //event.defaultPrevented;
          this.page = page;
        }
    this.pageChanged.emit({keyword: this.keyword, page: this.page, size: this.size});
  }

  // the paging logic
 // this is used to determine how to display (or whether or not to display) page index/link. 
  showPageLink(page: number):boolean {
    if(page ==0 || page == this.total -1) return true;
    if(Math.abs(page - this.page) > 5) {
      if(page%10 == 0) return true;
      else return false;
    } else {
      return true;
    }
  }

}


pagination.component.html

 
<ul *ngIf="total > 0" class="pagination  pagination-sm" >
    <li class="pagination-first page-item"
          [class.disabled]="page == 0">
      <a class="page-link" href (click)="selectPage(0, $event)"><span class="glyphicon glyphicon-step-backward" aria-hidden=true></span></a>
    </li>

    <li *ngIf="page > 0" class="pagination-prev page-item"
          [class.disabled]="page == 0">
      <a class="page-link" href (click)="selectPage(page - 1, $event)"><span class="glyphicon glyphicon-chevron-left" aria-hidden=true></span></a>
    </li>

    <li *ngFor="let pg of pages" class="pagination-page page-item" [class.active]="pg != null && page == pg.page">
      <a *ngIf="pg && showPageLink(pg.page)" class="page-link" href (click)="selectPage(pg.page, $event)">{{ pg.page + 1 }}</a>
    </li>

    <li *ngIf="page < (total - 1)" class="pagination-next page-item"
          [class.disabled]="page == (total - 1)">
      <a class="page-link" href (click)="selectPage(page + 1, $event)"><span class="glyphicon glyphicon-chevron-right" aria-hidden=true></span></a></li>

    <li class="pagination-last page-item"
          [class.disabled]="page == (total - 1)">
      <a class="page-link" href (click)="selectPage(total - 1, $event)"><span class="glyphicon glyphicon-step-forward" aria-hidden=true></span></a></li>
  </ul>






The following is not the core implementation of this UI component. 
This is to illustrate how to use it. 

Now, let's use it: 

My example is to load a list of Twitter messages (via HTTP API call) and populate the page (with pagination component). 

Note: 
The logic for loading tweets is not significant -- some components may be missing in this blog. 
I don't intend to show you how to implement a data API service with a business object component (in this case, Twitter messages). 
Please focus on the use of Pagination component. 

If you need help with building Angular 2 data components with HTTP API services, please write to me separately. 

tweets.service.ts

 
import { Injectable,EventEmitter } from '@angular/core';
import { Headers, Http, Response } from '@angular/http';

import { Config, Text } from '../common/config';
import { APIRequest } from '../common/api-request';
import { APIResponse } from '../common/api-response';
import { Event } from '../common/event';

import { Tweet } from './tweet';

import 'rxjs/add/operator/toPromise';

@Injectable()
export class TweetService {

  message: string;
  keyword: string;
  statusChange: EventEmitter<({object:any, message:string})> = new EventEmitter();

  private headers = new Headers({'Content-Type': 'application/json'});

  constructor(private http: Http) {}

  // TODO: 
  getObjects(sender: string, page: number, size: number): Promise<APIResponse> {

      this.sender = sender;
      let apiRequest = <APIRequest>({
          apiKey: 'example_api_key',
          operator: '',
          token: 'example_token',
          page: page,
          size: size,
          keyword: keyword
      });

      const url = 'http://************/tweets'; // define your own API end point

      return this.http.post(url, JSON.stringify(apiRequest), {headers: this.headers})
      .toPromise()
      .then(
          response =>
          {
          console.log(response);
            if(response.json().code == '200') {
              return response.json() as APIResponse
            } else {
              this.message = Text.val(500);
              this.emitStatusChangeEvent(null, this.message);
            }
          }
        )
      .catch((ex) => this.handleError(ex));

  }

  private handleError(error: any) {
    this.emitStatusChangeEvent(null, Text.val(500));
    //return Promise.reject(error.message || error);
  }

  emitStatusChangeEvent(object: any, message: string) {
    this.statusChange.emit({object:object, message:message});
  }

  getStatusChangeEmitter() {
    return this.statusChange;
  }
}



tweets.component.ts


import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { ActivatedRoute, Params }   from '@angular/router';

import { APIResponse } from '../common/api-response';
import { Event } from '../common/event';

import { Tweet } from './tweet';
import { TweetDetailComponent } from './tweet-detail.component';

import { TweetService } from './tweet.service';

@Component({
    moduleId: module.id,
    selector: 'my-tweets',
    templateUrl: 'tweets.component.html',
    styleUrls: [ 'tweets.component.css' ]
})

export class TweetsComponent implements OnInit {

  objects: Tweet[];
  selectedObject: Tweet;
  message: string;
  subscription: any;

  apiResponse: APIResponse;
  sender: string;
  page: number;
  size: number;

  constructor(
    private router: Router,
    private activatedRoute: ActivatedRoute,
    private service: TweetService,
    private authGuard: AuthGuard
  ) {
    this.message = this.service.message;
    this.subscription = this.service.getStatusChangeEmitter()
    .subscribe(($event:any) => {
      if($event.object instanceof Event && $event.object.type == Event.RELOAD) {
        // do nothing - reserved for future
        this.ngOnInit();
      }
      this.message = $event.message;
    } );
  }

  getObjects(sender: string, page: number, size: number): void {

    this.sender = sender;
    this.service.getObjects(sender, page, size).then(
      apiResponse => {
        this.apiResponse = apiResponse;
        this.objects = apiResponse.body as Tweet[];
      }
    );
  }

  ngOnInit(): void {
    this.activatedRoute.params.forEach((params: Params) => {
      let sender = params['sender'];
      this.getObjects(sender, 0, 50);
    })
  }
  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  onSelect(object: Tweet): void {
    this.selectedObject = object;
  }

  selectSender(object: Tweet): void {
    if(object != null) {
      this.sender = object.sender;
      this.router.navigate(['/tweets', object.sender]);
    } else {
      this.sender = '';
      this.router.navigate(['/tweets']);
    }
  }

 // this responds to user selection of new page and load/refresh data
  selectObjects(event: any) {
    this.getObjects(event.keyword, event.page, event.size);
  }
}






tweets.component.html


<div class="container">

<div *ngIf="message" class="alert alert-warning">
  {{ message }}
</div>

<div class="panel panel-default">

  <div *ngIf="apiResponse">
    <div align="center">
      <my-pagination [keyword]="sender" [page]="0" [size]="50" [total]="apiResponse.totalPages" (pageChanged)="selectObjects($event)"></my-pagination>
    </div>

Total Records: <span class="badge">{{ apiResponse.totalElements }}</span>
  </div>
  <table class="table table-bordered table-striped table-hover table-condensed">

    <tr>
      <th>Sender</th>
     <th>Time</th>
      <th>Text</th>
    </tr>

    <tr *ngFor="let object of objects"
      [class.selected]="object == selectedObject"
      (click)="onSelect(object)">
      <td>
        <button class="btn btn-primary btn-sm" (click)="selectSender(object)"><span class="glyphicon glyphicon-send" aria-hidden=true></span> {{object.sender}}</button>
      </td>
      <td>
        <h6>{{object.time | date:'MM/dd/yyyy hh:mm a' }}</h6>
      </td>
      <td>
        {{object.text}}
      </td>
    </tr>
  </table>

</div>

</div>




Enjoy!!!

About The Author

Julian Zhu is a principal consultant and managing partner at OSC Technologies  at the Great Boston area. He previously worked at CVS Health managing Enterprise Architecture team, and consultant at Greenwich Technology Partners. Contact him at julian.zhu@oscgc.com if you have any question. Thank you. 




Comments