Angular CDKのCdkDialogでダイアログを爆速実装


これは[Angular Advent Calendar 2023](https://qiita.com/advent-calendar/2023/angular)の8日目の記事です。

## はじめに

今年の Angular は Signal や Control Flow など盛りだくさんでした。これらの新機能も気になるところですが、他の方が素晴らしい記事をどんどん上げてくださっているので、僕は今年も Angular CDK の布教活動に努めたいと思います。

今回はアプリケーションを作る上で欠かせないモーダルダイアログを CdkDialog を利用して実装していきます。


※ 先に完成したものも置いておきます。

https://stackblitz.com/edit/stackblitz-starters-wrs3zq?embed=1&file=src%2Fmain.ts

## ダイアログの表示

CdkDialogはダイアログで表示したいComponentさえ用意すれば後は数行で実装できるので早速やってみましょう。

ダイアログで表示したいコンポーネントを作成
```typescript
@Component({
  selector: 'app-dialog',
  standalone: true,
  template: `
    <div style="background-color: white; padding: 24px;">
      <p>Dialog by CdkDialog</p>
    </div>
  `,
})
export class DialogComponent {}
```

CdkDialogの `open()` で呼び出す

```typescript
import { DialogModule, Dialog } from '@angular/cdk/dialog';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <button (click)="openDialog()">Open Dialog</button>
  `,
  imports: [DialogModule],
})
export class App {
  private readonly dialog = inject(Dialog);

  openDialog() {
    this.dialog.open(DialogComponent);
  }
}
```

![Dialog](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138755/661ec24c-8e21-1a3b-f55b-c2dea6bb4422.png)

たったこれだけで、シンプルなモーダルダイアログを実装出来ます。思ったよりずっと簡単ですよね。


## 見た目の共通化

CdkDialogでは、DialogConfigのcontainerオプションを利用することで共通の見た目を実装できます。


```typescript
import { PortalModule } from '@angular/cdk/portal';

@Component({
  selector: 'app-dialog-container',
  standalone: true,
  template: `
    <div style="background-color: white; padding: 24px;">
      <ng-template cdkPortalOutlet></ng-template>
    </div>
  `,
  imports: [PortalModule],
})
export class DialogContainer extends CdkDialogContainer {}
```

上記のように `CdkDialogContainer` を継承したコンポーネントを作成したらcontainerオプションに指定するだけ。

```typescript
openDialog() {
  this.dialog.open(DialogComponent, {
    container: DialogContainer
  });
}
```

これで全てのダイアログの見た目を共通化できます。

## 振る舞いの共通化

「ESCボタンを押したら閉じる」などダイアログに共通した振る舞いも DialogContainer に実装することで簡単に共通化できます。

```typescript
import { DialogRef } from '@angular/cdk/dialog';
import { ESCAPE } from '@angular/cdk/keycodes';

export class DialogContainer extends CdkDialogContainer {
  private readonly dialogRef = inject(DialogRef);

  @HostListener('keydown', ['$event'])
  handleKeydown(event: KeyboardEvent) {
    const { keyCode } = event;
    if (keyCode === ESCAPE) {
      this.dialogRef.close();
    }
  }
}
```

※ DialogRefは自作ダイアログを実装する上でかなり便利なクラスですが、今回説明は省きます。もし気になる方は、[公式ドキュメント](https://material.angular.io/cdk/dialog/api#DialogRef)を参照してください。

## 呼び出しの共通化

最後に呼び出し側のコードの冗長化を防ぐためにサービスを作成しましょう。

```typescript
@Injectable({
  providedIn: 'root',
})
export class DialogService {
  private readonly dialog = inject(Dialog);

  open<C>(component: ComponentType<C>) {
    return this.dialog.open(component, {
      container: DialogContainer,
    });
  }
}
```

```typescript
private readonly dialog = inject(DialogService);

openDialog() {
  this.dialog.open(DialogComponent);
}
```

これで呼び出しもスッキリし、細かい設定なども隠蔽することが出来ました。

## まとめ

今回、改めてCdkDialogを使った共通化をやってみて、こんなだったっけ・・・？と自分で思うくらい簡単で驚いています。
Angular CDKの記事はまだまだ少なく、取っつきづらいイメージもあるかもしれませんが、今回のCdkDialogのように簡単で便利な機能がたくさんあります。

特にダイアログの共通化は比較的難易度の高い部類に入ると思うので、ぜひCdkDialogを使ってみてください。

明日は @scrpgil さんです！