LoginSignup
26
23

More than 1 year has passed since last update.

[Angular] ngComponentOutlet を使ってモーダルダイアログを実装する

Last updated at Posted at 2018-09-24

はじめに

[Angular] ngComponentOutlet を使ってタブを実装する 」 で ngComponentOutlet を使用したタブの実装例について記事を書いた。本記事では同じく ngComponentOutlet を使った実装例として、モーダルダイアログの実装を試してみたい。

モーダルダイアログについては下記を参照。

更新情報

2021/01/11

  • 記事内で扱ったコードを Angular v11.0.5 で確認しました

作業環境

環境 バージョン 備考
Angular CLI v6.1.3 v11.0.5 $ ng --version
Angular v6.1.2 v11.0.5 同上
TypeScript v4.0.2 同上
Node.js v9.4.0 v12.18.3 $ node --version
npm v6.1.0 v6.14.6 $ 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

方針

実装

app.component

app.component ではモーダルダイアログを表示するためのベースとなる画面を作成し、ngComponentOutlet を利用したモーダルダイアログの呼び出しを行う。

以下、app.component.htmlapp.component.ts の実装について触れる。
app.component.css は単純なスタイル定義を担当しているだけなので説明を省略する。

app.component.html

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>

ここでの実装の流れは下記のとおり。

  • フッターに配置したボタンクリックでイベントをコンポーネントに送り
  • コンポーネントは modalModalComponent をセット
  • modal に実体( ModalComponent )がセットされたので ngComponentOutlet によってモーダルを表示する

つまりテンプレートはイベントトリガーとしての役割を持ち、次の app.component.ts との連携がポイントとなる。

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 点。

  • クリックイベント onClickngComponentOutlet にセットするコンポーネント: ModalComponent をセット
  • Subscription によってモーダルダイアログが閉じた際のイベントをキャッチ

上記 2 点によって

  • テンプレートでボタンクリック -> モーダルダイアログを表示する
  • モーダルダイアログで close ボタンクリック -> モーダルダイアログを閉じる

という動きを実現している。

modal.service

app.modal.service はモーダルダイアログの管理を担当する。本記事の例では close イベントを管理するだけだが、その内容について modal.service.ts で説明する。

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

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

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;
}

.overlaymodal.component.html でポイントとして取り上げた <div class="overlay"> のスタイル定義。
この要素で position: absolute, width:100%, height:100% を指定していることで、親要素( ここでは呼び出し元画面 )に対して全体を覆っている。

modal.component.ts

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 ボタンの制御に関する内容となる。
ポイントは

closeボタンクリック時の処理(コメントは除去済み)
  public onClick($event) {
    this.notifyCloseModal();
  }

  private notifyCloseModal() {
    this.modalService.requestCloseModal();
  }

の部分で、 close ボタンがクリックされたら modal.service.tsrequestCloseModal() メソッドを実行している。その後は app.component.ts に処理が流れてモーダルダイアログが閉じる動きとなる。

で、このときの処理の流れを図にしたものが以下のシーケンス。

modal04.png

ちなみにシーケンス上で Service から App に対して発火されている「 イベント通知 」は、厳密には App に対してのみ発火されているのではない。
Service が対象を指定せずに発火したイベントを App がキャッチしている 」 というのが正しい解釈になるのだが、シーケンス図として表現するには図のようにしたほうがイメージしやすいためそうしている。

またこのシーケンスの登場人物と各ファイルの対応は以下の表を参照。

  • 登場人物と各ファイルの対応表

    登場人物 ファイルとの対応
    View modal.component.html
    Component modal.component.ts
    Service modal.service.ts
    App app.component.ts

実行結果

  1. 初期表示

    modal01.png

    「open modal」 ボタンをクリックすることでモーダルダイアログが表示される

  2. モーダルダイアログを表示

    modal02.png

    「close」ボタンをクリックすることでモーダルダイアログが閉じる

  3. モーダルダイアログを閉じたときのログ

    modal03.png

    モーダルダイアログが閉じたときのログ。「 destroyed 」が出力されていることから、 modal.component.tsngOnDestroy() が実行されていることがわかる。

まとめ

ひとつのボタンにひとつのモーダルダイアログを紐付けただけのなんのひねりもない例だが、 ngComponentOutlet を使用してモーダルダイアログが簡単に実装できた。
やっていることは ngIfngSwitch でも実現できるが、 ngComponentOutlet を使うことの強みはやはり 抽象化 だと思う。

[Angular] ngComponentOutlet を使ってタブを実装する 」でもそうだったが、 ngComponentOutlet セットされたコンポーネントによって表示する内容を動的に切り替えることができるのはコードがスッキリして気持ちが良い。

ソースコード

今回の記事で作成したコードは こちら にアップしてあるのでご参考まで。

26
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
23