LoginSignup
62
60

More than 5 years have passed since last update.

Angular2でModal作る

Last updated at Posted at 2016-08-13

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

要件:

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

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

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();
  }
}
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 サービス。
実装は下記のようになる。

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を利用。
こいつを叩くために、ComponentFactoryInjectorが欲しくなるので、それぞれ ComponentFactoryResolverReflectiveInjector をゴニョゴニョしている。
Componentに渡すInjectorを自由にいじれるので、ここを凝れば「サービスの引数 -> Injector -> モーダルのComponentにDI」という流れを踏むことで、いくらでもモーダルに値を渡すことが出来る。

ViewContainerRef はModalサービスの外部からセットする形式にしている。
これを設定するためのComponent(=モーダルのマウントポイントに相当する)は下記のようになる。

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にも書いてあった。

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にも置いてるので、参考まで。

62
60
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
62
60