はじめに
「 [Angular] ngComponentOutlet を使ってタブを実装する 」 で ngComponentOutlet
を使用したタブの実装例について記事を書いた。本記事では同じく ngComponentOutlet
を使った実装例として、モーダルダイアログの実装を試してみたい。
モーダルダイアログについては下記を参照。
更新情報
2021/01/11
- 記事内で扱ったコードを Angular
v11.0.5
で確認しました
作業環境
環境 | バージョン | 備考 |
---|---|---|
Angular CLI |
|
$ ng --version |
Angular |
|
同上 |
TypeScript | v4.0.2 | 同上 |
Node.js |
|
$ node --version |
npm |
|
$ npm --version |
ng version の結果
$ ng version
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI: 11.0.5
Node: 12.18.3
OS: darwin x64
Angular: 11.0.5
... animations, cli, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router
Ivy Workspace: Yes
Package Version
---------------------------------------------------------
@angular-devkit/architect 0.1100.5
@angular-devkit/build-angular 0.1100.5
@angular-devkit/core 11.0.5
@angular-devkit/schematics 11.0.5
@schematics/angular 11.0.5
@schematics/update 0.1100.5
rxjs 6.6.0
typescript 4.0.2
本記事で扱う構成
作成にあたってのアプリの構成は次の通り。
src/
└ app/
├ component/
│ └ modal/ # モーダルダイアログ
│ ├ modal.component.css
│ ├ modal.component.html
│ └ modal.component.ts
├ service/ # モーダルを管理するサービス
│ └ modal.service.ts
├ app.component.css
├ app.component.html
├ app.component.ts
└ app.module.ts
方針
- モーダルダイアログはヘッダ/ボディ/フッタを持ち、フッタに close ボタンを配置する
- モーダルダイアログは呼び出し元の画面からボタンクリックで表示する
- モーダルダイアログを表示したらオーバーレイで呼び出し元の画面への操作を抑止する
- モーダルダイアログの close ボタンクリックで閉じる
実装
app.component
app.component ではモーダルダイアログを表示するためのベースとなる画面を作成し、ngComponentOutlet
を利用したモーダルダイアログの呼び出しを行う。
以下、app.component.html と app.component.ts の実装について触れる。
app.component.css は単純なスタイル定義を担当しているだけなので説明を省略する。
app.component.html
<div class="footer">
<input id="footer_button" type="button" value="open modal" (click)="onClick($event);">
</div>
<ng-container *ngComponentOutlet="modal"></ng-container>
ここでの実装の流れは下記のとおり。
- フッターに配置したボタンクリックでイベントをコンポーネントに送り
- コンポーネントは
modal
にModalComponent
をセット -
modal
に実体(ModalComponent
)がセットされたのでngComponentOutlet
によってモーダルを表示する
つまりテンプレートはイベントトリガーとしての役割を持ち、次の app.component.ts との連携がポイントとなる。
app.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
// モーダルダイアログとして表示するコンポーネント
import { ModalComponent } from './component/modal/modal.component';
// モーダルダイアログを閉じるためのイベントを管理するサービス
import { ModalService } from './service/modal.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
// モーダルダイアログが閉じた際のイベントをキャッチするための subscription
private subscription!: Subscription;
// ngComponentOutlet にセットするためのプロパティ
public modal: any = null;
/**
* コンストラクタ
*
* @memberof AppComponent
*/
constructor(
private modalService: ModalService
) {}
/**
* 初期処理
*
* @memberof AppComponent
*/
ngOnInit() {
// モーダルダイアログを閉じた際のイベントを処理する
this.subscription = this.modalService.closeEventObservable$.subscribe(
() => {
// プロパティ modal に null をセットすることでコンポーネントを破棄する
// このタイミングで ModalComponent では ngOnDestroy が走る
this.modal = null;
}
);
}
/**
* 終了処理
*
* @memberof AppComponent
*/
ngOnDestroy() {
this.subscription.unsubscribe();
}
/**
* クリックイベント
*
* @param {*} $event イベント情報
* @memberof AppComponent
*/
public onClick($event: any) {
this.setModal();
}
/**
* モーダルダイアログを表示する
*
* @private
* @memberof AppComponent
*/
private setModal() {
this.modal = ModalComponent;
}
}
ポイントは以下の 2 点。
- クリックイベント
onClick
でngComponentOutlet
にセットするコンポーネント:ModalComponent
をセット -
Subscription
によってモーダルダイアログが閉じた際のイベントをキャッチ
上記 2 点によって
- テンプレートでボタンクリック -> モーダルダイアログを表示する
- モーダルダイアログで close ボタンクリック -> モーダルダイアログを閉じる
という動きを実現している。
modal.service
app.modal.service はモーダルダイアログの管理を担当する。本記事の例では close イベントを管理するだけだが、その内容について modal.service.ts で説明する。
modal.service.ts
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ModalService {
// データの変更を通知するためのオブジェクト
private closeEventSubject = new Subject<string>();
// Subscribe するためのプロパティ( これでイベント通知をキャッチする )
public closeEventObservable$ = this.closeEventSubject.asObservable();
/**
* コンストラクタ
*
* @memberof ModalService
*/
constructor() { }
/**
* イベント通知のリクエストを処理する( モーダルダイアログを閉じる )
*
* @memberof ModalService
*/
public requestCloseModal() {
this.closeEventSubject.next();
}
}
前述のとおり、このサービスの役割はひとつだけ。
-
requestCloseModal()
でモーダルダイアログを閉じるためのイベント処理を受け付け、イベントを発火する
これのみとなる。
なお、ここで行っていることの詳細( Subject
を使用したイベントのやり取り )については、手前味噌ながら「 [Angular] サービスを使用してデータをコンポーネント間で共有する 」で説明しているので、ご興味あればそちらを参照いただきたい。
modal.component
modal.component はモーダルダイアログの本体であるコンポーネント。役割はモーダルダイアログを表示して閉じるだけのものとなる。
ただその中でもオーバーレイやダイアログを閉じる際の処理について触れる部分があるので、その説明を modal.component.html, modal.component.css, modal.component.ts で行う。
modal.component.html
<div class="overlay">
<div class="modal">
<div class="header">
<label id="header_label">header title</label>
</div>
<div class="contents">
contents body
</div>
<div class="footer">
<input id="close_button" type="button" value="close" (click)="onClick($event)">
</div>
</div>
</div>
ここでのポイントは
-
<div class="overlay">
でオーバーレイを実装している
こと。
これと後述の スタイルシート によって呼び出し元の app.component.html とモーダルダイアログとの間にオーバーレイが挟まるので、モーダルダイアログ表示中は呼び出し元の画面に対して操作ができなくなる。
modal.component.css
.overlay {
margin: 0;
padding: 0;
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
background: rgba(0, 0, 0, 0.4);
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
}
.modal {
overflow: hidden;
height: 200px;
width: 400px;
background-color: rgb(229, 241, 247);
border-radius: 15px;
box-shadow: 10px 10px 10px rgba(0,0,0,0.4);
border: solid 1px rgb(0, 0, 0);
z-index: 1001;
}
.header {
height: 20%;
width: 100%;
font-size: 1.2em;
border-bottom: solid 1px rgb(0, 0, 0);
display: flex;
justify-content: center;
align-items: center;
}
.header > #header_label {
display: block;
}
.contents {
height: 60%;
width: 100%;
text-align: center;
font-size: 1.8em;
}
.footer {
height: 20%;
width: 100%;
border-top: solid 1px rgb(0, 0, 0);
display: flex;
justify-content: center;
align-items: center;
}
.footer > #close_button {
display: block;
width: 30%;
height: 70%;
font-size: 1.2em;
}
.overlay
は modal.component.html でポイントとして取り上げた <div class="overlay">
のスタイル定義。
この要素で position: absolute
, width:100%
, height:100%
を指定していることで、親要素( ここでは呼び出し元画面 )に対して全体を覆っている。
modal.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
// モーダルダイアログを閉じるためのイベントを管理するサービス
import { ModalService } from '../../service/modal.service';
@Component({
selector: 'app-modal',
templateUrl: './modal.component.html',
styleUrls: ['./modal.component.css']
})
export class ModalComponent implements OnInit, OnDestroy {
/**
* コンストラクタ
*
* @param {ModalService} modalService
* @memberof ModalComponent
*/
constructor(
private modalService: ModalService
) {}
/**
* 初期処理
*
* @memberof ModalComponent
*/
ngOnInit() {}
/**
* 終了処理
*
* @memberof ModalComponent
*/
ngOnDestroy() {
// モーダルダイアログが閉じたタイミングで出力される
console.log('destroyed');
}
/**
* クリックイベント
*
* @param {*} $event イベント情報
* @memberof ModalComponent
*/
public onClick($event: any) {
this.notifyCloseModal();
}
/**
* モーダルダイアログを閉じる
*
* @private
* @memberof ModalComponent
*/
private notifyCloseModal() {
this.modalService.requestCloseModal();
}
}
こちらはモーダルダイアログで描画している close
ボタンの制御に関する内容となる。
ポイントは
public onClick($event) {
this.notifyCloseModal();
}
private notifyCloseModal() {
this.modalService.requestCloseModal();
}
の部分で、 close
ボタンがクリックされたら modal.service.ts の requestCloseModal()
メソッドを実行している。その後は app.component.ts に処理が流れてモーダルダイアログが閉じる動きとなる。
で、このときの処理の流れを図にしたものが以下のシーケンス。
ちなみにシーケンス上で Service から App に対して発火されている「 イベント通知 」は、厳密には App に対してのみ発火されているのではない。
「 Service が対象を指定せずに発火したイベントを App がキャッチしている 」 というのが正しい解釈になるのだが、シーケンス図として表現するには図のようにしたほうがイメージしやすいためそうしている。
またこのシーケンスの登場人物と各ファイルの対応は以下の表を参照。
-
登場人物と各ファイルの対応表
登場人物 ファイルとの対応 View modal.component.html Component modal.component.ts Service modal.service.ts App app.component.ts
実行結果
-
初期表示
「open modal」 ボタンをクリックすることでモーダルダイアログが表示される
-
モーダルダイアログを表示
「close」ボタンをクリックすることでモーダルダイアログが閉じる
-
モーダルダイアログを閉じたときのログ
モーダルダイアログが閉じたときのログ。「 destroyed 」が出力されていることから、 modal.component.ts の
ngOnDestroy()
が実行されていることがわかる。
まとめ
ひとつのボタンにひとつのモーダルダイアログを紐付けただけのなんのひねりもない例だが、 ngComponentOutlet
を使用してモーダルダイアログが簡単に実装できた。
やっていることは ngIf
や ngSwitch
でも実現できるが、 ngComponentOutlet
を使うことの強みはやはり 抽象化 だと思う。
「 [Angular] ngComponentOutlet を使ってタブを実装する 」でもそうだったが、 ngComponentOutlet
セットされたコンポーネントによって表示する内容を動的に切り替えることができるのはコードがスッキリして気持ちが良い。
ソースコード
今回の記事で作成したコードは こちら にアップしてあるのでご参考まで。