NgComponentOutletで生成したComponentにパラメータを渡す

投稿日: 2/18/2022

はじめに

AngularではComponentインスタンスを作る方法がいくつかある。最もよく使うのはテンプレート上でComponentのselectorを書く方法、そして次に来るのが多分 NgComponentOutlet Directiveを使う方法、最後に ComponentFactory を使って ViewContainerRef にTS上でインスタンスを挿入する方法ではないかと思う。

ここで生成したインスタンスに対して何かパラメータを渡すことを考える。この記事の本題は2番目に挙げた NgComponentOutlet を使う場合なのだが、一旦1番目と3番目の場合はどうだったかを最初に振り返っておく。

1番目のselectorを使う方法であれば単純にプロパティバインディングでパラメータ渡すことができる。

selectorの場合
<app-some-component [params]="params" ></app-some-component>

ここで SomeComponent は以下のように @Input Decoratorで params を受け取るようなComponentである。

some-component.ts
@Component({ selector: 'app-some-component', templateUrl: './some.component.html', styleUrls: ['./some.component.scss'], }) export class SomeComponent { @Input() params?: Record<string, unknown>; }

そして、3番目の ComponentFactory を使う場合だと、SomeComponentには以下のようにパラメータを渡すことができる。本題ではないので詳細は省略する。

ComponentFactoryの場合
// SomeComponentのComponentFactoryを作る const componentFactory = this.componentFactoryResolver.resolveComponentFactory( SomeComponent ); const componentRef = this.viewContainerRef.createComponent(componentFactory); // componentのインスタンスを取得 const instance = componentRef.instance; // インスタンスにデータをセット instance.params = params;

そしていよいよ本題となる NgComponentOutlet で生成したインスタンスにパラメータを渡す場合である。

NgComponentOutletでインスタンスを生成する

NgComponentOutletとは

NgComponentOutlet はDirectiveである。動的なComponent生成を最も簡単にやろうとした場合に使うことになる。TSファイル側で変数にSomeComponent Classへの参照を持っておき、テンプレート上で以下のように記述すると、Directiveが勝手にインスタンスを生成してViewに反映してくれる。

NgComponentOutletの使い方
<ng-container *ngComponentOutlet="component" ></ng-container>

よって動的にComponentを切り替えたい場合には、component で参照するComponent Classを何かしらのロジックに従って差し替えてやれば良い。

@Inputのプロパティに値を渡せない

上の例を見てわかる通り、SomeComponentはClassのままDirectiveに渡されており、そこで生成されたインスタンスにプロパティバインディングでパラメータを渡す手段がない。無理やりインスタンスへ参照を持って、ComponentFactory の場合と同様にパラメータをセットすることも可能かもしれないが、この記事ではDIを使ってこのComponentインスタンスにパラメータを渡す方法を考える。

@InjectableなパラメータとそれをProvideするDirective

@Injectableなパラメータ

component-outlet-params.service.ts
import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class ComponentOutletParams<T> { public readonly params$ = new BehaviorSubject<T | undefined>(undefined); public get params(): T | undefined { return this.params$.getValue(); } public set params(params: T | undefined) { this.params$.next(params); } }

パラメータを渡すためのService (Angularが公式で用意している ActivatedRoute などと同じようなイメージで捉えて貰えれば良い) を用意する。このServiceのメンバーにはObservableなパラメータと、パラメータを取得・更新するためのgetter/setterを定義している。

パラメータをProvideするためのDirective

NgComponentOutlet で生成されるComponentインスタンスは一般的には1つではないだろうから、上記の ComponentOutletParams はシングルトンであってはいけない。基本的にNgComponentOutlet Directiveと一対一で対応するパラメータが存在することになる。よって、@Injectableで用意した ComponentOutletParams はこのDirectiveにおいてインスタンスを生成し、Directiveを付与したDOMのElement InjectorからProvideすることにする。

component-outlet-params.directive.ts
@Directive({ selector: '[appComponentOutletParams]', providers: [ComponentOutletParams], }) export class ComponentOutletParamDirective<T> { constructor(private componentOutletParams: ComponentOutletParams<T>) {} @Input() set appComponentOutletParams(params: T | undefined) { this.componentOutletParams.params = params; } }

プロパティバインディングするプロパティに対してsetterを定義しているため、Directiveに新しい値がセットされるたびに、ComponentOutletParams がもつObservableなパラメータには新しい値が配信される。

Directiveを使ったパラメータの配信

パラメータの配信
<div [appComponentOutletParams]="params"> <ng-container *ngComponentOutlet="component"></ng-container> </div>

warn:Element Injectorの場所について

<ng-container> はDOMとして実体を持たないので、Element Injectorが存在しない。よって、<ng-container><div> でラップして、そこへパラメータセット用のDirectiveを付与している。このパラメータはこの <div> の子孫要素でInjectすることが可能となる。

SomeComponentにおけるパラメータの受け取り

配信されたパラメータを受け取る側の SomeComponent の実装は以下のように変わる。プロパティの受け取りをプロパティバインディングからDIに変更している。

some-component.ts
@Component({ selector: 'app-some', templateUrl: './some.component.html', styleUrls: ['./some.component.scss'], }) export class SomeComponent implements OnInit { constructor( private componentOutletParams: ComponentOutletParams< Record<string, unknown> > ) {} ngOnInit(): void { this.componentOutletParams.params$.subscribe((params) => { console.log(params); }); } }

さいごに

プロジェクトで使う場合、パラメータの受け渡し方法がプロパティバインディングとDIで混在するのは若干微妙な感じもあるが、プロパティバインディングに拘るとなると冒頭の3番目に挙げた ComponentFactory のような方法になる。なので、それでごちゃごちゃと手続き的にやるのと比べればこの方法は比較的すっきりしていて悪くはないのではないかと思う。AngularはReactに比べて動的なコンポーネントを扱うのが苦手な印象があり、ある程度の妥協は仕方ないように思える。例えばReactであればこんなことは返す関数Componentを動的に変える関数を定義してやるだけである。テンプレートファーストなAngularに対して、JSファーストなReactの強みだと思う。