Angularでモーダルウィンドウを自作する。

# Angularでモーダルウィンドウを自作する。

## 概要

* 本記事では、Angularでモーダルウィンドウを自作する方法を解説する。
   * Angularでモーダルウィンドウを自作する方法を検索するといくつかあるが、モーダルウィンドウの閉じ方によって、その後の動作を振り分けるられるようなサンプルが見つからなかったため

* 以下の機能を持つモーダルウィンドウを作成する。
   * モーダルウィンドウをどうやって閉じたかを、呼び出し元で受け取り可能。
      * OKボタンを押して閉じたのか、NGボタンを押して閉じたのか、あるいはオーバーレイをクリックして閉じたのかなどで処理を振り分けたいケースに対応できるように。
   * アニメーション適用可能。

* 必要知識
   * ソースにはなるべくコメントを付与しているが、以下の知識があると望ましい。

   * Angularでの動的コンポーネントの作成
      * [公式チュートリアル](https://angular.jp/guide/dynamic-component-loader)

   * Angularでのアニメーションの適用
      * [公式チュートリアル](https://angular.jp/guide/animations)

   * RxJSのサブジェクト
      * [公式チュートリアル](https://rxjs-dev.firebaseapp.com/guide/subject)

   * (モーダルウィンドウを実現する)CSSのやり方
      * [初心者でも分かる！モーダルウィンドウの作り方](https://syncer.jp/jquery-modal-window)

   * クロージャー
      * [MDN web docs](https://developer.mozilla.org/ja/docs/Web/JavaScript/Closures)

* その他参考サイト
   * 英語サイトだが、モーダルウィンドウを自作する方法を解説したサイトを記す。とくに`Angular — Create your own Modal Boxes`は解説が丁寧でわかりやすい。

   * Angularでのモーダルウィンドウの作成方法の解説
      * [Angular — Create your own Modal Boxes](https://itnext.io/angular-create-your-own-modal-boxes-20bb663084a1)

   * Angular7でのモーダルウィンドウの実装例
      * [Angular 7 - Custom Modal Window / Dialog Box](http://jasonwatmore.com/post/2019/04/16/angular-7-custom-modal-window-dialog-box)

* 環境
   * 開発した環境は以下

      ```PowerShell
      ng --version

      Angular CLI: 7.3.8
      Node: 10.15.3
      OS: win32 x64
      Angular: 7.2.12
      ```

## サンプル

* ダウンロードしてすぐに動かせるものを下記に配置した。このコードを見て、コメントを見るのがわかりやすいと思う。

* ダウンロード

   ```PowerShell
   git clone https://github.com/9ryuuuuu/angular_modal_window_sample.git
   ```

* 起動方法

   ```PowerShell
   cd angular_modal_window_sample
   npm install
   ng serve --open
   ```

## 構成

* 構成は以下となる。

* モーダルサービス: modal.service.ts
   * モーダルウィンドウ機能を実現するメインプログラム

* モーダルテンプレートコンポーネント: modal-template.component.ts
   * 表示するモーダルウィンドウのテンプレート

* モーダルテンプレートCSS: modal-template.component.css
   * 表示するモーダルウィンドウのテンプレートのCSS

* モーダルを呼び出すコンポーネント: modal-test.component.ts
   * モーダルウィンドウを表示するコンポーネント

* その他
   * app.module.tsの@NgModuleのentryComponentsに、動的に生成するコンポーネント(ModalTemplateComponent)を記載する必要がある。

## 詳細
* 各ファイルの詳細を以下に記す。 ※ githubにあるのと同じもの。

* モーダルサービス: modal.service.ts

   * モーダルウィンドウを表示したいコンポーネントから、openModal()を呼び出すことで使用する。
   * openModal()は、モーダルウィンドウを生成し、(モーダルウィンドウを閉じたときの操作を取得できるように)Observableを返す。

   ```javascript
   import { Injectable, ViewContainerRef, ComponentFactoryResolver, ComponentRef } from '@angular/core';
   import { Subject } from 'rxjs';
   import { ModalTemplateComponent } from './modal-template/modal-template.component';

   @Injectable({
      providedIn: 'root'
   })
   export class ModalService {

      /**
      * モーダルウィンドウの作成元。
      * モーダルウィンドウを二度目以降にに呼び出したときに、
      * 先に作成したモーダルウィンドウをクリアするために変数で保存しておく。 */
      viewContainerRef: ViewContainerRef;

      /**
      * RxJSのサブジェクト。
      * モーダルウィンドウの呼び出し元にObservablewo返し、
      * モーダルウィンドウが閉じられたときは、それを通知(publish)する。
      */
      subject: Subject<any>;

      constructor(private componentFactoryResolver: ComponentFactoryResolver) {
      }


      /**
      * モーダルウィンドウを開くメソッド。
      * モーダルウィンドウを表示するコンポーネントから呼び出す。
      * @param viewContainerRef 呼び出し元で生成し、渡す必要がある。
      * @param param 生成するモーダルウィンドウに表示するデータ
      */
      public openModal(viewContainerRef: ViewContainerRef, param: any) {

         // モーダルウィンドウを呼び出すたびにサブジェクトを新しく生成する。
         this.subject = new Subject();

         // モーダルウィンドウを二度目以降にに呼び出したときに、
         // 先に作成したモーダルウィンドウを破棄する。
         // しないと、モーダルウィンドウのDIV要素が永遠と増えていく。
         if (this.viewContainerRef) this.viewContainerRef.clear();

         // viewContainerRefをクラスプロパティに保存
         this.viewContainerRef = viewContainerRef;

         // モーダルウィンドウを作成し、呼び出し元画面に追加
         let componentRef = this.createComponent(ModalTemplateComponent, viewContainerRef);

         // 作成したモーダルウィンドウにデータを渡す。
         componentRef.instance.data = param;
         param.click = this.retPublish();

         // 呼び出し元にObservableを返す。
         return this.subject.asObservable();
      }

      /**
      * テンプレートからコンポーネントを作成し、viewContainerRefに追加する。
      * @param componentTemplate
      * @param viewContainerRef
      */
      private createComponent(componentTemplate: any, viewContainerRef: ViewContainerRef): ComponentRef<any> {
         let componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentTemplate);
         return viewContainerRef.createComponent(componentFactory);

      }

      /**
      * Observableに値を通知(publish)するメソッドを返すメソッド。
      * モーダル画面から呼び出す。
      */
      private retPublish() {
         let subject = this.subject;
         return (retVal: string) => {
               try {
                  subject.next(retVal);
                  subject.complete();
               } catch (err) {
                  subject.error(err);
               }
         }
      }
   }


   ```

* モーダルテンプレートコンポーネント: modal-template.component.ts
   * 表示するモーダルウィンドウのテンプレートで、モーダルサービスから呼び出される。
   * 表示するデータと、モーダルウィンドウの終了をObservableに通知するためのメソッドをモーダルサービスから受け取る。

   ```javascript
   import { Component, OnInit, Input } from '@angular/core';
   import { trigger, state, style, transition, animate } from '@angular/animations';

   @Component({
      selector: 'app-modal-template',
      template: `
      <div id="modal-container" [class]="data.class" [@openClose]="isOpen ? 'open' :'closed' ">
         <h2>{{data.title}}</h2>
         <div [innerHtml]="data.contents"></div>
         <p>
         <button (click)="data.click('OK'); closeModal()">OK</button>
         <button (click)="data.click('NG'); closeModal()">NG</button>
         </p>
      </div>

      <div id="overlay" (click)="data.click(); closeModal()" [@openClose]="isOpen ? 'open' :'closed'"></div>
      `,
      styleUrls: ['./modal-template.component.css'],
      // アニメーションの設定
      // opacityの遷移をアニメーションにする。
      // 遷移にかける時間は0.2s
      animations: [
         trigger('openClose', [
               state('open', style({
                  opacity: 1,
               })),
               state('closed', style({
                  display: 'none',
                  opacity: 0,
               })),
               transition('open => closed', [
                  animate('0.2s')
               ]),
               transition('closed => open', [
                  animate('0.2s')
               ]),
               transition('* => void', [
                  animate('0.2s')
               ]),
               transition('void => *', [
                  animate('0.2s')
               ]),
         ])
      ]
   })
   export class ModalTemplateComponent {

      @Input() data: any;
      isOpen: boolean;

      constructor() {
         this.isOpen = true;
      }

      /**
      * モーダルウィンドウを非表示にする。
      * ウィンドウの破棄は次にモーダルウィンドウのを呼び出したときに、
      * モーダルサービスで行うため、ここでは非表示にするだけ。
      */
      closeModal() {
         this.isOpen = false;
      }
   }

   ```

* モーダルテンプレートCSS: modal-template.component.css

   ```css

   /* 自由にカスタム可能 */
   .modal{
      top: 300px;
      left: 50%;
      width: 375px;
      background: #ffc0cb;
      margin-left: -187.5px;
      text-align: center;
      box-shadow: 0px 0px 2px 1px black;
   }

   /*  モーダルウィンドウを実現するためのCSS */
   #modal-container {
      position:  fixed;
      z-index: 100;
      opacity: 0;
   }

   /*  モーダルウィンドウを実現するためのCSS */
   #overlay {
      position: absolute;
      top: 0px;
      left:  0px;
      z-index:  99;
      background:  rgba(0,0,0,0.8);
      width:  100%;
      height: 100%;
      opacity: 0;

   }
   ```

* モーダルを呼び出すコンポーネント: modal-test.component.ts
   * モーダルサービスのopenModal()を呼び出してObservableを受け取り、その中でモーダルウィンドウを終了したときの結果を受け取る。
   * ViewContainerRefに作成したモーダル画面を挿入するため、openModal()に渡す必要がある。

   ```javascript
   import { Component, OnInit, ViewContainerRef } from '@angular/core';
   import { ModalService } from '../modal.service';

   @Component({
      selector: 'app-modal-test',
      template: `
   <h2>modal test</h2>
   <button (click)="openModal()">modal open</button>
   `,
      styleUrls: ['./modal-test.component.css'],

   })
   export class ModalTestComponent implements OnInit {

      constructor(
         private viewContainerRef: ViewContainerRef,
         private modalService: ModalService
      ) { }

      ngOnInit() {
      }

      /**
      * モーダル画面を表示する。
      */
      openModal(): void {
         // モーダルウィンドウに表示する内容
         let param = { title: 'タイトル', contents: '<u>コンテンツ。HTMLタグ使用可能</u>', class: 'modal' };

         // openModal()を呼んで、Observableを受け取る。
         let observable = this.modalService.openModal(this.viewContainerRef, param);

         // モーダルウィンドウの結果に対する処理は、subscribe内に記載する。
         observable.subscribe(
               {
                  next: v => console.log(v),
                  error: (err) => console.log(err),
                  complete: () => console.log("done")
               });

      }
   }

   ```

## 最後に
* この記事が誰かの役に立てば嬉しい。