Angular2でModal作る

Angular2でモーダルを自作したメモ。

要件:

* Angular以外のライブラリには頼らない
* モーダルを開く時はServiceとして扱いたい
* モーダルの内側はComponentとして開発したい
* モーダルを閉じた場合、Promise(or Observable)をresolveしたい。

要するに、下記のようなコードを書きたいのだ。

```ts:src/greeting-modal.component.ts
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();
  }
}
```

```ts:src/my-app.component.ts
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` サービス。
実装は下記のようになる。

```ts:src/modal.ts
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(=モーダルのマウントポイントに相当する)は下記のようになる。

```ts:src/modal-entry.component.ts
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](https://ng2-info.github.io/2016/07/preparing-for-ngmodule/#entrycomponents)にも書いてあった。

```ts:src/app.module.ts
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](https://plnkr.co/edit/ADBqEwtn0uXmB7Its85p)にも置いてるので、参考まで。