Using Angular 2 Components In a Non-Angular App

Lucidpress is a large application—hundreds of thousands of lines of handwritten JavaScript. After seeing the success Lucidchart had in modernizing its UI with Angular, we wanted to follow suit, but we didn’t have the resources to do a wholesale rewrite all at once. We wanted to write some new Angular 2 components and even reuse several big components from Lucidchart, but we needed to be able to fire up individual components scattered around the application, without having one big Angular app controlling them all.

This use case isn’t one that’s well-documented by the Angular team, but it turns out that it can be done cleanly and without a whole lot of boilerplate code for bootstrapping each component. In this blog post, I’ll walk you through the process of setting up a few Angular 2 components in a simple non-Angular JavaScript application.

(Note: Checkout this post If you are looking for ways to load a component dynamically from within Angular)

An example app

Say we have a simple to-do app that maintains a list of things to do. It lets you add items to the list and lets you mark them as done. An Angular implementation in typescript might look something like this:


@Component({
  selector: 'check-list',
  template: `
    <div>
      <h2>My Checklist in angular 2</h2>
      <input type="text" #itemInput/>
      <button (click)="addToList(itemInput.value)"> add new item </button>
      <div class="flex-container" *ngFor="let item of items">
        <check-list-item [value]="item"> </check-list-item>
      </div>
    </div>
  `,
  styles:[`
    flex-container { display:flex; flex-direction: column }
  `]
})
export class CheckList {
  items: string[] = [];
  constructor() {}
  addToList(item: string) {
    this.items.push(item);
  }
}

@Component({
  selector: 'check-list-item',
  template: `
    <input type="checkbox"/>
    <label>{{value}}</label>
  `,
  styles:[`
    input[type=checkbox]:checked + label {text-decoration: line-through;}
  `]
})
export class CheckListItem {
  @Input() value: string = "";
  constructor(){}
}

 

Screenshot showing a simple to-do app
Our example to-do app

Each entry in the checklist is represented by a CheckListItem component, and the CheckList app maintains the list of items in the checklist. We can then use this in our angular app simply by adding <check-list></check-list> before bootstrapping the module.

Loading components from outside Angular dynamically

The method above works well when the <check-list></check-list> tags are already on the HTML page before the Angular app is bootstrapped. Then, as long as CheckList is a bootstrap component, Angular will load the components at the tag on bootstrap. What if we want to do this dynamically from outside Angular? Looking at the Angular source code for ApplicationRef, we see the following:

 /**
   * Attaches a view so that it will be dirty checked.
   * The view will be automatically detached when it is destroyed.
   * This will throw if the view is already attached to a ViewContainer.
   */
  abstract attachView(view: ViewRef): void;

 

So if we have some way of providing the component’s ViewRef, we should be able to dynamically load components. In fact, this is exactly what Angular does as part of its bootstrapping. We will do this by creating a DynamicNg2Loader class that looks like this:

import {Type, ApplicationRef, ComponentFactoryResolver, Component, ComponentRef, Injector, NgZone} from '@angular/core';


export class DynamicNg2Loader {
    private appRef: ApplicationRef;
    private componentFactoryResolver: ComponentFactoryResolver;
    private zone:NgZone;

    constructor(private injector:Injector) {
        this.appRef = injector.get(ApplicationRef);
        this.zone = injector.get(NgZone);
        this.componentFactoryResolver = injector.get(ComponentFactoryResolver);
    }

    loadComponentAtDom<T>(component:Type<T>, dom:Element, onInit?: (Component:T) => void): ComponentRef<T> {
        let componentRef;
        this.zone.run(() => {
            try {
                let componentFactory = this.componentFactoryResolver.resolveComponentFactory(component);
                componentRef = componentFactory.create(this.injector, [], dom);
                onInit && onInit(componentRef.instance);
                this.appRef.attachView(componentRef.hostView);
                
            } catch (e) {
                console.error("Unable to load component", component, "at", dom);
                throw e;
            }
        });
        return componentRef;
    }
}

 

There is quite a bit going on here, so let’s break it down. The constructor takes an Ng2Module Injector. We will explain how to get the injector for your Ng2Module in a minute, but assuming you have an injector, we can then get a reference to the ApplicationRef, NgZone, and  ComponentFactoryResolver for the loaded module. The loadComponentAtDom function takes a reference to the Component that we want to load (in our case the CheckListItem), an Element that is already part of the DOM, and an onInit function that is called once the component is loaded.  We can initialize and push values to the components directly through the component reference passed in through the onInit function. The dom element that is passed into loadComponentAtDom is the location where our Angular 2 component will be loaded.

Let’s step through the function to see what each line does.

let componentFactory = this.componentFactoryResolver.resolveComponentFactory(component);
componentRef = componentFactory.create(this.injector, [], dom); this.appRef.attachView(componentRef.hostView);

 

The first line gets the component factory for the component that we want to load. Remember that the component has to be an entry component for this to work. The second line creates a new instance of the component and returns a ComponentRef. The third line then calls the onInit function if it’s passed in with an instance of the Component type. Finally, the last line attaches the componentRef’s ViewRef to the ApplicationRef so that Angular can then perform change detection and other lifecycle events on the component. All of these need to happen inside Angular’s zone.

Coming back to the constructor, we can get the Angular module’s injector when bootstrapping the module like so:

platformBrowserDynamic().bootstrapModule(AppModule).then(function(ng2ModuleInjector){
     console.log(“I have a reference to the injector : “, ng2ModuleInjector);
     let ng2Loader = new DynamicNg2Loader(ng2ModuleInjector);
});

Putting it all together for our example

Let’s make our CheckListItem component an EntryComponent, bootstrap the module, and create an instance of the DynamicNg2Loader class.

@NgModule({
  imports: [ BrowserModule ],
  declarations: [ CheckList, CheckListItem ],
  entryComponents: [ CheckList, CheckListItem ],
})
export class AppModule {
     ngDoBootstrap() {}
}

And finally we can use the loader to load the component when required.

let loadedComponentReferences: ComponentRef = [];

platformBrowserDynamic().bootstrapModule(AppModule).then(function(ng2ModuleInjector){
  let ng2Loader = new DynamicNg2Loader(ng2ModuleInjector);
  let container = document.getElementById('angular-container');
  document.getElementById('non-angular').hidden = false;
  
  let count = 0;
  document.getElementById('add-component').onclick = function() {
    let parent = document.createElement('app-parent');
    container.appendChild(parent);
    let compRef = ng2Loader.loadComponentAtDom(CheckListItem, parent, (instance) => {
      instance.value = document.getElementById('text-input').value;
    });
    loadedComponentReferences.push(compRef);
  };
  document.getElementById('remove-components').onclick = function () {
    loadedComponentReferences.forEach(compRef => {
      compRef.destroy();
    });
  }
});

 

Screenshot showing the final to-do app in JS
A to-do app that dynamically loads Angular components

You can see the plunkr in action here.

7 Comments

  1. Not working if I try to use the component in a different website.

  2. I will be trying to incorporate this into a jQuery based rendering backend where the whole Dom object graph is created dynamically.
    @Harsh, why do think it won’t work.

  3. Can you extend this example to show how this could work to embed an angular app hosted separately from another web app? I would like to try to avoid using an iframe.

    Thanks!

  4. lebi lebiMay 28, 2018 at 7:55 am

    plunker is not working

  5. Dmitry PashkevichMay 30, 2018 at 3:32 pm

    Thanks lebi! There seems to be a problem with Plunkr, we reached out to them for support but haven’t heard back yet.

  6. I created a copy of this on stackblitz – works fine: https://stackblitz.com/edit/angular-3nu6em?file=src%2Fsrc%2Fmain.ts

Your email address will not be published.