/ angular

Dynamically create an Angular component and add it to the DOM

It is difficult to find good documentation on how to dynamically/programmatically instantiate an Angular component and add it to the DOM tree. This process is not as straightforward as one might hope.

I have created a "simple" service that encapsulates all that logic. See the code below. I annotated it with some comments to make it easier to understand what's going on.

component-factory.service.ts:

import {
  ApplicationRef, ComponentFactoryResolver, ComponentRef,
  EmbeddedViewRef, Injectable, Injector, Type, ViewContainerRef
} from '@angular/core';

@Injectable()
export class ComponentFactoryService {

  constructor(
      private cfr: ComponentFactoryResolver,
      private defaultInjector: Injector,
      private appRef: ApplicationRef) {
  }

  /**
   * Instantiates a component and adds it to the DOM.
   * @constructor
   * @param {Type<T>} componentType - Type of the component to create, e.g. "DynamicComponent".
   * @param {HTMLElement | ViewContainerRef} location - (Optional) Location where to inject the
   * component in the DOM, can be an arbitrary HTML element or a ViewContainerRef.
   * @param {Injector} injector - (Optional) Injector that should be used as a parent injector
   * for the component. This is useful only if you want to inject into the component services
   * that are provided in a different place from where ComponentFactoryService is provided.
   */
  createComponent<T>(
      componentType: Type<T>,
      location?: HTMLElement | ViewContainerRef,
      injector?: Injector): ComponentRef<T> {

    // Grab the instance of a factory for our component from Angular module.
    // Important: the component needs to be added to the "entryComponents" as part of the
    // "@NgModule" declaration on the same
    // module where this service is added to the "providers".
    let componentFactory = this.cfr.resolveComponentFactory(componentType);

    let componentRef: ComponentRef<T>;

    // We can handle two scenarios here depending on how the location where the component
    // needs to be injected is provided:
    // 1. ViewContainerRef. In this case we can just call location.createComponent and the
    //    component will be added as a child of the container automatically.
    // 2. HTMLElement. This can be just any HTML element in the DOM, even outside of the Angular
    //    application.

    if (location && location instanceof ViewContainerRef) {
      // The component will be added as a child of the container host view
      componentRef = location.createComponent(
        componentFactory,
        undefined /* index */,
        injector || this.defaultInjector);
    }
    else {
      // Here the location is any HTML element (could be outside of Angular app), so we need a bit
      // more work to "attach" it to our Angular app.

      // 1. Instantiate the component
      componentRef = componentFactory.create(injector || this.defaultInjector);

      // 2. Attach the component to the Angular application so that Angular knows about the
      //    component and performs change detection on it.
      this.appRef.attachView(componentRef.hostView);

      // 3. Insert the component HTML view as a child of the given HTML element or just as
      //    a child of "body" if no element is provided.
      this.addComponentToDom(location as HTMLElement || document.body, componentRef);
    }

    return componentRef;
  }

  private addComponentToDom<T>(parent: HTMLElement, componentRef: ComponentRef<T>): HTMLElement {
    // Grabe the actual HTML element of the component
    let componentElement = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;

    parent.appendChild(componentElement);

    return componentElement;
  }
}

And here is how you would use it. First, you need to add the service to the list of providers in your @NgModule, as well as add your dynamic component to the list of entryComponents on the same module:

app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component'
import { DynamicComponent } from './dynamic.component';
import { ComponentFactoryService } from './component-factory.service';

@NgModule({
  declarations: [
    AppComponent,
    DynamicComponent
  ],
  imports: [BrowserModule],
  providers: [ComponentFactoryService],
  bootstrap: [AppComponent],
  entryComponents: [DynamicComponent]
})
export class AppModule { }

Then, you can inject the ComponentFactoryService anywhere in your app and call createComponent passing the type of the component that you want to create, and optionally the location in the DOM tree:

home.component.ts:

import { Component } from '@angular/core';
import { ComponentFactoryService } from './component-factory.service';
import { DynamicComponent } from './dynamic.component';

@Component({
  selector: 'app-home',
  template: '<div #host></div>'  
})
export class HomeComponent {
   @ViewChild('host', {read: ViewContainerRef})
   hostEl: ViewContainerRef;
  
  constructor(private componentFactoryService: ComponentFactoryService) {    
  }

  // Option 1. Add the component as a child of "body"
  createComponentAndAddItToBody() {
    let componentRef = this.componentFactoryService.createComponent(DynamicComponent);
    
    // Access the instance of the component class via "componentRef.instance":
    // componentRef.instance.functionOnComponentClass();
  }
  
  // Option 2. Add the component as a child of the "div" element in the template
  // of this "HomeComponent".
  createComponentAndAddItAsChildOfHostDiv() {
    let componentRef = this.componentFactoryService.createComponent(DynamicComponent, this.hostEl);
    
    // Access the instance of the component class via "componentRef.instance":
    // componentRef.instance.functionOnComponentClass();
  }
}

Common use case

Perhaps the most common use case for dynamic components is modals (dialogs, panels, notifications). If you're looking for a complete and ready to use solution for modals, or want to see an example how the above code is used for this purpose, check out ind-modal - a lightweight implementation of modal dialogs/panels for Angular.

Pavlo Glazkov

Pavlo Glazkov

Programmer. Full stack, with a focus on UI. JavaScript/TypeScript, Angular, Node.js, .NET

Read More