Angular 6 で、日付や時刻との双方向データバインディング

Angular + ng-bootstrap でモーダルポップアップを表示した場合、Angular powered Bootstrap - Modal に、基本的な使い方は書いてあるのですが、もっと簡単に、 modal.confirm('title', 'message') みたく使えるようにしてみました。

Components as content には、Modal に表示する内容(template)を別のコンポーネントクラスで提供する方法が書かれています。

これを Angular のサービスと組み合わせると、「モーダル表示を行うサービス」を作ることができます。 サービスは利用クラス側で、コンストラクタインジェクションが可能なので、簡単に使用できます。

部品側

以降説明するクラス群は、すべて一つの my-modal.service.ts に記述できます。 このファイル自体も ng g service my-modal で作ったものです。同時に my-modal.service.spec.ts も作成されます。こちらは作成後まったく編集しませんので、以降は触れません。

MyModalService クラス

利用者が使うサービスクラスです。ここでは「確認モーダル」を表示する confirm() メソッドを定義しています。 モーダルに表示する内容は、後述する MyModalConfirmContent により提供されます。

import { Injectable, Component, Input } from '@angular/core';
import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';

@Injectable({
  providedIn: 'root'
})
export class MyModalService {
  constructor(private modalService: NgbModal) { }

  confirm(title: string, message: string, okCaption?: string, cancelCaption?: string): Promise<boolean> {
    const modalRef = this.modalService.open(MyModalConfirmContent);
    const component = modalRef.componentInstance as MyModalConfirmContent;
    if (component != null) {
      component.title = title;
      component.message = message;
      component.okCaption = okCaption || 'OK';
      component.cancelCaption = cancelCaption || 'Cancel';
    }

    const source = new PromiseCompletionSource<boolean>();

    modalRef.result.then(result => {
      source.resolve(true);
    }, reason => {
      source.resolve(false);
    });

    return source.promise;
  }
}

PromiseCompletionSource クラス

Kotlin でいう Continuation<T>、 C# でいう TaskCompletionSource<T> を模したクラスです。 非同期処理の終了とエラーを通知するために使います(JavaScript/TypeScript に慣れないので思わず移植しちゃったけど、他に実現方法がありそう、おしえてplz)。

class PromiseCompletionSource<T> {
  public readonly promise: Promise<T>;

  private resolver: (x?: T) => void;
  private rejector: (reason?: any) => void;

  constructor() {
      this.promise = new Promise<T>((resolve, reject) => {
          this.resolver = resolve;
          this.rejector = reject;
      });
  }

  public resolve(x: T) {
      if (this.resolver) {
          this.resolver(x);
      }
  }

  public reject(reason?: any) {
      if (this.rejector) {
          this.rejector(reason);
      }
  }
}

MyModalConfirmContent クラス

確認モーダルの内容を示すクラスです。 実体はほぼなく、重要なのは template: に定義された HTML とそれへのデータバインディング用プロパティです。

@Component({
  template:
    `
<div class="modal-header">
  <h4 class="modal-title">{{title}}</h4>
  <button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('dissmiss')">
    <span aria-hidden="true">&times;</span>
  </button>
</div>
<div class="modal-body">
  <p>{{message}}</p>
</div>
<div class="modal-footer">
  <button type="button" class="btn btn-primary" (click)="activeModal.close('ok')">{{okCaption}}</button>
  <button type="button" class="btn btn-outline-dark" (click)="activeModal.dismiss('cancel')">{{cancelCaption}}</button>
</div>
  `
})
// tslint:disable-next-line   <-- tslint さんが「コンポーネントならクラス名に Component を付けろ」と怒り心頭なので黙らせる
export class MyModalConfirmContent {

  @Input() title: string;
  @Input() message: string;
  @Input() okCaption = 'OK';
  @Input() cancelCaption = 'Cancel';

  constructor(public activeModal: NgbActiveModal) { }
}

利用者側

上で作った MyModalService の使い方です。

1. app.module に登録する

まず MyModalConfirmContent を module に登録する必要があります(app.module じゃなくてもいいです)。 下記のように MyModalConfirmContentdeclarations: entryComponents: に追加します(どちらも必要です)。

xxx.moduleクラス

@NgModule({
  imports: [
    CommonModule,],
  declarations: [
    …
    MyModalConfirmContent], ←追加
  exports: [LayoutComponent],
  entryComponents: [MyModalConfirmContent] ←追加
})
export class AppModule { }

2. モーダルを表示する

あとは任意のコンポーネントで使用します。 コンストラクタに MyModalService を定義して注入させ、任意の場所で this.modal.confirm() を呼び出します。返値は Promise<boolean> なので async/await でも使えますね。

@Component({
  selector: 'app-my-top',
  templateUrl: './my-top.component.html',
  styleUrls: ['./my-top.component.scss']
})
export class MyTopComponent {

  constructor(
    private modal: MyModalService) { }

  async showConfirm() {
    const res = await this.modal.confirm('たいとる', 'もーだるですか?', 'はい', 'いいえ');
    console.log(`result = {res}`);
    if (!res) {
      return;
    }
  }
}

正しく実行できれば、次のように表示されるはずです。

IfsApp.png

すべてのソースコードは

にもあります。

実際に動いた package.json の一部はこんな感じです。

"dependencies": {
  "@angular/animations": "^6.0.3",
  "@angular/common": "^6.0.3",
  "@angular/compiler": "^6.0.3",
  "@angular/core": "^6.0.3",
  "@angular/forms": "^6.0.3",
  "@angular/http": "^6.0.3",
  "@angular/platform-browser": "^6.0.3",
  "@angular/platform-browser-dynamic": "^6.0.3",
  "@angular/router": "^6.0.3",
  "@ng-bootstrap/ng-bootstrap": "^2.2.0",
  "bootstrap": "^4.1.1",
  "core-js": "^2.5.4",
  "font-awesome": "^4.7.0",
  "moment": "^2.22.2",
  "ng-spin-kit": "^5.1.1",
  "ngx-loading": "^1.0.14",
  "rxjs": "^6.0.0",
  "zone.js": "^0.8.26"
},

モーダルの種類を増やしたい場合

  1. MyModalXxxxContent を追加
  2. MyModalXxxxContentapp.module.ts に追加
  3. MyModalServiceMyModalXxxxContent に対応した新しいメソッドを追加

で ok です。