使用 Angular Signals 實作分頁功能

今天這篇文章來簡單用 Angular Signals 實作一個分頁功能,體驗一下使用 Angular Signals 與 Reactive Programming 的開發思維。

不使用 Angular Signals

一般來說,至少會需要一個 currentPage 變數,儲存目前頁碼。另外我們也準備上一頁的變數 previousPage 和下一頁的變數 nextPage

另外一個常識性的重點是,上一頁不可以小於 1,且下一頁不可以大於總頁數,當然目前頁碼也一樣不可以小於 1 及大於總頁數。

因此我們需要至少兩個 @Input()

@Input() page = 1;
@Input() pageCount = 1;

另外是在這個範例中不是重點,但也是需要的 pageChange 事件,以告知使用元件的人頁碼已經被改變了

@Output() pageChange = new EventEmitter<number>();

最後前面規劃的三個內部狀態,供畫面顯示用

currentPage = 1;
previousPage = 1;
nextPage = 1;

接著是切換頁碼的功能,無論是點擊某個單頁面頁碼,或是點擊「上一頁」、「下一頁」這種按鈕,其實都是「跳到某一個頁碼」的實作

jumpTo(num: number) {
  this.currentPage = num;

  // 觸發事件
  this.pageChange.emit(num);

  // 上一頁頁碼
  this.previousPage = num - 1;

  // 下一頁頁碼
  this.nextPage = num + 1;
}

以上是最簡的實作,但三個頁碼的狀態其實都有額外的規則,我們可以稍微調整一下

jumpTo(num: number) {
  // 頁碼不可以小於 1 或大於總頁碼
  if(num < 1 || num > this.pageCount) {
    return;
  }

  this.currentPage = num;

  // 觸發事件
  this.pageChange.emit(num);

  // 上一頁不可以小於 1
  if(this.currentPage > 1) {
    this.previousPage = this.currentPage - 1;
  }

  // 下一頁不可以大於總頁碼
  if(this.currentPage + 1 <= this.pageCount) {
    this.nextPage = this.nextPage + 1;
  }
}

再補上「上一頁」和「下一頁」的按鈕功能

goPrevious() {
  this.jumpTo(this.currentPage - 1);
}

goNext() {
  this.jumpTo(this.currentPage + 1);
}

一個簡單的邏輯就完成了,邏輯不算太複雜,但也要處理不少事情,接著我們來看看 Angular Signals 加上 Reactive Programming 思維會如何寫作。

使用 Angular Signals

在 Reactive Programming 思維下,所有的資料變化都有一個來源,而整個功能目前的最主要來源,就是「頁碼的切換」,因此我們先將 currentPage 變成是一個 signal

currentPage = signal(1);

在切換頁碼時,可以單純呼叫 set 方法就好

jumpTo(num: number) {
  // 頁碼不可以小於 1 或大於總頁碼
  if(num < 1 || num > this.pageCount) {
    return;
  }

  // 送出目前頁碼變更的訊號
  this.currentPage.set(num);

  // 觸發事件
  this.pageChange.emit(num);
}

此時上一頁以及下一頁的頁碼,就可以很簡單的用 computed 來根據 currentPage 的變化進行計算

// 上一頁頁碼
previousPage = computed(() => {
  // 檢查頁碼是否大於 1
  return this.currentPage() > 1 
    ? this.currentPage() - 1 
    : this.currentPage();
})

// 下一頁頁碼
nextPage = computed(() => {
  // 檢查頁碼是否小於總頁數
  return this.currentPage() < this.pageCount
    ? this.currentPage() + 1
    : this.currentPage()
});

最後「上一頁」以及「下一頁」的功能就沒什麼變化,只是改成從 signal 取得資料

goPrevious() {
  this.jumpTo(this.currentPage() - 1);
}

goNext() {
  this.jumpTo(this.currentPage() + 1);
}

從這樣的範例可以發現,與之前相比,jumpTo 的功能就只關注於「目前頁碼的變化」,不再關注「上一頁頁碼」以及「下一頁頁碼」,職責明顯變簡單,只要明確的處理「目前頁碼」相關的邏輯即可。

而「上一頁頁碼」就單純的「回應」目前頁碼的變化,並只關注在「上一頁頁碼」該如何計算的邏輯;同理,「下一頁頁碼」也是一樣。

如此一來,每段程式碼都會變得更加簡單,且更加關注自己的職責,達到 Reactive Programming 的設計精神囉!

完整程式碼一次看:

export class PaginationComponent implements OnInit {
  @Input() page = 1;
  @Input() pageCount = 1;

  @Output() pageChange = new EventEmitter<number>();

  // 目前頁碼
  currentPage = signal(1);

  // 上一頁頁碼
  previousPage = computed(() => {
    // 檢查頁碼是否大於 1
    return this.currentPage() > 1 
      ? this.currentPage() - 1 
      : this.currentPage();
  })

  // 下一頁頁碼
  nextPage = computed(() => {
    // 檢查頁碼是否小於總頁數
    return this.currentPage() < this.pageCount
      ? this.currentPage() + 1
      : this.currentPage()
  });

  ngOnInit() {
    // 根據 @Input() 初始頁碼狀態
    this.currentPage.set(this.page);
  }

  jumpTo(num: number) {
    // 頁碼不可以小於 1 或大於總頁碼
    if(num < 1 || num > this.pageCount) {
      return;
    }

    // 送出目前頁碼變更的訊號
    this.currentPage.set(num);

    // 觸發事件
    this.pageChange.emit(num);
  }

  goPrevious() {
    this.jumpTo(this.currentPage() - 1);
  }

  goNext() {
    this.jumpTo(this.currentPage() + 1);
  }
}
如果您覺得我的文章有幫助,歡迎免費成為 LikeCoin 會員,幫我的文章拍手 5 次表示支持!