Angular MaterialのMatTableでサーバサイドページング

# 前提・概要

Angular MaterialのMatTableでサーバサイドページングを行うには、DataSourceを自前で実装する必要がある。
公式マニュアルには、クライアントサイドページングの例しか載っていなかったため、整理しておく。
もうすぐAngular6が出るが、まだ正式版ではないため、Angular4で実現した方法を記載する。
また、概略を掴みやすくするため、エラー処理は省略している。

# 下準備

## モジュール

MatTableModuleとMatPaginatorModuleをimportしておく。

## エンティティ

ユーザ情報をサーバサイドページングして表示する。

```ts:user.ts
export class User {
  public userId: string;
  public userName: string;
  public email: string;
}
```

## サービス

ページ番号と、1ページの件数を指定して、サーバからレコードを取得する。

```ts:user.service
@Injectable()
export class UserService {
  constructor(private http: HttpClient) { }

  /**
   * ユーザ情報を取得する。
   *
   * @param page 取得するページ番号（0-based）
   * @param size 1ページの件数
   */
  public getUsers(page: number, size: number): Observable<any> {
    return this.http.get('http://localhost:8080/users', {
        params: new HttpParams().set('page', page).set('size', size)
    });
  }
}
```

getUsers()の戻り値は、全件の件数と、指定ページのユーザ情報の配列を持つJSONとする。
Spring Data(JPA)のPageは、この構造に近い形となっている。

```json
{
  "totalElements": 51,
  "contents": [
    {"userId": "user1", "userName": "ユーザ1", "email": "user1@example.com"},
    {"userId": "user2", "userName": "ユーザ2", "email": "user2@example.com"},
    {"userId": "user3", "userName": "ユーザ3", "email": "user3@example.com"},
    {"userId": "user4", "userName": "ユーザ4", "email": "user4@example.com"},
    {"userId": "user5", "userName": "ユーザ5", "email": "user5@example.com"}
  ]
}
```

# Angular Materialとの連携

## DataSourceの作成

「@angular/cdk/collections」のDataSourceを実装するクラスを作成する。
サーバから取得したユーザ情報は、userSubjectを経由して、ObservableとしてAngular Material Tableに渡される（connect()の部分）。

```ts:datasource.ts
export class UserDataSource implements DataSource<User> {
  /** ユーザ情報 */
  private userSubject = new BehaviorSubject<User[]>([]);

  /** 全レコード数 */
  public totalElements: number;

  constructor(private userService: UserService) { }

  connect(collectionViewer: CollectionViewer): Observable<User[]> {
    return this.userSubject.asObservable();
  }

  disconnect(collectionViewer: CollectionViewer): void {
    this.userSubject.complete();
  }

  /**
   * ユーザ情報を取得する。
   *
   * @param page 取得するページ番号（0-based）
   * @param size 1ページの件数
   */
  loadMaster(page: number = 0, size: number = 5): void {
    this.userService.getUsers(page, size)
      .subscribe(response => {
        this.totalElements= response.totalElements;
        this.userSubject.next(response.contents);
      });
  }
}
```

## コンポーネント

ngOnInitでDataSourceを作成し、最初のページをサーバから取得する。
ページングが行われた場合に、そのページのユーザ情報をサーバから取得する処理を、ngAfterViewInitで登録する。

```ts:user-list.component.ts
@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html',
  styleUrls: ['./user-list.component.css']
})
export class UserListComponent implements AfterViewInit, OnInit {
  /** ユーザ情報をMatTableで表示するためのDataSource */
  public dataSource: UserDataSource;

  /** ページングコンポーネント */
  @ViewChild(MatPaginator)
  public paginator: MatPaginator;

  /** カラムの表示順序 */
  public columns = ['userId', 'userName', 'email'];

  constructor(private userService: UserService) { }

  ngOnInit() {
    this.dataSource = new UserDataSource(this.userService);
    this.dataSource.loadMaster();
  }

  ngAfterViewInit() {
    this.paginator.page.map(() => this.loadMaster()).subscribe();
  }

  private loadMaster() {
    this.dataSource.loadMaster(this.paginator.pageIndex, this.paginator.pageSize);
  }
}
```

## テンプレート

クライアントサイドページングと同じように、mat-tableのdataSourceに、作成したDataSourceを指定する。

```html:user-list.component.html
<div>
  <mat-table [dataSource]="dataSource">
    <ng-container matColumnDef="userId">
      <mat-header-cell *matHeaderCellDef>ユーザID</mat-header-cell>
      <mat-cell *matCellDef="let record">{{record.userId}}</a></mat-cell>
    </ng-container>
    <ng-container matColumnDef="userName">
      <mat-header-cell *matHeaderCellDef>ユーザ名</mat-header-cell>
      <mat-cell *matCellDef="let record">{{record.userName}}</mat-cell>
    </ng-container>
    <ng-container matColumnDef="email">
      <mat-header-cell *matHeaderCellDef>メールアドレス</mat-header-cell>
      <mat-cell *matCellDef="let record">{{record.email}}</mat-cell>
    </ng-container>
    <mat-header-row *matHeaderRowDef="columns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: columns"></mat-row>
  </mat-table>
  <mat-paginator [length]="dataSource.totalElements" [pageSize]="5"></mat-paginator>
</div>
```

これで、ページングを行うたびにUserComponentのloadMaster()が呼ばれて、サーバサイドにユーザ情報を取りに行くようになった。

# 終わりに

DataSourceの作成がひと手間かかるが、それさえできれば簡単に実現できた。