Angular Material で、処理中のスピナーに進捗%をテキスト表示する

はじめに
---
Angular Material の Progress Spinner で進捗%をテキスト表示したのだが、意外とハマったので備忘録として作成。
やりたいことは以下の画像のようにしたいだけだが、テキスト表示を追加するにはコンポーネントを自作しないといけないっぽい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285246/2d350799-0141-f3e8-99c1-14b7a9dec73a.png)

デザインなどは別途修正するとして、とりあえず必要最低限のコードだけ抜粋。

コンポーネント
---

まずはコンポーネントを作成する。
html, ts, scssの3セット。

```progressSpinner.component.html
<mat-progress-spinner
  mode="indeterminate"
  diameter="100"
  strokeWidth="8"
>
</mat-progress-spinner>
<div class="message">{{ message }}</div>
```

```progressSpinner.component.ts
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
  selector: 'app-progress-spinner',
  templateUrl: './progressSpinner.component.html',
  styleUrls: [ './progressSpinner.component.scss' ],
  encapsulation: ViewEncapsulation.None,
})
export class ProgressSpinnerComponent {
  message: string;

  constructor() {}
}
```

```progressSpinner.component.scss
.progress-spinner {
    color: white;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}
  
.progress-spinner-backdrop {
    background-color: rgba(0, 0, 0, .30)!important;
}

.message {
    font-size: x-large;
    color: white;
    position: absolute;
    text-align: center;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}
```

スピナーとテキストを持つだけのシンプルな構成。
スタイルシートはグローバルの styles.scss に書きたくなかったので切り出しているが、`ViewEncapsulation.None`を付けないと正しく読み込めなかった。
（雑だが、主題とは関係ないので、とりあえず割愛。）

サービス
---
コンポーネントを生成、管理するためのサービスを追加。
overlay の attach、detach などもこちらで管理する。

overlay を生成する時に、panelClass、backdropClass に先ほどのスタイルシートで定義したクラスをセットしている。

```progressSpinner.service.ts
import { ComponentRef } from '@angular/core';
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { Injectable } from '@angular/core';

import { ProgressSpinnerComponent } from './progressSpinner.component';

@Injectable({
  providedIn: 'root'
})
export class ProgressSpinnerService {

  overlayRef: OverlayRef;
  spinner: ComponentRef<ProgressSpinnerComponent>;

  constructor(
    private readonly overlay: Overlay,
  ) {}

  // show progress spinner
  public show(): void {
    const portal = new ComponentPortal(ProgressSpinnerComponent);

    this.overlayRef = this.overlay.create({
      width: '100%',
      height: '100%',
      hasBackdrop: true,
      panelClass: 'progress-spinner',
      backdropClass: 'progress-spinner-backdrop',
    });
    this.spinner = this.overlayRef.attach(portal);
  }

  public close(): void {
    this.overlayRef.detach();
    this.overlayRef.dispose();
  }

  public setMessage(message): void {
    this.spinner.instance.message = message;
  }
}
```

app.module.ts
---
他から呼び出せるようにするために、MatProgressSpinnerModule、ProgressSpinnerComponent の定義を app.module.ts に追加する。

```app.module.ts
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ProgressSpinnerComponent } from './views/common/components/progressSpinner/progressSpinner.component';
・
・
・
@NgModule({
    declarations: [
        AppComponent,
        ProgressSpinnerComponent,
　　　　・
　　　　・
　　　　・
    imports: [
　　　　・
　　　　・
　　　　・
        MatProgressSpinnerModule,
```

呼び出し方
---
あとは任意のコンポーネントからサービスを import し、show → setMessage →・・・→ close としてやればOK。

```typescript
import { ProgressSpinnerService } from '../../common/components/progressSpinner/progressSpinner.service';
　　　　・
　　　　・
　　　　・
  constructor(
    private spinner: ProgressSpinnerService,
  ) {}
　　　　・
　　　　・
　　　　・
  test() {
    try {
      // show spinner
      this.spinner.show();

      this.spinner.setMessage('ファイルアップロード中 10%');
　　　　・
　　　　・
　　　　・
      this.spinner.setMessage('ファイルアップロード中 20%');
　　　　・
　　　　・
　　　　・

    } catch (e) {
      alert(e.message);
    } finally {
      // stop spinner
      this.spinner.close();
    }
  }
```

※ とりあえずスピナーは1つしかない前提。
　　複数ある場合、コンポーネントのインスタンスを返すようにするか、あるいはサービスを複数Injectするか。まあだいたい1つで事足りるでしょうけど。