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
はDirectiveである。動的なComponent生成を最も簡単にやろうとした場合に使うことになる。TSファイル側で変数にSomeComponent Classへの参照を持っておき、テンプレート上で以下のように記述すると、Directiveが勝手にインスタンスを生成してViewに反映してくれる。
NgComponentOutletの使い方<ng-container *ngComponentOutlet="component" ></ng-container>
よって動的にComponentを切り替えたい場合には、component
で参照するComponent Classを何かしらのロジックに従って差し替えてやれば良い。
上の例を見てわかる通り、SomeComponentはClassのままDirectiveに渡されており、そこで生成されたインスタンスにプロパティバインディングでパラメータを渡す手段がない。無理やりインスタンスへ参照を持って、ComponentFactory
の場合と同様にパラメータをセットすることも可能かもしれないが、この記事ではDIを使ってこのComponentインスタンスにパラメータを渡す方法を考える。
component-outlet-params.service.tsimport { 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を定義している。
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なパラメータには新しい値が配信される。
パラメータの配信<div [appComponentOutletParams]="params"> <ng-container *ngComponentOutlet="component"></ng-container> </div>
Element Injectorの場所について
<ng-container>
はDOMとして実体を持たないので、Element Injectorが存在しない。よって、<ng-container>
を<div>
でラップして、そこへパラメータセット用のDirectiveを付与している。このパラメータはこの<div>
の子孫要素でInjectすることが可能となる。
配信されたパラメータを受け取る側の 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の強みだと思う。