Using Angular 2 Components in a Non-Angular App

July 06, 2017 0 Comments

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: &lt;input type=&quot;checkbox&quot;/&gt; &lt;label&gt;<span>{{value}}</span>&lt;/label&gt;, 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 CheckListapp 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 Ng2ModuleInjector. 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 onInitfunction 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.


Tag cloud