Marginalia

Angular CDKのPortalを使ったローディングラッパーの実装

今回は Angular CDK(Component Dev Kit)の Portal 機能を使って、ローディングラッパーコンポーネントを実装する例の紹介です。 Angular の基本的な書き方はわかっている前提の内容になります。


ローディングラッパーとは次のようなテンプレートで、ローディング中はローディング表示を、ローディングが終わったら子要素を表示するようなコンポーネントを指しています。 たとえばこのようなテンプレートです。

<mat-card>
  <loading-wrapper [loading]="isLoading$ | async">
    <div>Done!</div>
  </loading-wrapper>
</mat-card>

このように、ローディング状態によってビューが差し替わります。

CdkPortal の使い方

@angular/cdk/portalからインポートできるPortalModuleによって、cdkPortalOutletなどのいくつかのディレクティブが有効になります。

import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { PortalModule } from "@angular/cdk/portal";

import { AppComponent } from "./app.component";
import { LoadingWrapperComponent } from "./loading-wrapper.component";

@NgModule({
  imports: [BrowserModule, PortalModule],
  declarations: [AppComponent, LoadingWrapperComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

cdkPortalOutletディレクティブは、渡されたCdkPortalに紐づくビューをその位置に表示します。

https://material.angular.io/cdk/portal/api#CdkPortalOutlet

<ng-template [cdkPortalOutlet]="contentPortal"></ng-template>

つまり、ローディングラッパーコンポーネントがおこなうことは、ローディング状態に応じてcontentPortalの中身を差し替えることです。

TemplatePortal の作成

CdkPortalはいくつかの種類がありますが、今回はTemplateRefをビューとして保持するTemplatePortalを使います。 ローディング状態のテンプレートをloadingContent、親コンポーネントから渡されるコンテンツ要素をcontentとして、それぞれViewChildでコンポーネントから参照できるようにします。

<ng-template #loadingContent>
  <div>
    <div>Loading...</div>
    <mat-spinner color="accent"></mat-spinner>
  </div>
</ng-template>

<ng-template #content>
  <ng-content></ng-content>
</ng-template>

<ng-template [cdkPortalOutlet]="contentPortal"></ng-template>

コンポーネント側では、初期化時と、ローディング状態を制御するloadingプロパティが変わったときにビューをスイッチするようにします。 次のコードにおけるswitchViewメソッドが、TemplateOutletを作成している部分です。

@Component({
  selector: "loading-wrapper",
  templateUrl: "./loading-wrapper.component.html"
})
export class LoadingWrapperComponent implements OnInit, OnChanges {
  @Input() loading: boolean;

  @ViewChild("loadingContent") loadingContentTemplate: TemplateRef<any>;
  @ViewChild("content") contentTemplate: TemplateRef<any>;

  contentPortal: CdkPortal;

  constructor(private vcRef: ViewContainerRef) {}

  ngOnInit() {
    this.switchView();
  }

  ngOnChanges(changes: SimpleChanges) {
    if (changes.hasOwnProperty("loading")) {
      this.switchView();
    }
  }

  // 現在のローディング状態から適切なTemplatePortalを作成する
  switchView() {
    this.contentPortal = new TemplatePortal(this.getTemplate(), this.vcRef);
  }

  private getTemplate() {
    if (this.loading) {
      return this.loadingContentTemplate;
    }
    return this.contentTemplate;
  }
}

まとめ

  • CdkPortalを使って、状態に応じたビューの差し替えの実装が簡単にできる
  • TemplatePortalを使って、ng-templateから取り出したTemplateRefCdkPortalに変換できる

完成形がこちらです。