Passing parameters to the Component generated by the NgComponentOutlet

Posted on: February 18, 2022

Introduction

In Angular, there are several ways to create a Component instance. The most common is to write a Component selector on the template, and probably the next one is Using the NgComponentOutlet directive method, and finally using the ComponentFactory The last method is to use ComponentFactory to insert an instance on TS into ViewContainerRef.

Now we think about passing some parameters to the created instance. The main topic of this article is the second case of using NgComponentOutlet, but let's look back at the first and third cases first.

If you use the first method of using selector, you can simply pass parameters using property binding.

Case of selector
<app-some-component [params]="params" ></app-some-component>

Here, SomeComponent is a Component that accepts params with the @Input decorator as follows.

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

And if you use the third one, ComponentFactory, you can pass parameters to SomeComponent as follows. Since this is not the main topic, I will omit the details.

Case of ComponentFactory
// Creating a ComponentFactory for SomeComponent const componentFactory = this.componentFactoryResolver.resolveComponentFactory( SomeComponent ); const componentRef = this.viewContainerRef.createComponent(componentFactory); // get instance of Component const instance = componentRef.instance; // Set a data in the instance instance.params = params;

And now, the main topic, the case of passing parameters to the instance created by NgComponentOutlet.

Creating an instance with NgComponentOutlet

What is NgComponentOutlet?

NgComponentOutlet is a directive. If you have a reference to SomeComponent Class in a variable in the TS file, and write the following in the template, Directive will create an instance of the class by itself and reflect it in the View.

Usage of NgComponentOutlet
<ng-container *ngComponentOutlet="component" ></ng-container>

Therefore, if you want to switch Components dynamically, you should replace the Component Class referred to by component according to some logic.

Cannot pass values to @Input properties.

As you can see in the example above, SomeComponent is passed to Directive as a Class, and there is no way to pass parameters to the instance created there by property binding. It may be possible to force a reference to the instance and set the parameters as in the case of ComponentFactory, but in this article, I would like to use DI to deliver the parameters to this Component instance.

@Injectable parameters and the Directives that Provide them

@Injectable parameters

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); } }

Only the properties of the parameters are defined in Observable for the Service to pass the parameters. This should be arranged according to the requirements.

Directives to Provide parameters.

The above ComponentOutletParams should not be a singleton, since there will generally be more than one Component instance created by NgComponentOutlet. Basically, there will be parameters that correspond one-to-one with the NgComponentOutlet Directive. Therefore, the ComponentOutletParams prepared by @Injectable should be instantiated in this directive and Provide from the Element Injector of the DOM to which the directive is given.

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

Since setter is defined for properties that have property bindings, whenever a new value is set for a directive, the new value is delivered to the Observable parameters of ComponentOutletParams.

Delivering parameters using a Directive

Set Parameters
<div [appComponentOutletParams]="params"> <ng-container *ngComponentOutlet="component"></ng-container> </div>

Location of the Element Injector

Since <ng-container> does not have any entity as DOM, there is no Element Injector. Therefore, <ng-container> is wrapped with <div>, and a directive for the parameter set is given to it. This parameter can be Injected by the descendant elements of this <div>.

Receiving parameters in SomeComponent

The implementation of SomeComponent that receives the delivered parameters changes as follows. Receiving properties is changed from property binding to 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); }); } }

Conclusion

If you use it in a project, it may be a little delicate to mix property binding and DI for parameter passing, but if you are concerned about property binding, you should use a method like the ComponentFactory mentioned in the first paragraph. I have the impression that Angular is not as good at handling dynamic components as React, so some degree of compromise seems unavoidable. For example, in React, this is just a matter of defining a function that dynamically changes the returning functional Component. I think this is the strength of React, which is JS-first compared to Angular, which is template-first.


share on...