EmotionTechテックブログ

株式会社エモーションテックのProduct Teamのメンバーが、日々の取り組みや技術的なことを発信していくブログです。

Angular Material の mat-paginator にページ指定を追加する

はじめに

こんにちは、フロントエンドエンジニアのありまです。皆さんは Angular Material 使っていますか?弊社ではとてもお世話になっています。優秀なUIライブラリなのですが、時々あともう一歩かゆいところに手が届かないことがあります。今回はそんな Angular Material のコンポーネントの一つである mat-paginator にページ指定の UI を追加することにチャレンジした記録をお伝えしていきます。

この記事はエモーションテックアドベントカレンダー 2023の 22日目の記事です。

背景

弊社ではアンケートなどのデータを扱う関係でページ数がとても多いテーブルを表示することがあります。その際にある程度あたりをつけてからページ送りをしたくなると思うのですが、Angular Material の mat-paginator にはページ指定のUIがなく、1ページごとの切り替えか最初か最後のページに飛ぶことしかできませんでした。 Angular Material の issue としても、6年前から上がっていますが、追加されることがまだないためカスタマイズを検討することにしました。

ゴール

この記事のゴールとしては以下の要件を満たしたコンポーネントを作成できることとします。

  • ページ指定ができること
  • mat-paginator と同じイベントが Output されること

完成形

stackblitz.com

Step.1 コンポーネントの作成

まずは mat-paginator を配置しておきます。親コンポーネントから値の受けとり、親へのイベントの通知ができるように Input() と Output() を設定します。

@Component({
  selector: 'app-paginator-with-goto',
  templateUrl: 'paginator-with-goto.component.html',
  styleUrls: ['paginator-with-goto.component.css'],
  standalone: true,
  imports: [
    MatPaginatorModule,
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PaginatorWithGotoComponent {
  @Input() length = 50;
  @Input() pageSize = 10;
  @Input() showFirstLastButtons = true;
  @Input() pageIndex = 0;
  @Input() pageSizeOptions = [5, 10, 25];

  @Output() page = new EventEmitter<PageEvent>();

  handlePageEvent(e: PageEvent) {
    this.page.emit(e);
  }
}
<mat-paginator
  (page)="handlePageEvent($event)"
  [length]="length"
  [pageSize]="pageSize"
  [showFirstLastButtons]="showFirstLastButtons"
  [pageSizeOptions]="pageSizeOptions"
  [pageIndex]="pageIndex"
  aria-label="Select page"
>
</mat-paginator>

皆さんおなじみの mat-paginator を表示できましたね。

Step.2 ページ指定セレクターの作成

ページ指定のUIですが表示できるページだけを選択できるようにしたいため、以下のようなセレクトボックスにしたいと思います。

セレクトボックスのオプションに表示できるページだけが選択肢になるように以下のような処理を書きます。

  // ページ指定セレクターの値
  pageSelectValue = 0;
  // ページ指定セレクターの選択肢
  pageSelectOptions: number[] = [];
  
  /**
   * ページ指定選択肢の更新処理
   */
  updatePageSelector(): void {
    // paginator の pageIndex は 0 から始まるため 1 を足す
    this.pageSelectValue = this.pageIndex + 1;
    // 選択肢の初期化
    this.pageSelectOptions = [];
    // 1ページごとの表示数でアイテムの総件数を割れる数だけ選択肢に追加
    for (let i = 1; i <= Math.ceil(this.length / this.pageSize); i++) {
      this.pageSelectOptions.push(i);
    }
  }

Input された値が変わるごとに再計算されるように、ngOnChanges() で更新処理を呼び出します。

  ngOnChanges() {
    this.updatePageSelector();
  }

それではページ指定のセレクターを作成してみましょう。

@Component({
  selector: 'app-paginator-with-goto',
  templateUrl: 'paginator-with-goto.component.html',
  styleUrls: ['paginator-with-goto.component.css'],
  standalone: true,
  // MatSelectModule, FormsModule をインポート
  imports: [MatSelectModule, FormsModule, MatPaginatorModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
<mat-paginator
  (page)="handlePageEvent($event)"
  [length]="length"
  [pageSize]="pageSize"
  [showFirstLastButtons]="showFirstLastButtons"
  [pageSizeOptions]="pageSizeOptions"
  [pageIndex]="pageIndex"
  aria-label="Select page"
>
</mat-paginator>

<div>
  <div>page:</div>
  <mat-form-field appearance="outline">
    <mat-select [(ngModel)]="pageSelectValue">
      @for (pageSelectOption of pageSelectOptions; track pageSelectOption) {
      <mat-option [value]="pageSelectOption">
        {{ pageSelectOption }}</mat-option
      >
      }
    </mat-select>
  </mat-form-field>
</div>

ページ指定のセレクターを作成できましたね。現状では選択してもページ指定のイベントが発行されないので、イベントの発行処理を作っていきます。

  @ViewChild(MatPaginator) paginator?: MatPaginator;
  
  /**
   * ページ指定イベントの発行・mat-paginatorへの反映
   */
  pageSelectChange(): void {
    if (this.paginator == undefined) return;
    // mat-paginator の値をページ指定の変更にあわせて更新
    this.paginator.pageIndex = this.pageSelectValue - 1;
    // pageEvent の発行
    const event: PageEvent = {
      length: this.paginator.length,
      pageIndex: this.paginator.pageIndex,
      pageSize: this.paginator.pageSize,
    };
    this.page.emit(event);
  }
<!-- #paginatorを付与 -->
<mat-paginator
  #paginator
  (page)="handlePageEvent($event)"
  [length]="length"
  [pageSize]="pageSize"
  [showFirstLastButtons]="showFirstLastButtons"
  [pageSizeOptions]="pageSizeOptions"
  [pageIndex]="pageIndex"
  aria-label="Select page"
>
</mat-paginator>

<div>
  <div>page:</div>
  <mat-form-field appearance="outline">
    <!-- pageSelectChange()を呼び出し -->
    <mat-select
      [(ngModel)]="pageSelectValue"
      (selectionChange)="pageSelectChange()"
    >
      @for (pageSelectOption of pageSelectOptions; track pageSelectOption) {
      <mat-option [value]="pageSelectOption">
        {{ pageSelectOption }}</mat-option
      >
      }
    </mat-select>
  </mat-form-field>
</div>

これでページ指定のイベントを発行することができるようになりました。

Step.3 スタイルの調整

さて、機能的には目標を達成したのですが、見た目がとても不自然になっています。見た目を整えていきましょう。

.paginator-container {
  display: flex;
  justify-content: flex-end;
  align-items: baseline;
  background: white;
}
<div class="paginator-container">
  <mat-paginator
    #paginator
    (page)="handlePageEvent($event)"
    [length]="length"
    [pageSize]="pageSize"
    [showFirstLastButtons]="showFirstLastButtons"
    [pageSizeOptions]="pageSizeOptions"
    [pageIndex]="pageIndex"
    aria-label="Select page"
  >
  </mat-paginator>
  <div class="mat-mdc-paginator">
    <div class="mat-mdc-paginator-page-size">
      <div class="mat-mdc-paginator-page-size-label">page:</div>
      <mat-form-field
        appearance="outline"
        class="mat-mdc-paginator-page-size-select"
      >
        <mat-select
          [(ngModel)]="pageSelectValue"
          (selectionChange)="pageSelectChange()"
        >
          @for (pageSelectOption of pageSelectOptions; track pageSelectOption) {
          <mat-option [value]="pageSelectOption">
            {{ pageSelectOption }}</mat-option
          >
          }
        </mat-select>
      </mat-form-field>
    </div>
  </div>
</div>

ページサイズ選択UIの見た目とあわせるため、Angular Materialのソースコードを参考に同じクラスを付与しました。 css をほとんど書かずにわりと自然な見た目にできたのではないでしょうか?

気になるところ

HTMLの要素としては mat-paginator の外に置かれているので、以下のような点が気になります。

レスポンシブが不自然

折り返しが必要な幅になると不自然な折り返しになってしまいます。

ページ指定のセレクターを端にしか配置できない

例えばページ送りの矢印の中央にセレクトボックスを配置したいと言われたときに対応できません。

そこでDOMを操作して以下のようなUIを作ってみたのですが、あまりおすすめはしませんのでご参考程度に。

stackblitz.com

おわりに

いかがだったでしょうか?今回はカスタマイズをして対応しましたが、ライブラリの変更に合わせて保守していかなければならなくなるのでなるべく Angular Material のアップデートを待ちたいですね。

エモーションテックでは顧客体験、従業員体験の改善をサポートし、世の中の体験を変えるプロダクトを開発しています。プロダクトに興味のある方、Angular を使ったアプリケーション開発をしたい方、ぜひ採用ページからご応募をお願いいたします。

hrmos.co

参考

https://github.com/angular/components/issues/7615#issuecomment-634502814 https://dev.to/krivanek06/angular-matpaginator-custom-styling-dkb