DEV Community

Connie Leung
Connie Leung

Posted on

New Angular 19 feature - ngComponentOutlet componentInstance

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);
 }
}
Enter fullscreen mode Exit fullscreen mode

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>();
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
@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.`));
 }
}
Enter fullscreen mode Exit fullscreen mode
@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()}.`));
 }
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode
const injector = Injector.create({
 providers: [{
   provide: GREETING_TOKEN,
   useClass: AdminGreetingService
 }]}
);
Enter fullscreen mode Exit fullscreen mode

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
 },
}
Enter fullscreen mode Exit fullscreen mode

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(',')
   });
 }
}
Enter fullscreen mode Exit fullscreen mode
componentType = computed(() => configs[this.userType()]);
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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(',')
   });
}
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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:

Top comments (2)

Collapse
 
scooperdev profile image
Stephen Cooper

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.

Collapse
 
railsstudent profile image
Connie Leung

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.