- Introduction
- Creating an instance with NgComponentOutlet
- What is NgComponentOutlet?
- Cannot pass values to @Input properties.
- @Injectable parameters and the Directives that Provide them
- @Injectable parameters
- Directives to Provide parameters.
- Delivering parameters using a Directive
- Receiving parameters in SomeComponent
- Conclusion
Passing parameters to the Component generated by the NgComponentOutlet
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.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);
}
}
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: [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>
warn: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.