© 2020, Developed by Hieu Dev

Thêm tính năng phân trang (pagination) trong Angular

Phân trang là một tính năng rất hay gặp khi bạn làm việc với một dữ liệu lớn, thay vì đưa tất cả dữ liệu ra thì thay vì đó bạn cần phân trang. Cụ thể mình mình sẽ hướng dẫn chi tiết trong bài hôm nay.

Thêm tính năng phân trang (pagination) trong Angular

Phân trang Angular là gì?

Như các bạn được biết, phân trang là việc hiển thị dữ liệu trên nhiều trang thay vì đặt tất cả chúng lên một trang duy nhất.

Với những trang web có dữ liệu lớn, việc phân trang là rất quan trọng, nó giúp trang web sẽ load nhanh hơn , giúp người dùng dễ dàng nhấn chọn trang mà mình mong muốn, đến được trang có nội dung mà mình cần, rất nhanh chóng, dễ dàng và chính xác.

Trong khi viết phân trang, chúng ta có rất nhiều cách, nhưng cụ thể nó lại chia thành 2 cách lấy như sau, với những ưu nhược điểm hoàn toàn khác nhau:

  • Cách 1: Phân trang toàn bộ trên client, cụ thể là sử dụng 1 request duy nhất để get tất cả data từ API về, sau đó xử lí thuần phân trang trên client (ví dụ như sử dụng javascript).
  • Cách 2: kết hợp giữa client và API ( với ví dụ client sử dụng Angular và API sử dụng .NET Core) Với cách phân trang này, mỗi khi chuyển page phân trang, sẽ có một request gửi lên API để lấy đúng 1 lượng data ở page đó.
Trong thực tế ta luôn ưu tiên dùng cách 2, vì nó sẽ giảm gánh nặng của client khi data quá lớn, giảm lượng data không cần thiết. Nhưng đối với 2 cách trên thì theo từng trường hợp mà ta linh hoạt sử dung.

Sử dụng ngx-pagination

Theo như bài giới thiệu trên, thì hôm nay mình sẽ hướng dẫn các bạn phân trang theo cách thứ 2, đó là kết hợp giữa client và API.

Trước tiên, để code chức năng phân trang bạn cần có API, với API bạn đã code chức năng phân trang từ server, nếu chưa có bạn có thể làm theo bài hướng dẫn theo link dưới đây:


Hoặc nếu bạn tự code, thì đáp ứng những endpoint và response ví dụ như sau:
  • /api/course/filter?filter=filter_name&pageIndex=1&pageSize=10

Như endpoint trên ta thấy được:
  • filter: sẽ filter dữ liệu có trong khoản pagination với tiêu đề bao gồm từ khóa filter_name.
  • pageIndex: Giá trị trang hiện tại.
  • pageSize: Giá trị kích thước mỗi trang.
Và ta cũng có ví dụ response từ server như sau:

{
  "items": [],
  "pageIndex": 1,
  "pageSize": 10,
  "totalRecords": 0,
  "pageCount": 0
}


Đây là một loại phân trang phía máy chủ, nơi máy chủ chỉ gửi một trang duy nhất tại một thời điểm. ngx-pagination hỗ trợ kịch bản này, vì vậy chúng ta thực sự chỉ cần sử dụng items totalRecords khi làm việc với thư viện này.

Để sử dụng ngx-pagination, ta có 2 việc chính cần làm đó là thêm 2 phần sau:

PaginatePipe: đặt cuối biểu thức ngFor:

<your-element *ngFor="let item of collection | paginate: { id: 'foo',
                                                      itemsPerPage: pageSize,
                                                      currentPage: page,
                                                      totalItems: total }">...</your-element>


PaginationControlsComponent: đây là component mặc định khi hiển thị phần phân trang, ta dễ dàng bằng việc thêm:

<pagination-controls></pagination-controls>


Bạn có thể tìm hiểu nhiều hơn với link dưới đây: 


Hướng dẫn thêm phân trang trong dự án Angular

Trong phần hướng dẫn này, chúng ta sẽ đặt được những mục tiêu sau đây:
  • Hoàn thành tính năng phân trang dữ liệu
  • Thêm tính năng filter
  • Cho người dùng chọn số records mỗi trang
Ok, bây giờ chúng ta sẽ tiến hành thực hiện với các bước cụ thể sau đây:

Bước 1: Cài đặt ngx-pagination với command sau:

npm install ngx-pagination --save


Bước 2: Thêm NgxPaginationModule trong app.module.ts, hoặc file module của bạn:

import { HttpClientModule } from '@angular/common/http';
import { NgxPaginationModule } from 'ngx-pagination';
@NgModule({
  declarations: [ ... ],
  imports: [
    ...
    NgxPaginationModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }


Bước 3: Tạo data service, ở phần service các bạn sử dụng Angular HTTPClient để gửi các HTTP requests:
services/course-category.service.ts:

getPage(page: number, size: number, query: string): Observable<CourseCategory[]> {
        return this.httpClient
            .get<CourseCategory[]>(this.apiURL + '/api/course-category/filter?pageSize=' + size + '&pageIndex=' + page + '&filter=' + query, this.httpOptions)
            .pipe();
    }


Như code trên, các bạn có thể thấy được, chúng ta gọi lại endpoint từ server với 3 tham số tương ứng pageIndex, pageSize và filter.

Bước 4: Sau khi đã có service, chúng ta tiến hành gọi lại service này với code sau:
components/index.component.ts:

...

@Component({
  selector: 'app-index',
  templateUrl: './index.component.html',
  styleUrls: ['./index.component.scss'],
})

export class IndexComponent implements OnInit {
  courseCategories: any = [];
  totalItems: any;
  p: number = 1;
  pageSize = 10;
  pageSizes = [10, 15, 20];
  query: string = '';

  constructor(
    private router: Router,
    private ngZone: NgZone,
    public courseCategoryService: CourseCategoryService
  ) {}

  ngOnInit(): void {
    this.getPage(this.p, this.pageSize, this.query);
  }

  handlePageChange(event: number): void {
    this.p = event;
    this.getPage(this.p, this.pageSize, this.query);
  }

  handlePageSizeChange(event: any) {
    this.pageSize = event.target.value;
    this.p = 1;
    this.getPage(this.p, event.target.value, this.query);
    this.handlePageChange(this.p);
  }

  handleSearch(ev: any) {
    this.query = ev.target.value;
    this.getPage(this.p, this.pageSize, ev.target.value);
  }

  getPage(p: number, pageSize: number, query: string) {
    this.courseCategoryService
      .getPage(p, pageSize, query)
      .subscribe((data: any) => {
        console.log(data);
        this.courseCategories = data.items;
        this.totalItems = data.totalRecords;
      });
  }
}

Cụ thể, ta dùng phương thức getPage() với 3 params tương ứng như cách gọi từ service, sau đó ta gán giá trị cho courseCategories để lấy các item từ reponse để hiển thị dữ liệu, còn biến totalItems để phục vụ cho phân trang, cụ thể để gán cho giá trị totalItems của PaginatePipe.

Tiếp theo, ta dùng phương thức handlePageChange() để bắt event khi người dùng chuyển trang và chúng ta sẽ thực hiện 1 request mới để trả các item ở trang người dùng yêu cầu.

Như ở phần mục tiêu, ta sẽ có một dropdown với các số lượng item mỗi page để người dùng chọn để hiện thị, khi các enduser chọn, chúng ta sẽ gọi đến phương thức handlePageSizeChange() để request đến server, trả về số lượng ietm phù hợp.

Và tương tụ, ta có method handleSearch() để handle chức năng filter dữ liệu.

Bước 5: Ta hiển thị giá trị ra HTML:
components/index.component.html:

<h1 class="my-6 text-lg font-bold text-gray-700 dark:text-gray-300">Course Categories</h1>
<div class="flex justify-between filter-bar">
  <div>
    Items per Page:
      <select (change)="handlePageSizeChange($event)">
        <option *ngFor="let size of pageSizes" [ngValue]="size">
          {{ size }}
        </option>
      </select>
  </div>
  <div>
    <input type="text" class="w-96 form-input px-4 py-2 border border-gray-200" placeholder="Nhập từ khóa để tìm kiếm..." (input)="handleSearch($event)">
  </div>
  <div>
    <a routerLink="/admin/course-category/create" class="align-bottom inline-flex items-center justify-center cursor-pointer leading-5 transition-colors duration-150 font-medium focus:outline-none px-3 py-1 rounded-md text-xs text-white bg-green-500 border border-transparent active:bg-green-600 hover:bg-green-600 focus:ring focus:ring-purple-300">
      Thêm mới
    </a>
  </div>
  
</div>

<div
    class="w-full overflow-hidden border border-gray-200 dark:border-gray-700 rounded-lg ring-black ring-opacity-5 mb-8 rounded-b-lg">
    <div class="w-full overflow-x-auto">
        <table class="w-full whitespace-no-wrap">
            <thead
                class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b border-gray-200 dark:border-gray-700 bg-gray-100 dark:text-gray-400 dark:bg-gray-800">
                <tr>
                    <td class="px-4 py-3">Name</td>
                    <td class="px-4 py-3">Description</td>
                </tr>
            </thead>
            <tbody
                class="bg-white divide-y divide-gray-100 dark:divide-gray-700 dark:bg-gray-800 text-gray-700 dark:text-gray-400">
                <tr *ngFor="let item of courseCategories | paginate: { itemsPerPage: pageSize, currentPage: p, totalItems: totalItems }">
                    <td class="px-4 py-3"><span class="text-sm">{{item.name}}</span></td>
                    <td class="px-4 py-3"><span class="text-sm">{{item.description}}</span></td>
                </tr>
            </tbody>
        </table>
        <pagination-controls (pageChange)="handlePageChange($event)"></pagination-controls>
    </div>
</div>


Và chúng ta đã hoàn thành, bây giờ các bạn run lên và xem kết quả.

Lời kết

Hi vọng, bài viết này sẽ giúp đỡ được những người mới tiếp xúc với Angular xử lí phân trang một các hiệu quả.

Hieu Ho.

Đăng nhận xét

Mới hơn Cũ hơn