Angular 19.1.0 introduces an enhancement to the ngComponentOutlet directive by adding a new getter, componentInstance, which allows developers to access the instance of the dynamically created component. This feature is crucial for Angular developers as it facilitates direct interaction with the rendered component, enabling them to access the inputs and methods on the component instance after it has been created. With componentInstance, developers can directly interact with components in the templates and component classes.
Define a Greeting Service
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AdminGreetingService {
greeting = signal('');
setGreeting(msg: string) {
this.greeting.set(msg);
}
}
The AdminGreetingService
is a service with a setGreeting
method that will be injected into the rendered components.
Create a User Form
import { ChangeDetectionStrategy, Component, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-user-form',
imports: [FormsModule],
template: `
@let choices = ['Admin', 'User', 'Intruder'];
@for (c of choices; track c) {
@let value = c.toLowerCase();
<div>
<input type="radio" [id]="value" [name]="value" [value]="value"
[(ngModel)]="userType" />
<label for="admin">{{ c }}</label>
</div>
}
Name: <input [(ngModel)]="userName" />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserFormComponent {
userType = model.required<string>();
userName = model.required<string>();
}
The UserFormComponent
has an input field where you can enter a name and radio buttons to select the user type. When users select "Admin", the demo programmatically renders the AdminComponent
component. When users select "User", it programmatically renders the UserComponent
component.
Dynamically rendered Components
// app.component.html
<h2>{{ type() }} Component</h2>
<p>Name: {{ name() }}</p>
<h2>Permissions</h2>
<ul>
@for (p of permissions(); track p) {
<li>{{ p }}</li>
} @empty {
<li>No Permission</li>
}
</ul>
@Component({
selector: 'app-admin',
templateUrl: `app.component.html`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AdminComponent implements Permission {
permissions = input.required<string[]>();
name = input('N/A');
type = input.required<string>();
service = inject(GREETING_TOKEN);
getGreeting(): string {
return `I am an ${this.type()} and my name is ${this.name()}.`;
}
constructor() {
effect(() => this.service.setGreeting(`Hello ${this.name()}, you have all the power.`));
}
}
@Component({
selector: 'app-user',
templateUrl: `app.component.html`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserComponent implements Permission {
type = input.required<string>();
permissions = input.required<string[]>();
name = input('N/A');
service = inject(GREETING_TOKEN);
getGreeting(): string {
return `I am a ${this.type()} and my name is ${this.name()}.`;
}
constructor() {
effect(() => this.service.setGreeting(`Hello ${this.name()}.`));
}
}
The demo has two components, AdminComponent
and UserComponent
, for dynamic rendering. Each component has three signal inputs for the type, permissions, and name. It has a getGreeting
method to return a greeting. When the name input is updated, the effect
runs a callback to set the greeting in the AdminGreetingService
.
Define dynamic component configuration
export const GREETING_TOKEN = new InjectionToken<{ setGreeting: (name: string) => void }>('GREETING_TOKEN');
const injector = Injector.create({
providers: [{
provide: GREETING_TOKEN,
useClass: AdminGreetingService
}]}
);
The GREETING_TOKEN
token is an injection token that provides an object implementing a setGreeting
function. The Injector.create
static method creates an injector that returns an AdminGreetingService
when codes inject the GREETING_TOKEN
injection token.
export const configs = {
"admin": {
type: AdminComponent,
permissions: ['create', 'edit', 'view', 'delete'],
injector
},
"user": {
type: UserComponent,
permissions: ['view'],
injector
},
}
The configs
object maps the key to the dynamic component, permissions input, and injector.
Programmatically render components with ngComponentOutlet
@Component({
selector: 'app-root',
imports: [NgComponentOutlet, UserFormComponent],
template: `
<app-user-form [(userType)]="userType" [(userName)]="userName" />
@let ct = componentType();
<ng-container [ngComponentOutlet]="ct.type"
[ngComponentOutletInputs]="inputs()"
[ngComponentOutletInjector]="ct.injector"
#instance="ngComponentOutlet"
/>
@let componentInstance = instance?.componentInstance;
<p>Greeting from componentInstance: {{ componentInstance?.getGreeting() }}</p>
<p>Greeting from componentInstance's injector: {{ componentInstance?.service.greeting() }}</p>
<button (click)="concatPermissionsString()">Permission String</button>
hello: {{ permissionsString().numPermissions }}, {{ permissionsString().str }}
`,
})
export class App {
userName = signal('N/A');
userType = signal<"user" | "admin" | "intruder">('user');
componentType = computed(() => configs[this.userType()]);
inputs = computed(() => ({
permissions: this.componentType().permissions,
name: this.userName(),
type: `${this.userType().charAt(0).toLocaleUpperCase()}${this.userType().slice(1)}`
}));
outlet = viewChild.required(NgComponentOutlet);
permissionsString = signal({
numPermissions: 0,
str: '',
});
concatPermissionsString() {
const permissions = this.outlet().componentInstance?.permissions() as string[];
this.permissionsString.set({
numPermissions: permissions.length,
str: permissions.join(',')
});
}
}
componentType = computed(() => configs[this.userType()]);
The componentType
is a computed signal that looks up the component, injector, and user permissions when users select a user type.
<ng-container [ngComponentOutlet]="ct.type"
[ngComponentOutletInputs]="inputs()"
[ngComponentOutletInjector]="ct.injector"
#instance="ngComponentOutlet"
/>
The App
component creates a NgContainer
and assigns type, injector, and inputs to ngComponentOutlet
, ngComponentOutletInputs
, and ngComponentOutletInjector
inputs.
Moreover, the ngComponentOutlet
directive exposes the componentInstance
to the instance
template variable.
Use the componentInstance within the template
@let componentInstance = instance?.componentInstance;
<p>Greeting from componentInstance: {{ componentInstance?.getGreeting() }}</p>
<p>Greeting from componentInstance's injector: {{ componentInstance?.service.greeting() }}</p>
In the inline template, I can access the componentInstance
and display the value of the getGreeting
method. Moreover, I access the AdminGreetingService
service and display the value of the greeting
signal.
Use the componentInstance inside the component class
outlet = viewChild.required(NgComponentOutlet);
permissionsString = signal({
numPermissions: 0,
str: '',
});
concatPermissions() {
const permissions = this.outlet().componentInstance?.permissions() as string[];
this.permissionsString.set({
numPermissions: permissions.length,
str: permissions.join(',')
});
}
The viewChild.required
function queries the NgComponentOutlet, and this.outlet().componentInstance
exposes the rendered component. The concatPermissions
method concatenates the permissions
input of the rendered component and assigns the result to the permissionsString
signal.
<button (click)="concatPermissions()">Permission String</button>
hello: {{ permissionsString().numPermissions }}, {{ permissionsString().str }}
The button click invokes the concatPermissions
method to update the permissionString signal, and the template displays the signal value.
In conclusion, the componentInstance exposes the rendered component for Angular developers to call its signals, inputs, methods, and internal services.
References:
- ngComponentOutlet API: https://angular.dev/api/common/NgComponentOutlet
- ngComponentOutlet Doc: https://angular.dev/guide/components/programmatic-rendering#using-ngcomponentoutlet
- Stackblitz Demo: https://stackblitz.com/edit/stackblitz-starters-2vwgzqus?file=src%2Fmain.ts
Top comments (2)
Oh that's good to know! Was just working on a spike where I really needed to get the component instance back. This would be much simpler.
Happy to help, Stephen. Please let me know if you have any issues when using this feature, and I will reach out to the Angular team to get some clarifications.