読者です 読者をやめる 読者になる 読者になる

Carpe Diem

備忘録。https://github.com/jun06t

Angularで中身を動的に変えられるModalを作る【応用編】

Angular

概要

前回の続きです。

前回の要件に加え、今回対応したいのは以下です。

  • 開く時にパラメータを渡して、Modal内のComponentで利用したい

例えばエラーメッセージを渡して上部に通知バーを表示するなどを作る時にこういった仕組みが必要になると思います。

今回は統一した完了ダイアログComponentを使うが、メッセージだけ切り替えたいという要件を実現します。

環境

  • angular 2.4.7
  • angular-cli 1.0.0-beta.32.3

成果物

今回の成果物は以下です。

github.com

ブログの説明でよく分からない時は参考にしてください。

フォルダ構造

.
├── app.component.css
├── app.component.html
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── complete
│   ├── complete.component.css
│   ├── complete.component.html
│   ├── complete.component.spec.ts
│   └── complete.component.ts
└── modal
    ├── modal.component.css
    ├── modal.component.html
    ├── modal.component.spec.ts
    ├── modal.component.ts
    ├── modal.service.spec.ts
    └── modal.service.ts

実装

complete.component.ts

export const COMPLETE_TEXT_TOKEN = new OpaqueToken('complete.text');

@Component({
  selector: 'app-complete',
  templateUrl: './complete.component.html',
  styleUrls: ['./complete.component.css']
})
export class CompleteComponent {
  public test: string;

  constructor(@Inject(COMPLETE_TEXT_TOKEN) t: string) {
    this.text = t;
  }
}

今回呼び出すComponentへ文字列を渡したいので、constructorのところに@Injectの引数を持たせます。
この時serviceやcomponentなどのclass型であれば、DIするときのprovider tokenを宜しくやってくれるのですが、stringやobjectの場合はOpaqueTokenを使って自分でtokenを発行する必要があります。

modal.service.ts

  open(data: any, provider: Provider): void {
    if (!data) {
      return;
    }

    const providers = ReflectiveInjector.resolve([provider]);
    const injector = ReflectiveInjector.fromResolvedProviders(providers, this.vcr.parentInjector);

    const factory = this.resolver.resolveComponentFactory(data);
    const component = this.vcr.createComponent(factory, this.vcr.length, injector);

今回は開く時に引数をもたせます。
注意として、vcrcreateComponentが引数に持つInjectorは、vcrparentInjectorをベースにComponentに渡したいproviderをくっつけてInjectします。
もし直接resolveAndCreate([{ provide: TOKEN, useValue: 'xxx' }])でInjectorを作って入れてみるとNo provider for ViewUtilsとエラーが出ます。

これはどうしてかというと、createComponentは内部でComponentFactorycreateというメソッドを使用しているのですが、これはViewUtilsというproviderを持ったinjectorでなくてはいけません。

  create(
      injector: Injector, projectableNodes: any[][] = null,
      rootSelectorOrNode: string|any = null): ComponentRef<C> {
    const vu: ViewUtils = injector.get(ViewUtils);
    if (!projectableNodes) {
      projectableNodes = [];
    }
    const hostView: AppView<any> = new this._viewClass(vu, null, null, null);
    return hostView.createHostView(rootSelectorOrNode, injector, projectableNodes);
  }

ref: angular/component_factory.ts at ab26b6518d16e54d198031c9994399cc29e270f1 · angular/angular · GitHub

ちなみに前回のようにつけない場合は勝手にvcr.parentInjectorを使用するコードになっているので問題ないです。

  createComponent<C>(
      componentFactory: ComponentFactory<C>, index: number = -1, injector: Injector = null,
      projectableNodes: any[][] = null): ComponentRef<C> {
    const s = this._createComponentInContainerScope();
    const contextInjector = injector || this._element.parentInjector;
    const componentRef = componentFactory.create(contextInjector, projectableNodes);
    this.insert(componentRef.hostView, index);
    return wtfLeave(s, componentRef);
  }

ref: angular/view_container_ref.ts at c33fda260726f5c050a75fc01fbd7a74800f8e37 · angular/angular · GitHub

また、今回の理由とは別ですが、パフォーマンス的にも

var injector = ReflectiveInjector.resolveAndCreate([Car, Engine]);

より

var providers = ReflectiveInjector.resolve([Car, Engine]);
var injector = ReflectiveInjector.fromResolvedProviders(providers);

という書き方を公式のAPIドキュメントは推奨をしています。

app.component.ts

  register(): void {
    const provider = { provide: COMPLETE_TEXT_TOKEN, useValue: '登録が完了しました' };
    this.modal.open(CompleteComponent, provider);
  }

  send(): void {
    const provider = { provide: COMPLETE_TEXT_TOKEN, useValue: '送信が完了しました' };
    this.modal.open(CompleteComponent, provider);
  }

呼び出す際にproviderも一緒に渡すようにしています。

動作確認

f:id:quoll00:20170219164227p:plain

Registerボタンをクリックします。

f:id:quoll00:20170219164238p:plain

「登録が完了しました」と出てます。もう片方は

f:id:quoll00:20170219164306p:plain

送信が完了しました」と出てます。メッセージを切り替えられる事がわかります。

ソース