Angular Material Component ~ table入門

## 概要
今回はAngular Materialのコンポーネントのテーブルについて、以下の機能を一つのテーブルにまとめてみました。これらの機能は、ユーザーが必要な情報を素早く見つけ出せるだけでなく、データの可視化と管理がしやすくなるため、アプリにテーブルを組み込む際は非常によく使っています。
ソート機能: 列ごとにデータを昇順や降順に並び替えられます。
フィルター機能: 各列のデータを入力フィールドを用いてフィルタリングし、特定の条件に合ったデータのみを表示できます。
ページネーター機能: データのページングをサポートし、大量のデータを扱う際に視覚的に見やすくしています。

:::note warn
Angular Material コンポーネントのサイト
https://material.angular.io/components/table/overview
:::

:::note warn
プロジェクト全体のソースコードはこちらに置いてあります。
https://github.com/ist-h-i/Angular/tree/main/v-18
:::
## 環境設定
### node -v
v20.17.0

### ng version
```
Package | Version
— | ---
@angular-devkit/architect | 0.1802.6
@angular-devkit/build-angular | 18.2.6
@angular-devkit/core | 18.2.6 (cli-only)
@angular-devkit/schematics | 18.2.6
@schematics/angular | 18.2.6
rxjs | 7.8.1
typescript | 5.5.4
zone.js | 0.14.10
```

## 初期構築コマンド
### Angularインストール
```
npm install -g @angular/cli
```

### プロジェクト作成
```
ng new “アプリ名”
```

### コンポーネント作成
```
ng g c components/”コンポーネント名”
```

### node-modulesインストール
```
npm i
```

### アプリ起動
```
npm start
```

## 実装
上記手順でtableコンポーネントを作成しています。
### table.component.html
```html
<!-- 
  テーブルコンポーネント
  データソースはdataSource、フィルタリングはfilterFormで行う
  ソートはmatSortChangeイベントで通知される
-->
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="announceSortChange($event)"
    class="mat-elevation-z8 demo-table" [formGroup]="filterForm">
    <!-- 
      列定義の繰り返し
      columnは列定義のオブジェクト
    -->
    @for (column of columns; track column) {
    <ng-container [matColumnDef]="column.columnDef">
        <!-- 
          列ヘッダー
          mat-sort-headerでソート可能
        -->
        <th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by {{column.columnDef}}">
            {{column.header}}
        </th>
        <!-- 
          列データ
          column.cell関数でデータを取得
        -->
        <td mat-cell *matCellDef="let row">
            {{column.cell(row)}}
        </td>
    </ng-container>
    <!-- 
      列番号の列
      フィルタリング用の入力フィールドを表示
    -->
    <ng-container [matColumnDef]="column.columnNum">
        <th mat-header-cell *matHeaderCellDef>
            <mat-form-field class="filter-field">
                <mat-label>Filter</mat-label>
                <input matInput (keyup)="applyFilter()" [formControlName]="column.columnDef"
                    placeholder="{{column.header}}" #input>
            </mat-form-field>
        </th>
    </ng-container>
    }

    <!-- 
      ヘッダー行定義
      displayedColumnsDefとdisplayedColumnsNumを使用
    -->
    <tr mat-header-row *matHeaderRowDef="displayedColumnsDef"></tr>
    <tr mat-header-row *matHeaderRowDef="displayedColumnsNum"></tr>
    <!-- 
      データ行定義
      displayedColumnsDefを使用
    -->
    <tr mat-row *matRowDef="let row; columns: displayedColumnsDef;"></tr>
</table>

<!-- 
  ページネーター
  ページサイズのオプションは5, 10, 20
-->
<mat-paginator class="mat-elevation-z8" [pageSizeOptions]="[5, 10, 20]" showFirstLastButtons
    aria-label="Select page of periodic elements"></mat-paginator>
```

### table.component.ts
```typescript
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatSort, MatSortModule, Sort } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatInputModule } from '@angular/material/input';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';

/**
 * 表示するデータ
 */
export interface PeriodicElement {
  name: string;
  position: number;
  weight: number;
  symbol: string;
}

/**
 * サンプルデータ
 */
const ELEMENT_DATA: PeriodicElement[] = [
  { position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H' },
  { position: 2, name: 'Helium', weight: 4.0026, symbol: 'He' },
  { position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li' },
  { position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be' },
  { position: 5, name: 'Boron', weight: 10.811, symbol: 'B' },
  { position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C' },
  { position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N' },
  { position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O' },
  { position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F' },
  { position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne' },
];

@Component({
  selector: 'app-table',
  standalone: true,
  imports: [
    MatFormFieldModule,
    MatInputModule,
    MatPaginatorModule,
    MatSortModule,
    MatTableModule,
    ReactiveFormsModule
  ],
  templateUrl: './table.component.html',
  styleUrls: ['./table.component.scss']
})
export class TableComponent implements AfterViewInit {
  /**
   * 表示する列
   */
  columns = [
    {
      columnNum: '0',
      columnDef: 'position',
      header: 'No.',
      cell: (element: PeriodicElement) => `${element.position}`,
    },
    {
      columnNum: '1',
      columnDef: 'name',
      header: 'Name',
      cell: (element: PeriodicElement) => `${element.name}`,
    },
    {
      columnNum: '2',
      columnDef: 'weight',
      header: 'Weight',
      cell: (element: PeriodicElement) => `${element.weight}`,
    },
    {
      columnNum: '3',
      columnDef: 'symbol',
      header: 'Symbol',
      cell: (element: PeriodicElement) => `${element.symbol}`,
    },
  ];
  /**
   * LiveAnnouncer
   */
  private _liveAnnouncer = inject(LiveAnnouncer);
  /**
   * データソース
   */
  dataSource = new MatTableDataSource<PeriodicElement>(ELEMENT_DATA);
  /**
   * 表示する列
   */
  displayedColumnsDef = this.columns.map(c => c.columnDef);
  displayedColumnsNum = this.columns.map(c => c.columnNum);
  /**
   * フィルター用のフォーム
   */
  filterForm: FormGroup;

  constructor(private formBuilder: FormBuilder) {
    this.filterForm = this.formBuilder.group({
      position: [''],
      name: [''],
      weight: [''],
      symbol: ['']
    });
  }
  @ViewChild(MatPaginator) paginator!: MatPaginator;
  @ViewChild(MatSort) sort!: MatSort;

  ngAfterViewInit() {
    this.dataSource.paginator = this.paginator;
    this.dataSource.sort = this.sort;
  }

  /**
   * フィルター
   */
  applyFilter() {
    const filterValue = this.filterForm.value;
    this.dataSource.filterPredicate = (data: PeriodicElement, filter: string) => {
      const filterValues = JSON.parse(filter);
      return (
        data.position.toString().includes(filterValues.position) &&
        data.name.toLowerCase().includes(filterValues.name.toLowerCase()) &&
        data.weight.toString().includes(filterValues.weight) &&
        data.symbol.toLowerCase().includes(filterValues.symbol.toLowerCase())
      );
    };
    this.dataSource.filter = JSON.stringify(this.filterForm.value);
    if (this.dataSource.paginator) {
      this.dataSource.paginator.firstPage();
    }
  }

  /**
   * Sort変更イベント
   */
  announceSortChange(sortState: Sort) {
    if (sortState.direction) {
      this._liveAnnouncer.announce(`Sorted ${sortState.direction}ending`);
    } else {
      this._liveAnnouncer.announce('Sorting cleared');
    }
  }
}
```

### app.config.ts
```typescript
import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideClientHydration } from '@angular/platform-browser';
import {provideAnimationsAsync} from "@angular/platform-browser/animations/async";

export const appConfig: ApplicationConfig = {
  providers: [
    provideExperimentalZonelessChangeDetection(),
    provideRouter(routes),
    provideClientHydration(),
    provideAnimationsAsync()
  ]
};
```

### table.component.scss
```css
.demo-table {
    width: 100%;
}

th.mat-sort-header-sorted {
    color: black;
}
```
## まとめ
普段からAngular Materialコンポーネントのテーブルを使用していますが、サンプルコードでは個々の機能が別々に紹介されているため、複数の機能を持ったテーブルを構築するのはかなり手間がかかる作業でした。特に、ソート、フィルター、ページネーションといった基本的な機能はよく使うので一つのテーブルにまとめておくと便利かなと思い作ってみました。
また、今回の実装を通じて、Angular Materialの強力な機能とそれらを組み合わせることで得られる利便性を再確認しました。よく使う複数の機能を一つのコンポーネントに統合した備忘録を増やしていき、今後の開発で活用していこうと考えています。