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.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.