Angular Materialのテーブル
以前はAngular Materialのダイアログを紹介しましたが、今回はテーブルを利用してみます。Materialのtableを使うとデフォルトのままでもデザインが整っているという利点の他に、1.HTMLで、ngForのループを使わずに列毎にテーブルの記述ができること、2.ページネーション機能が備わっている事、3.フィルタ機能が備わっている事などが導入メリットとして挙げられると思います。
基本的な使い方
e-Stat(政府統計)の2022年3月の小売物価統計調査(自動車ガソリン)のデータを使って、サンプルを作成してみたいと思います
まず、app.module.tsにテーブルコンポーネントを設定します。
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatTableModule } from '@angular/material/table';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
MatTableModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
次に、元となるデータを作成します。行の構成をインターフェースにしてその配列をデータとします。
インターフェースは次のように設定しました。データはこの配列になるようにします。正規の方法(ng generate interface)で構成してもいいのですが、今回はコンポーネントに直接記述してしまいます。
export interface RowData {
area_code: string, //地域コード
area_name: string, //地域
price: number //単価
}
MatTableDataSourceをインポートし、テーブルの列を設定します。列の設定は文字列の配列になります。列を識別するためのキーを定義します。
テーブル用のデータを「tableData」とします。これは先ほど定義したインターフェースの配列として定義するか、MatTableDataSourceとして定義します。ここではあとで例示するページネーションを設定するために後者を採用します。
OnInitで実際のデータをセットします。
ここまでのスクリプトをまとめると次のようになります。
app.component.ts
import { Component,OnInit } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
export interface RowData {
area_code:string,
area_name:string,
price: number
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title='test-app';
//列の定義
columnsdefine: string[] = ['col_area_code','col_area_name','col_price'];
//データの定義
public tableData= new MatTableDataSource<RowData>();
ngOnInit(): void {
this.tableData=new MatTableDataSource([
{area_code:"01100",area_name:"札幌市",price:177},
{area_code:"01202",area_name:"函館市",price:175},
{area_code:"01204",area_name:"旭川市",price:176},
{area_code:"02201",area_name:"青森市",price:172},
{area_code:"02203",area_name:"八戸市",price:169},
{area_code:"03201",area_name:"盛岡市",price:163},
{area_code:"04100",area_name:"仙台市",price:170},
{area_code:"05201",area_name:"秋田市",price:170},
{area_code:"06201",area_name:"山形市",price:182},
{area_code:"07201",area_name:"福島市",price:176},
...
}
}
HTML側は次のようになります。多くの設定要素があるのでコードの中に説明を書きます。
app.compnent.html
<table mat-table [dataSource]="tableData">
<!-- ng-containerで囲まれた部分が一つの列の定義です -->
<!-- このmatColumnDefに.ts側で定義した列のidを設定します -->
<ng-container matColumnDef="col_area_code">
<!-- thにmat-header-cell、*matHeaderCellDefを設定します。これがヘッダになります -->
<th mat-header-cell *matHeaderCellDef> 地域コード </th>
<!-- thにmat-cell、*matCellDefを設定します。これがデータ行になります -->
<!-- elementにデータを受けると、設定したインターフェース(行)が取得できます -->
<td mat-cell *matCellDef="let element"> {{element.area_code}} </td>
</ng-container>
<ng-container matColumnDef="col_area_name">
<th mat-header-cell *matHeaderCellDef> 地域 </th>
<td mat-cell *matCellDef="let element"> {{element.area_name}} </td>
</ng-container>
<ng-container matColumnDef="col_price">
<th mat-header-cell *matHeaderCellDef> 価格 </th>
<td mat-cell *matCellDef="let element"> {{element.price}} </td>
</ng-container>
<!-- ヘッダ部の定義です。ここに全体の列の定義を設定します -->
<tr mat-header-row *matHeaderRowDef="columnsdefine"></tr>
<!-- ヘッダ同様にデータ部にも列の定義を設定します -->
<tr mat-row *matRowDef="let row; columns: columnsdefine;"></tr>
</table>
あとはCSSを設定するだけで、先の画像のようなテーブルが作成されます。
また、trに対して(click)などのイベントを設定したい場合は下段にあるtrタグに記述することができます。その時行のデータの値も利用できます。上の例でいえば、row.area_nameとすることで、「地域」を取得することができます。
行の追加と削除
行を追加したり削除するにはテーブルの参照をViewChildを使って取得します。MatTableの参照を取得するので、そのimportも必要です。
app.component.ts
import { Component,OnInit, ViewChild } from '@angular/core';
import { MatTable, MatTableDataSource } from '@angular/material/table';
export interface RowData {
area_code:string,
area_name:string,
price: number
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title='test-app';
columnsdefine: string[] = ['col_area_code','col_area_name','col_price'];
public tableData= new MatTableDataSource<RowData>();
@ViewChild(MatTable) table: MatTable<RowData> | undefined;
ngOnInit(): void {
this.tableData=new MatTableDataSource([
{area_code:"01100",area_name:"札幌市",price:177},
{area_code:"01202",area_name:"函館市",price:175},
...
]);
}
public addRow():void {
console.log(this.tableData.data);
this.tableData.data.push({area_code:"12345",area_name:"豊田市",price:179});
this.table?.renderRows();
}
public delRow():void {
this.tableData.data.pop();
this.table?.renderRows();
}
}
先の設定で、tableDataをRowDataの配列としている場合は単にその配列に対して、pushやpopをするだけでいいです。
また、おそらくオーバーヘッドが大きくなると思いますが、tableDataに対して新しい配列やMatTableDataSourceをセットしてしまえば、ViewChildからrenderRows()メソッドを呼ばなくてもデータは更新されます。
ページネーションを追加
今度はテーブルに表示されたデータをページ毎に分割するページネーションを設定します。ページネーションを利用するにはapp.module.tsで「MatPaginatorModule」を、app.component.tsで「MatPagiator」をインポートする必要があります。
app.module.ts
...
import { MatPaginatorModule } from '@angular/material/paginator';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
MatTableModule,
MatPaginatorModule
],
...
そして、ViewChildを利用します。また、AfterViewInitライフサイクルフックも利用します。AfterViewInitは必須ではありません。どこかのタイミングで「this.tableData.paginator = this.paginator」を設定できればそれで構いません。またtableDataを差し替えた場合はそのプロパティであるpagenatorも変わってしまうので、再度セットする必要があります。
あと、先ほどtableDataは行データの配列でもいいという話をしましたが、ページネーションを利用するためにはtableDataはMatTableDataSourceのインスタンスである必要がありますので注意してください。
app.component.ts
import {AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { MatPaginator } from '@angular/material/paginator';
export interface RowData {
area_code:string,
area_name:string,
price: number
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, AfterViewInit {
title='test-app';
columnsdefine: string[] = ['col_area_code','col_area_name','col_price'];
public tableData= new MatTableDataSource<RowData>();
@ViewChild(MatPaginator) paginator: MatPaginator | undefined;
ngOnInit(): void {
this.tableData=new MatTableDataSource([
{area_code:"01100",area_name:"札幌市",price:177},
{area_code:"01202",area_name:"函館市",price:175},
...
]);
}
ngAfterViewInit() {
//この条件はコンパイルを通すためだけに利用しています
if (this.paginator) {
this.tableData.paginator = this.paginator;
}
}
}
HTMLではtableタグを閉じた後に、mat-paginatorを記述します。
app.compnent.html
...
</table>
<mat-paginator [pageSizeOptions]="[10, 20, 30]" showFirstLastButtons>
</mat-paginator>
pageSizeOptionsで指定した数でページの行数を変更できます。
ページネーションが英語なのが少し気になったので、Quiita:「AngularMaterialのPaginatorを日本語化する」を参考に日本語化します。
まずプロジェクト内にMatPaginatorIntlをオーバーライドしたクラスを返す関数を記述し、app.module.tsでそれをprovidersに設定します。本当は、関数は別のファイルにしてimportするべきですが、ここではそのままapp.module.tsに記述しています。
app.module.ts
...
import { MatPaginatorIntl, MatPaginatorModule } from '@angular/material/paginator';
@NgModule({
...
providers: [
{ provide: MatPaginatorIntl, useValue:JpPaginator()}
],
bootstrap: [AppComponent]
})
export class AppModule { }
export function JpPaginator(): MatPaginatorIntl {
const customPaginatorIntl = new MatPaginatorIntl();
customPaginatorIntl.itemsPerPageLabel = '行数: ';
customPaginatorIntl.getRangeLabel = (intSelectedPage: number, intPageRowCnt: number, intTotalCnt: number): string => {
if (intTotalCnt <= 0) intTotalCnt = 0;
if (intTotalCnt==0) return "データなし";
let intStart = intSelectedPage * intPageRowCnt+1;
let intEnd = intStart + intPageRowCnt -1;
return '全'+ String(intTotalCnt)+' 件中 '+String(intStart)+' ~ '+ String(intEnd) + ' 件目';
};
return customPaginatorIntl;
}
先のように設定しても、Paginatorが「0 of 0」のまま稼働しない症状に遭遇しました。ViewChildに設定したMatPaginatorがundefinedのままなのが原因のようです。stackoverflow:「MatPaginator gets undefined」によると、MatPaginatorを*ngIfの中に入れているとそういう症状になるそうです。
*ngIfでの運用している部分を、「[style.display]="条件 ? 'block' : 'none'"」とすることで回避できました。
ページネーションの値をHTML(テンプレート)側で[10,20,30]にしておいてデフォルトでは20を選択させておきたいような場合は、正しい方法か不明ですが、次のようにngAfterViewInitへ設定することで実現できました。
app.component.ts
...
ngAfterViewInit() {
setTimeout(()=>{
if (this.paginator) {
this.paginator!.pageSize=20;
this.dataSource.paginator = this.paginator;
}
});
}...
setTimeoutをかませないと「Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value」というエラーがでます。コンポーネントを初期化中に一度設定した項目を後から変更する際に表示されるものです。
またスクリプトからページサイズを変えたい場合は、paginatorのサイズを変更した後、それをテーブルに再度適用させます。
HTML(テンプレート)側で既に選択可能なページのリストを作ってあると思いますが、それに存在しない値でも反映されます。値はラベルの選択肢に加わりますが、一度別の値を選択すると加わった値は消えます。
app.component.ts
...
public changeSize():void {
if (this.paginator) {
this.paginator.pageSize=15;
this.dataSource.paginator = this.paginator;
}
}...
フィルター
特定の値を持つフィルターを作成する場合も、tableDataはMatTableDataSourceである必要があります。
全ての列を対象に、指定したの値と部分一致する行を抽出するには次のようにします。
this.tableData.filter = 'フィルタしたい値';
特定の列だけを対象にしたい場合はfilterPredicateにフィルタリングのルールとなる関数を渡します。
trueを返した場合に行が抽出されるようになります。ここでは「検索したい列」のみを検索し、filter変数に渡す文字列が含まれていたらtrueが返るようにしています。
この方法は「stackoverflow:Filtering specific column in Angular Material Table with filtering in angular?」を参考にさせていただきました。
関数の中身を変えることで、完全一致にすることも可能です。またデフォルトの挙動では検索対象文字列を''(空文字)にすることで、全件抽出に戻すことができます。
this.dataSource.filterPredicate=function(data, filter: string): boolean {
return data.検索したい列.includes(filter);
};
もし複数の列に対して別の値のフィルターを適用したい場合は、工夫が必要です。filterPredicateに設定できる関数の引数は(データ行,値)となっており、値は「文字列」で定義されています。
そのため、filterに「["検索値1","検索値2"]」といった配列や「{ 列3 : "検索値1", 列4 : "検索値2"}」といったようにオブジェクトを渡すことができません。回避策として、それらを一度JSON.stringifyで文字列化して、filterPredicate関数のなかでJSON.parseして戻すといった方法があります。
public setFilterPridicate():void {
this.tableData.filterPredicate=function(data, filter: string): boolean {
let filterParam = JSON.parse(filter);
//(複数の列に対してフィルタリング処理)
if(data.列1==filterParam.値1 && data.列2 > filterParam.値2) {
return true;
} else {
return false;
}
};
}
public applyfilter():void {
let filterParam = { 値1: "テスト", 値2: 10 };
this.tableData.filter=JSON.stringify(filterParam);
}
参考にさせていただきましたサイトの皆様、ありがとうございました。