Experiments Never Fail

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">Angular 6 で、日付や時刻との双方向データバインディング</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></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="activeModal.close('ok')"></button>
<button type="button" class="btn btn-outline-dark" (click)="activeModal.dismiss('cancel')"></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 です。

published at tags: angular TypeScript Bootstrap