Dynamically Creating Components With Angular 2.0

Written by Christopher Gosselin

Dynamically Creating Components With Angular 2.0

The Evolution

Creating components has been a topic of interest at Rangle because with each RC version of Angular 2, it seemed like the way to do so had changed.

Near the beginning there was the DynamicComponentLoader.

After that was deprecated, there was ComponentResolver.

And now that the dust has settled, there is the ComponentFactoryResolver!

The (Main) Reason

Why would I ever want to dynamically create a component? Modals

  • Imagine throughout your app you have buttons that trigger different modals to open
  • Some of these modals need to be initialized with some information for their state
  • And as we know, modals are best kept as children of the body tag to avoid issues

A good solution to this problem would be a modal factory.
The modal factory would:

  • Be given the component class, representing the modal we want, along with any information it needs
  • Use this information and create the modal on the fly
  • Keep reference to the created component and destroy it at some point

The Code / Explanation

Imagine we have a simple component called App that contains two buttons:

  • One to create a hello world modal component
  • One to create a world hello modal component

In App we have another component with the selector dynamic-component. This component takes a property called componentData which gives reference to the component we want to create and any information we want to pass down.

It could look something like this:

@Component({
  selector: 'my-app',
  template: `
    <div>
      <h2>Lets dynamically create some components!</h2>
      <button (click)="createHelloWorldComponent()">Create Hello World</button>
      <button (click)="createWorldHelloComponent()">Create World Hello</button>
    </div>
    <dynamic-component [componentData]="componentData"></dynamic-component>
  `,
})
export class App {  
  componentData = null;

  createHelloWorldComponent(){
    this.componentData = {
      component: HelloWorldComponent,
      inputs: {
        showNum: 9
      }
    };
  }

  createWorldHelloComponent(){
    this.componentData = {
      component: WorldHelloComponent,
      inputs: {
        showNum: 2
      }
    };
  }
}

By clicking on either of the Create Hello World or Create World Hello buttons, the appropriate event will fire, sending data into dynamic-component.
The dynamic-component will now generate the appropriate component using the information given.

Now let's look at how this DynamicComponent would be implemented.

This component will need a few things:

  • Knowledge of all possible components it will be creating
  • A place to put a created component
  • Access to the ComponentFactoryResolver service
  • A variable to store a created component so we can remove it later
import {Component, ViewContainerRef, ViewChild, ReflectiveInjector, ComponentFactoryResolver} from '@angular/core';  
import HelloWorldComponent from './hello-world-component';  
import WorldHelloComponent from './world-hello-component';

@Component({
  selector: 'dynamic-component',
  entryComponents: [HelloWorldComponent, WorldHelloComponent], // Reference to the components must be here in order to dynamically create them
  template: `
    <div #dynamicComponentContainer></div>
  `,
})
export default class DynamicComponent {  
  currentComponent = null;
  @ViewChild('dynamicComponentContainer', { read: ViewContainerRef }) dynamicComponentContainer: ViewContainerRef;

  constructor(private resolver: ComponentFactoryResolver) {

  }

  …
}

As you can see above, entryComponents is where we inform DynamicComponent of the possible components we want it to create.

We’ve attached a reference to a div in our class so that we can access it and place the component we create inside of it.

We’ve injected the ComponentFactoryResolver into the class.

And lastly, we've created a variable called currentComponent to store the created component.

With all of the setup complete, we’re able to move on to the actual component creation!

Let's implement an Input setter that takes a component class and an object with key/value pairs mapping to what we want access in the created component.

  // component: Class for the component you want to create
  // inputs: An object with key/value pairs mapped to input name/input value
  @Input() set componentData(data: {component: any, inputs: any }) {
    if (!data) {
      return;
    }

    // Inputs need to be in the following format to be resolved properly
    let inputProviders = Object.keys(data.inputs).map((inputName) => {return {provide: inputName, useValue: data.inputs[inputName]};});
    let resolvedInputs = ReflectiveInjector.resolve(inputProviders);

    // We create an injector out of the data we want to pass down and this components injector
    let injector = ReflectiveInjector.fromResolvedProviders(resolvedInputs, this.dynamicComponentContainer.parentInjector);

    // We create a factory out of the component we want to create
    let factory = this.resolver.resolveComponentFactory(data.component);

    // We create the component using the factory and the injector
    let component = factory.create(injector);

    // We insert the component into the dom container
    this.dynamicComponentContainer.insert(component.hostView);

    // Destroy the previously created component
    if (this.currentComponent) {
      this.currentComponent.destroy();
    }

    this.currentComponent = component;
  }

The function above does the following:

  • Turns the inputs into a format Angular 2 can understand
  • Creates an injector out of the resolved inputs and DynamicComponent injector
  • Creates a factory out of the component we want to create
  • Creates a component out of the factory and injector
  • Inserts the component into the placeholder container we created earlier
  • Destroys the previously created component

And there you have it, we’ve dynamically created a modal!

The Example

Here's a simple Plunker I made demonstrating what was explained above: http://plnkr.co/edit/ZXsIWykqKZi5r75VMtw2?p=preview

More Resources