余白

https://blog.lacolaco.net/ に移転しました

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

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


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

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

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

f:id:lacolaco:20180701162327g:plain

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に変換できる

完成形がこちらです。

stackblitz.com