はじめに

何やかんやでAngularネタを書くのはわりと久しぶりです。

長らくbetaだったAngular MaterialもいよいよRCを迎えています。
今日は、Angular MaterialのサブプロジェクトであるAngular Component Dev Kit(以下CDK)について簡単に紹介していきます。

CDKを学ぶべき理由

Angular Materialのステータスアップデート にもあるとおり、

CDKの目的は、Webのための素晴らしいコンポーネントを開発するためのツールを開発者に提供することです。これは、Material Designビジュアル言語を採用せずにAngular Materialの機能を利用したいプロジェクトに特に役立ちます。

CDKはUIライブラリ開発にありがちな機能を、コンポーネント開発基盤としてまとめたパッケージです。

「CSSをデザイナが提供してくれるけど、UIライブラリは自分たちで作らなければ...」といったケースは比較的よくあると思いますが、CDKはそのようなケースにうってつけです。

車輪の再発明を防ぐためにも、まずはどんな機能が提供されているのか知っておくに越したことはありません。

また、CDKはライブラリのためのライブラリというポジションですが、命名規則やDirectiveの粒度、Angularの本体機能をどのように利用するか、等の点において、Angularを深く知りたいエンジニアが参考にすべき箇所が山のようにあります。
そういった意味でも一度見てみることをオススメします。

CDKが提供する機能

CDKには以下のモジュールから構成されています。Angularのコアパッケージと同様、モジュール毎の選択的なインストールが可能です。

モジュール名 概要
Accessibility a11y機能。スクリーンリーダーによる読み上げを行う LiveAnnouncer や、リスト要素をキーボードで選択可能にする際に用いる ListKeyManager などが含まれています
Observers MutationObserver をemitするDirectiveが含まれています
Layout レスポンシブデザインを実装する上であると便利なService達です。ブレークポイントを監視するなど
Overlay フローティングパネルの描画に必要となるServiceやDirectiveが含まれています。後述します
Portal Angular Componentを動的に操作するためのモジュールです。後述します
Bidirectionality テキストを左から右へ読むか(LTR)、右から左へ読むか(RTL)の情報を知ることのできる Directionality Serviceが含まれています
Scrolling スクロール可能なコンテナを作成するためのDirectiveとServiceが含まれています
Table <mat-table> の原型である <data-table> を含んでいます
Stepper ウィザートUIを構築するためのDirectiveを含んでいます

恐らく今後も増えていくと思います。
Angular Materialが機能を提供しており、UI基盤として汎用性が認められれば、リファクタリングされてCDKに追加されることもあるでしょう1

CDKを触ってみよう

それでは早速CDKを触っていきましょう。今回はOverlayモジュールを利用して、自作のフローティングパネルを作成していきます。

インストール

Angular CLIでサクッとプロジェクトを作り、 @angular/cdk をインストールします。

npm i -g @angular/cli
ng new <プロジェクト名>
cd <プロジェクト名>
npm install @angular/cdk@5.0.0-rc.2

CDKモジュールのimport

続いて、 OverlayModule と PortalModule をimportします。これらを AppModule の importsに加えましょう。

src/app/app.module.ts
import { OverlayModule } from '@angular/cdk/overlay';
import { PortalModule } from '@angular/cdk/portal';

OverlayModule だけでなく PortalModule を利用する理由については後述します。

なお、Overlay モジュールには標準のCSSが付属しています。こちらもCSS importしておきましょう。

src/styles.css
@import "~@angular/cdk/overlay-prebuilt.css";

これでOverlayモジュールの利用準備が整いました。

余談ですが、僕ははじめてCDKを触ったとき、CSSのimportを見逃したお陰で1時間くらい無駄にしました。
すべてのCDKモジュールにCSSが付属しているわけではありません。現状だと特にドキュメンテーションされていないので、GitHubレポジトリを覗いて、<モジュール名>-prebuilt.scssというファイルが含まれているか確認しておくと良いでしょう。
Overlay以外だとAccessibilityモジュールなどにも標準のCSSが付属しています。

Overlayの中身を作る

Overlayを利用するには、フローティングパネルの中身が必要です。
まずは中身となるComponentを作成しましょう。

ng generate component my-first-panel

NgModule の entryComponents に、作成された MyFirstPanelComponent を追加します。

src/app/app.module.ts
@NgModule({
  /* 中略 */
  entryComponents: [MyFirstPanelComponent],
})
export class AppModule { }

MyFirstPanelComponent はテンプレートHTMLからは呼び出されず、TypeScriptでプログラマティックに描画されます。
このようなクラスは明示的にAngularのComponent探索対象に加える必要があるためです。

さて、作成したComponentがフローティングパネルであることが見た目にもわかりやすいように、スタイルをいじっておきます。

src/app/my-first-panel/my-first-panel.component.css
:host {
  display: block;
  width: 400px;
  height: 300px;
  background: #888;
}

はじめてのOverlay

これで準備が整いました!Overlayしてみましょう。

src/app/app.component.ts を下記のように編集しましょう。

import { Component, OnInit, ViewContainerRef } from '@angular/core';
import { Overlay } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { MyFirstPanelComponent } from './my-first-panel/my-first-panel.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'app';

  constructor(private overlay: Overlay) { }

  ngOnInit() {
    const overlayRef = this.overlay.create();
    overlayRef.attach(new ComponentPortal(MyFirstPanelComponent));
  }
}

ng serve でアプリを起動して画面を確認してみましょう。
いつものAngular CLI初期画面に、"my-first-panel works!" というグレーのパネルがオーバーレイされていればOKです。

Portal

Overlayの機能を追っていく前に、Portalについて触れておきます。

Portalは「どこかにレンダリングしたいなにか」を表しており、MaterialおよびCDKの中でも中核をなす概念です。
例えば、さきほどの例では MyFirstPanelComponent というパネルを用意しましたが、このパネルがPortalに相当します。

より厳密にいうと、先述のコードに登場した new ComponentPortal(MyFirstPanelComponent) がPortalを作る処理そのものです。

Portalがレンダリングしたい対象の「中身」であるならば、そのPortalに対応した「外側」も存在します。
それがPortalOutletです。
Angualr において xxxOutlet という名前をみたら、何かがマウントされる場所のことです(e.g. RouterOutlet, ComponentOutlet)。

Poratal.png

PortalはPortalOutletに関連付けられることではじめてブラウザ上に描画されます。この関連付けには attach メソッドを利用します。

portalOutlet.attach(portal);

// こちらでも可
portal.attach(portalOutlet);

勘の良い方はお気づきでしょうが、さきほどのコードで const overlayRef = this.overlay.create(); として作成した overlayRef は PortalOutletが実装されているため、Portalをattachできた、というわけです。
先述の図をOverlayに適用すると、次のような関係になっています。

Overlay.png

Overlayの場合、PortalOutletを囲むContainer Elementが存在しますが、こいつはただのHTML要素です。
CDKが自動的に生成する要素であり、基本的に利用者が意識する必要はありません2

このように、CDKにおいて動的な描画が必要な場合、Portalが登場します。現状だとOverlay以外にPortalを利用するCDKモジュールは存在していませんが、知識として知っておいて損はないでしょう。

テンプレートを利用したOverlay

さきほどは ComponentPortal を使って、ComponentクラスからPortalを作成しましたが、テンプレートを使ってPortalを作成する方法も存在します。

CdkPortal Directiveを利用すると、ng-templateからPortalを取り出すことができます。

src/app/app.component.html
<div *cdk-portal>
  Template Portal!
  <button (click)="templatePortal.detach()">click to detach</button>
</div>
src/app/app.component.ts
export class AppComponent implements OnInit {

  @ViewChild(CdkPortal) templatePortal: CdkPortal;

  constructor(private overlay: Overlay) { }

  ngOnInit() {
    const overlayRef = this.overlay.create();
    this.templatePortal.attach(overlayRef);
  }
}

Componentのクラスを作る必要がないので、ちょっとしたトースターのようなシンプルなPortalを構築する場合は、こちらのやり方の方が楽です。
逆に、モーダルのような、テンプレートもインタラクションも複雑なPortalの場合は、きちんとComponentに分割して ComponentPortal を使ってPortal化するとよいでしょう。

位置調整

ここからは純粋にOverlayの機能の話をします。

overlay.create() の引数に OverlayConfig型のインスタンスを渡すことで、Overlayにさまざまなオプションを設定できます。

例えば、Portalを上下左右中央に設置したい場合、下記のように書きます。

const positionStrategy = this.overlay.position()
    .global().centerHorizontally().centerVertically();
const config = new OverlayConfig({
  positionStrategy,
});
const overlayRef = this.overlay.create(config);

overlay.position().global() は GlobalPositionStrategy というブラウザのViewPort(正確にはcdk-overlay-container)に対する絶対座標系としてPortalを描画する際に利用します。

.top('100px').bottom('100px') のように値を直接指定することも可能です。

positionStrategyに指定可能なクラスとして、ConnectedPositionStrategyというのもあります。
これは ElementRef で指定した特定の要素からの相対的な位置でPortalを描画したいときに用います。具体的にはツールチップとかでしょうか。

const positionStrategy = this.overlay.position().connectedTo(targetElementRef, {
  originX: 'center',
  originY: 'bottom'
}, {
  overlayX: 'center',
  overlayY: 'center',
}).withOffsetX(100);

背景とスクロール制御

hasBackdropを true にセットすると、Overlayにバックドロップが有効化されます。

const config = new OverlayConfig({
  hasBackdrop: true,
});
const overlayRef = this.overlay.create(config);

デフォルトではprebuilt CSSに定義されたスタイルが適用されますが、バックドロップに任意のスタイルを当てたい場合は、さらに backdropClass を指定するとよいでしょう。

バックドロップはモーダルやダイアログで必要とされるケースが多いと思いますが、このようにバックドロップで背景要素をガードするケースではスクロールを抑止したいという要件がセットで付いてくることも多いと思います。
そんな場合は、scrollStrategyを利用するとよいです。

例えば、下記のようにすると、背景でのスクロールが一切行えなくなります。

const scrollStrategy = this.overlay.scrollStrategies.block();
const config = new OverlayConfig({
  scrollStrategy,
  hasBackdrop: true,
});
const overlayRef = this.overlay.create(config);

scrollStrategiesには、block以外にも以下が選択できます。

  • noop: 何もしない
  • close: スクロールが発生したらOverlayを閉じる
  • reposition: スクロール発生時にOverlayの位置を更新する

おわりに

Angular 2が出たてのころに自分でモーダルを作る必要があったため、当時はゼロから書いた記憶があります3

やってみるとわかりますが、CDKが提供するPortalのような仕組みはどのような見た目にするかに関わらず、必ず必要になる機能ですし、上下左右中央揃え + スクロール抑止 要件とか、あるあるな割に地味に書くのが面倒なので、この部分だけ切り出して提供してくれるのは本当に助かると思います。

一方で、APIドキュメントは割としっかりと書いてあるのですが、サンプルやblog記事は殆ど見当たりません。
現状ではCDKの利用方法を知るにはAngular Materialのソースコードを見て、利用箇所を追っていくのが確実です。

今回はPortalとOverlayの紹介に留めましたが、機会があれば別のCDKモジュールについても紹介していきたいです。

明日は @studioTeaTwo さんです。


  1. 例えば、Tabs機能がFeature Requestされていますね。 

  2. OverlayContainer Serviceを使うと、この要素を取得することは可能です。多少込み入ったことをしたい場合、こいつに手を入れることがあるかも。 

  3. 自作モーダルの詳細については https://qiita.com/Quramy/items/ccfcfa0e45dd9e43f041 を参照。 

1473681159
Front-end web developer. TypeScript, Angular and Vim, weapon of choice.