In this article, you will learn the essentials of working with dynamic component injection as a part of the logic of the component.

Before you start, a replica of this work is available in this link in stackblitz.

Injecting an arbitrary component inside of a component

One of the most fascinating features Angular offers is related to component injection. Component injection is a technique that allows a component of your choice to be injected inside another component. This is particularly valuable as modern websites are often composed in such a way that allows for this flexibility.

How I do that?

Recommended Approach for Angular 17+ projects

  • If you are using a Standalone Component, your components have to be declared in your imports section:
// If I were to use this component to be dynamically imported along other compoents
@Component({
  standalone: true,
  selector: 'profile-photo',
})
export class ProfilePhoto { }

@Component({
  standalone: true,
  imports: [ProfilePhoto], //then this is the place where it should be declared
  templateUrl: 'userProfile.component.html'
})
export class UserProfile { }

Important note here: If you are using standalone components, all the other components in the operation, needs to be standalone as well. Importing a component that is not standalone will require you to import the respective module that includes the declaration of such component.

If a non-standalone component is directly imported into a standalone component, it will show this message.

Implementation

Now that we know the basics, let's create a small project where we will dynamically inject two components inside a component template:

  • The UI should show two buttons, each one calling the respective component into the DOM.
  • The respective component should follow the DOM flow, avoiding overlap or deleting previously injected components.
  • Each component, when clicked, should remove itself.

Composition

Injecting a component is probably the easiest job inside the context of a component; however, the component will be placed arbitrarily at the end of its body. If you want to place things in the right place, it is advised to use a ng-container to ensure the right area where the components will be injected into the view.

First, let's create our host component where we will do most of the work. There are diverse ways to project content into a component, but I find using ViewContainerRefto be the simplest way to achieve this approach.

Let's create an Angular component that will be the host of our application, with two buttons:

import {Component, ViewContainerRef, inject} from "@angular/core";

@Component({
  standalone: true,
  selector:'app-root',
  template: `
  <button type="button" (click)="injectC1()" >Inject Component 1</button>
  <button type="button" (click)="injectC2()" >Inject Component 2</button>  
  `
})
export class AppComponent { 
  private viewContainerRef = inject(ViewContainerRef);

  public injectC1(){
    //inject component1
  }

  public injectC2() {
    // inject component2
  }
}

So far, so good. We defined a private viewContainerRef that will ensure any component can be injected into the component that invokes it. Now let's create two child components that will be injected into the component, and make the buttons execute the injection

import {Component, ViewContainerRef, inject} from "@angular/core";

@Component({
  standalone: true,
  selector:'app-component1',
  styleUrl :'c1.css',
  template: `
  <div class="c1">This is Component 1</div>
  `
})
export class Component1 {}

@Component({
  standalone: true,
  selector:'app-component2',
  styleUrl :'c2.css',
  template: `
  <div class="c2">This is Component 2</div>
  `
})
export class Component2 {}

In addition, we add the injection of components on the desired functions:

@Component({
...
})
export class AppComponent { 
  private viewContainerRef = inject(ViewContainerRef);

  public injectC1(){
+    this.viewContainerRef.createComponent(Component1)
  }

  public injectC2() {
+    this.viewContainerRef.createComponent(Component2)
  }
}

finally, let's create the stylesheets for each component:

main.css
.grid {
  margin-top:5px;
  display:grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap:5px;
}
.container {
  border: 1px solid green;
}
c1.css
.c1{
  background-color:#FF0000;
  color:#FFF;
}
c2.css
.c2{
  background-color:#0000FF;
  color:#FFF;
}

The result will look like this. Note that, from the perspective of the component, injected components without a specific container will be placed at the end of the invoking component, which is AppComponent.

Injected components will be placed at the end of the AppComponent template

However, as a demo, it works nicely, but what if we have to inject components in a specific area of the page? Let's create in the AppComponent two columns in a grid where one is static and one is supposed to be the target of our injected components:

@Component({
 ...
  template: `
  <button type="button" (click)="injectC1()" >Inject Component 1</button>
  <button type="button" (click)="injectC2()" >Inject Component 2</button> 
+ <div class="grid">
+   <div class="container">this is static</div>
+    <div class="container">this is dynamic</div>
+ </div>
  `
})
export class AppComponent { 
...
}

Using ng-container

To determine the target, we might want to use an ng-container with a name on it in our AppComponent, so we can use it to programatically inject content in such space. We will use @ViewChild to target the right space named by the ng-container.

@Component({
...
,
  template: `
  <button type="button" (click)="injectC1()" >Inject Component 1</button>
  <button type="button" (click)="injectC2()" >Inject Component 2</button>
  <div class="grid">
    <div class="container">this is static</div>
    <div class="container">
+    <ng-container #targetSpace />
    </div>
  </div>
  `
})
export class AppComponent { 
- private viewContainerRef = inject(ViewContainerRef);
+  @ViewChild('targetSpace', {
+    read: ViewContainerRef
+  })
+  private targetSpace!:ViewContainerRef;

  public injectC1(){
+    this.targetSpace.createComponent(Component1)
-    this.viewContainerRef.createComponent(Component1)
  }

  public injectC2() {
+    this.targetSpace.createComponent(Component2)
-    this.viewContainerRef.createComponent(Component2)
  }
}

The resultant code now injects into the ng-container #targetSpace the components programatically:

Injected components will now be targeted on the ng-container #targetSpace

Clicked components remove themselves from the DOM

To achieve this, we need to trigger an event, and for that, we will use the function output. You probably saw @Output, which is a decorator and has the same result as output. When the component is clicked, we will emit the event through the output, so the parent component can resolve accordingly.

For Component1 and Component2 we will create the respective output events and the event handler that will trigger the behavior to emit the click event. As clicked is public, we can execute the emit event on the template using clicked.emit()


+ import {Component, ViewChild, ViewContainerRef, output} from "@angular/core";

@Component({
...
template: `
-  <div class="c1">This is Component 1</div>
+  <div class="c1" (click)="clicked.emit()">This is Component 1</div>
  `
})
export class Component1 {
+  public clicked = output<void>()
}

@Component({
...
template: `
- <div class="c2">This is Component 2</div>
+ <div class="c2" (click)="clicked.emit()">This is Component 2</div>
  `
})
export class Component2 {
+  public clicked = output<void>()
}

To keep the logic of removing the element on the parent element, we need to call the reference of the created component so when clicked, it can be removed. Output events are subscribable, so in AppComponent you want to use the subscribe method for the instance of the component event (in this case clicked for each injected component).

Each time a component is injected, a reference in memory keeps a pointer to that component. This ensures that each component occupies a specific space in memory, eliminating the risk of closing the wrong component. Understanding the concept of a reference and instance in JavaScript/TypeScript is crucial, especially in complex cases, as it helps to understand how to manage and access these components effectively..

@Component({
 ...
})
export class AppComponent { 
  ...

 public injectC1(){
-   this.targetSpace.createComponent(Component1)  
+    const componentRef = this.targetSpace.createComponent(Component1)
+    componentRef.instance.clicked
+    .subscribe( () => {
+      componentRef.destroy();
+    })
  }

  public injectC2() {
-   this.targetSpace.createComponent(Component2)
+    const componentRef = this.targetSpace.createComponent(Component2)
+    componentRef.instance.clicked
+    .subscribe( () => {
+      componentRef.destroy();
+    })
  }
}

Voila! now each component, when clicked, will remove itself:

Injected components when clicked will be removed by itself

Handling component destruction events

As we dinamycally inject components, and destroy them on the fly, we should ask ourselves what should happen when a component is destroyed. We can use some of the new features of Angular 17 to make this task a less daunting one.

TakeUntilDestroyed, OutputToObservable and DestroyRef implementation

With TakeUntilDestroy along with OutputToObservable will help with the task of converting our output event into an observable one. However, we need to first ensure that a Destroy Reference is available on each component. For this we will use DestroyRef on the ngOnInit to ensure that some action needs to be triggered on each child component as the component is unmounted from the UI.

Note that on Angular 16+, ngOnDestroy became optional as we are using now DestroyRef to indicate the component to do something once the component is destroyed.


@Component({
 ...
})
- export class Component1 {
+ export class Component1 implements OnInit {
+  public destroyRef = inject(DestroyRef);
  public clicked = output<void>();

+  ngOnInit() {
+    this.destroyRef.onDestroy(() => {
+      console.log('Component 1 was destroyed');
+    });
+  }
}

@Component({
 ...
})
- export class Component2 {
+ export class Component2 implements OnInit {
+  public destroyRef = inject(DestroyRef);
  public clicked = output<void>();

+  ngOnInit() {
+    this.destroyRef.onDestroy(() => {
+      console.log('Component 2 was destroyed');
+    });
+  }
}

DestroyRef will be executed when the component is destroyed, and what we will see, is that, once destroyed, we should see the respective message on the terminal. Now let's focus on the AppComponent to add the respective logic for our clicked event to become an observable

import {
  ViewContainerRef,
  Component,
  ViewChild,
  provideExperimentalZonelessChangeDetection,
  output,
  inject,
  DestroyRef,
  OnInit,
} from '@angular/core';

import { bootstrapApplication } from '@angular/platform-browser';

+ import {
+  outputToObservable,
+  takeUntilDestroyed,
+ } from '@angular/core/rxjs-interop';


...

@Component({
 ...
})
export class AppComponent {
  @ViewChild('targetSpace', {
    read: ViewContainerRef,
  })
  private targetSpace!: ViewContainerRef;

  public injectC1() {
    const componentRef = this.targetSpace.createComponent(Component1);
-    componentRef.instance.clicked.subscribe(() => {
+    outputToObservable(componentRef.instance.clicked)
+      .pipe(takeUntilDestroyed(componentRef.instance.destroyRef))
+      .subscribe(() => {
+        componentRef.destroy();
+      });
  }

  public injectC2() {
    const componentRef = this.targetSpace.createComponent(Component2);
-    componentRef.instance.clicked.subscribe(() => {
+    outputToObservable(componentRef.instance.clicked)
+      .pipe(takeUntilDestroyed(componentRef.instance.destroyRef))
+      .subscribe(() => {
+        componentRef.destroy();
+      });
  }
}

Once we have the OutputToObservable on place, we can pipe the response of the observable to use TakeUntilDestroy, which takes as a parameter the public destroyRef reference of the instantiated component.

With this in mind, now we will see the respective console.log of each referenced component when destroyed:

Using OutputAsObservable, TakeUntilDestroy and DestroyRef, we can trigger events when the referenced component is destroyed

Final Code

This is the final resultant code.

main.ts
import {
  ViewContainerRef,
  Component,
  ViewChild,
  provideExperimentalZonelessChangeDetection,
  output,
  inject,
  DestroyRef,
  OnInit,
} from '@angular/core';
import {
  outputToObservable,
  takeUntilDestroyed,
} from '@angular/core/rxjs-interop';
import { bootstrapApplication } from '@angular/platform-browser';

@Component({
  standalone: true,
  selector: 'app-component1',
  styleUrl: 'c1.css',
  template: `
  <div class="c1" (click)="clicked.emit()">This is Component 1</div>
  `,
})
export class Component1 implements OnInit {
  public destroyRef = inject(DestroyRef);
  public clicked = output<void>();

  ngOnInit() {
    this.destroyRef.onDestroy(() => {
      console.log('Component 1 was destroyed');
    });
  }
}

@Component({
  standalone: true,
  selector: 'app-component2',
  styleUrl: 'c2.css',
  template: `
  <div class="c2" (click)="clicked.emit()">This is Component 2</div>
  `,
})
export class Component2 {
  public destroyRef = inject(DestroyRef);
  public clicked = output<void>();

  ngOnInit() {
    this.destroyRef.onDestroy(() => {
      console.log('Component 2 was destroyed');
    });
  }
}

@Component({
  standalone: true,
  imports: [Component1, Component2],
  selector: 'app-root',
  styleUrl: 'main.css',
  template: `
  <button type="button" (click)="injectC1()" >Inject Component 1</button>
  <button type="button" (click)="injectC2()" >Inject Component 2</button>
  <div class="grid">
    <div class="container">this is static</div>
    <div class="container">
    <ng-container #targetSpace />
    </div>
  </div>
  `,
})
export class AppComponent {
  @ViewChild('targetSpace', {
    read: ViewContainerRef,
  })
  private targetSpace!: ViewContainerRef;

  public injectC1() {
    const componentRef = this.targetSpace.createComponent(Component1);
    outputToObservable(componentRef.instance.clicked)
      .pipe(takeUntilDestroyed(componentRef.instance.destroyRef))
      .subscribe(() => {
        componentRef.destroy();
      });
  }

  public injectC2() {
    const componentRef = this.targetSpace.createComponent(Component2);
    outputToObservable(componentRef.instance.clicked)
      .pipe(takeUntilDestroyed(componentRef.instance.destroyRef))
      .subscribe(() => {
        componentRef.destroy();
      });
  }
}

bootstrapApplication(AppComponent, {
  providers: [provideExperimentalZonelessChangeDetection()],
});

main.css
.grid {
  margin-top:5px;
  display:grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap:5px;
}
.container {
  border: 1px solid green;
}
c1.css
.c1{
  background-color:#FF0000;
  color:#FFF;
}
c2.css
.c2{
  background-color:#0000FF;
  color:#FFF;
}

Appendixes

Working with Modules

It is possible that you will encounter a project with a module. Previous versions of Angular, prior to version 16, used modules to group components and other Angular features to organize your code. However, this approach was not very effective from the perspective of developer experience (you spend more time grouping/organizing code than creating features to work right away). If you are using a module in your project, the needed components for injection have to be declared in the declarations section:

@NgModule({
  /**
   * Declare your components to be injected in the declarations section
   */
  declarations: [
    AppComponent, // your main component
    ComponentToInject1, // component to be used on programatic injection
    ComponentToInject2, // component to be used on programatic injection
  ],
  imports: [BrowserModule],
  providers: [CurrentDateService],
  bootstrap: [AppComponent],
})
export class AppModule {}

Most of the code from the standalone approach will work here. Standalone components can't work along with Angular Modules, so you have to remove the definition for standalone in your component:

@Component({
-  standalone: true,
  selector:'app-component2',
  styleUrl:'c2.css',
 template: `
  <div class="c2" (click)="clicked.emit()">This is Component 1</div>
  `
})
export class Component2 {
  public clicked = output<void>()
}

The only part that needs to be considered is the definition of the module and how it will load in the final code.

This is how the final code looks like now with a module:

main.ts
import {platformBrowser} from '@angular/platform-browser';
import { AppModule } from './app.module';

platformBrowser()
  .bootstrapModule(AppModule)
  .catch((err) => console.error(err));
app,module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import {Component1} from './c1.component';
import {Component2} from './c2.component';

@NgModule({
  declarations: [
    AppComponent,
    Component1,
    Component2,
  ],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}
app.component.ts
import {Component, ViewChild, ViewContainerRef} from "@angular/core";
import {Component1} from "./c1.component";
import {Component2} from "./c2.component";

@Component({
  selector:'app-root',
  styleUrl:'main.css',
  template: `
  <button type="button" (click)="injectC1()" >Inject Component 1</button>
  <button type="button" (click)="injectC2()" >Inject Component 2</button>
  <div class="grid">
    <div class="container">this is static</div>
    <div class="container">
    <ng-container #targetSpace />
    </div>
  </div>
  `
})
export class AppComponent { 
  @ViewChild('targetSpace', {
    read: ViewContainerRef
  })
  private targetSpace!:ViewContainerRef;

  public injectC1(){
    const componentRef = this.targetSpace.createComponent(Component1)
    componentRef.instance.clicked
    .subscribe( () => {
      componentRef.destroy();
    })
  }

  public injectC2() {
    const componentRef = this.targetSpace.createComponent(Component2)
    componentRef.instance.clicked
    .subscribe( () => {
      componentRef.destroy();
    })
  }
}
c1.component.ts
import {Component, output} from "@angular/core";

@Component({
  selector:'app-component1',
  styleUrl :'c1.css',
  template: `
  <div class="c1" (click)="clicked.emit()">This is Component 1</div>
  `
})
export class Component1 {
  public clicked = output<void>()
}
c2.component.ts
import {Component, output} from "@angular/core";

@Component({
  selector:'app-component2',
  styleUrl :'c2.css',
  template: `
  <div class="c2" (click)="clicked.emit()">This is Component 2</div>
  `
})
export class Component2 {
  public clicked = output<void>()
}
main.css
.grid {
  margin-top:5px;
  display:grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap:5px;
}
.container {
  border: 1px solid green;
}
c1.css
.c1{
  background-color:#FF0000;
  color:#FFF;
}
c2.css
.c2{
  background-color:#FF0000;
  color:#FFF;
}

Summary

Understanding how programatic injection works can lower down the architecture complexity of applications, making those quite maintainable and easy to operate. I hope you enjoyed this reading.


Note 1: I have added notes regarding component destruction events and how to handle this on the context of this exercise (June 10th, 2024). Shoutouts to Jeff Getzin who pointed out the need of documenting the handling of this kind of event.

Last Update: June 10, 2024