Angular2でモーダルを自作したメモ。
要件:
- Angular以外のライブラリには頼らない
- モーダルを開く時はServiceとして扱いたい
- モーダルの内側はComponentとして開発したい
- モーダルを閉じた場合、Promise(or Observable)をresolveしたい。
要するに、下記のようなコードを書きたいのだ。
import { Component , Input } from "@angular/core";
import { ModalContext } from "./modal";
@Component({
selector: "greeting",
template: `
<div>
<section>
<input [(ngModel)]="message" placeholder="Message">
</section>
<footer>
<button (click)="onClickOk()">OK</button>
<button (click)="onClickCancel()">Cancel</button>
</footer>
</div>
`,
})
export class GreetingModalComponent {
private message: string;
constructor(
private modalCtx: ModalContext
) {
}
onClickOk() {
this.modalCtx.resolve(this.message);
}
onClickCancel() {
this.modalCtx.reject();
}
}
import { Component } from "@angular/core";
import { Modal } from "./modal";
import { GreetingModalComponent } from "./greeting-modal.component";
@Component({
selector: "my-app",
template: `
<modal-entry></modal-entry>
<div class="app">
<h1>Ng2 Modal Example</h1>
<button (click)="openModal()">Open Modal</button>
<span>Result: {{result}}</span>
</div>
`,
})
export class MyApp {
private result = "";
constructor(
private modal: Modal
) {
}
openModal() {
this.modal.open<string>(GreetingModalComponent).then(message => {
this.result = message;
});
}
}
アプリケーション(MyApp
)の側から、モーダルの本体となるComponent(ここではGreetingModalComponent
)のClassを引数にしてモーダルを開き、その結果をPromiseとして扱っている。
本エントリの主眼は、上記コードにおける Modal
サービス。
実装は下記のようになる。
import {
Injectable,
ViewContainerRef,
ComponentFactoryResolver,
ReflectiveInjector,
ComponentRef,
} from "@angular/core";
@Injectable()
export class ModalContext {
constructor(
private _resolve: Function,
private _reject: Function
) {
}
resolve(val: any) {
this._resolve(val);
}
reject(reason?: any) {
this._reject(reason);
}
}
@Injectable()
export class Modal {
public vcr: ViewContainerRef;
private count = 0;
constructor(
private cfr: ComponentFactoryResolver
) {
}
isShow() {
return this.count > 0;
}
open<T>(comp: any) {
let cr: ComponentRef<any>;
return new Promise<T>((resolve, reject) => {
const cf = this.cfr.resolveComponentFactory(comp);
const _resolve = (val: T) => {
if (cr) {
cr.destroy();
resolve(val);
this.count--;
}
};
const _reject = (reason?: any) => {
if (cr) {
cr.destroy();
reject(reason);
this.count--;
}
};
const bindings = ReflectiveInjector.resolve([
{provide: ModalContext, useValue: new ModalContext(_resolve, _reject)}
]);
const ctxInjector = this.vcr.parentInjector;
const injector = ReflectiveInjector.fromResolvedProviders(bindings, ctxInjector);
cr = this.vcr.createComponent(cf, this.vcr.length, injector);
this.vcr.element.nativeElement.appendChild(cr.location.nativeElement);
this.count++;
});
}
}
Componentの生成には、ViewContainerRef#createComponent
を利用。
こいつを叩くために、ComponentFactory
と Injector
が欲しくなるので、それぞれ ComponentFactoryResolver
や ReflectiveInjector
をゴニョゴニョしている。
Componentに渡すInjectorを自由にいじれるので、ここを凝れば「サービスの引数 -> Injector -> モーダルのComponentにDI」という流れを踏むことで、いくらでもモーダルに値を渡すことが出来る。
ViewContainerRef
はModalサービスの外部からセットする形式にしている。
これを設定するためのComponent(=モーダルのマウントポイントに相当する)は下記のようになる。
import {
Injectable,
ViewContainerRef,
Component,
ViewChild,
Directive,
} from "@angular/core";
import { Modal } from "./modal";
@Directive({
selector: "[modal-inner]",
})
export class ModalInner {
constructor(public vcr: ViewContainerRef) { }
}
@Component({
selector: "modal-entry",
styles: [`
.bg {
display: none;
}
.bg.active {
display: block;
background-color: rgba(0, 0, 0, 0.2);
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
padding: 100px 0 0 0;
z-index: 10;
}
[modal-inner] {
margin: 0 auto;
width: 400px;
}
`],
template: `
<div class="bg" [class.active]="modal.isShow()">
<div modal-inner></div>
</div>
`,
directives: [ ModalInner ],
})
export class ModalEntryComponent {
@ViewChild(ModalInner) private inner: ModalInner;
constructor(
private modal: Modal
) {
}
ngAfterViewInit() {
this.modal.vcr = this.inner.vcr;
}
}
最後に一点、補足。
モーダルとなるComponent(上記の例ではGreetingModalComponent)は、ModuleにてentryComponents
に指定しないと上手く動作しない。
ここに書いておかないと、modal.tsのComponentFactoryResolverがComponentFactoryを解決してくれなくなってしまう。
何故かはよくわからんので、情報知ってればコメント等ください。
これは、bootstrapを起点としたComponent(今回の例ではMyApp)に対するテンプレートから探索されないようなComponentはentryComponentsに記載する必要があるため(@laco0416 に感謝)。
ちなみに、NgModule導入について Angular2 Infoにも書いてあった。
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { BrowserModule } from "@angular/platform-browser";
import { MyApp } from "./my-app.component";
import { Modal } from "./modal";
import { ModalEntryComponent } from "./modal-entry.component";
import { GreetingModalComponent } from "./greeting-modal.component";
@NgModule({
imports: [ BrowserModule, FormsModule ],
declarations: [ MyApp, ModalEntryComponent, GreetingModalComponent ],
providers: [Modal],
entryComponents: [ GreetingModalComponent ],
bootstrap: [ MyApp ],
})
export class AppModule { }
今回説明したモーダルのdemoはhttps://plnkr.co/edit/ADBqEwtn0uXmB7Its85pにも置いてるので、参考まで。