Dynamically Loading Angular 2 components

In Angular 2 Component templates are not always fixed. An application may need to load new components at runtime. This tutorial shows you how to use ComponentFactoryResolver to add components dynamically.

Let’s start with a requirement about why we need to load components dynamically in Angular 2. To understand that first we need to understand how components in Angular 2 are rendered in HTML DOM, how are they organized in Angular 2 module. I’ll not go into lot of detail about Angular 2 modules and how code is organized but, will try to give you fair understanding so that you can see why we need to load components dynamically.

An Angular 2 application typically will have one or more NgModule (@NgModule) and there will be at least one root NgModule. Many Angular libraries are modules (such as FormsModule, HttpModule, and RouterModule). Many third-party libraries are available as NgModules (such as Material Design, Ionic, AngularFire2). Not necessarily but, by convention, the root module class is called AppModule and it exists in a file named app.module.ts. NgModule consolidate components (bootstrap and entry components), directives, and pipes into cohesive blocks of functionality, each focused on a feature area, application business domain, workflow, or common collection of utilities.

Modules can also add services to the application. Such services might be internally developed, such as the application logger. Services can come from outside sources, such as the Angular router and Http client. Modules can be loaded eagerly when the application starts. They can also be lazy loaded asynchronously by the router. Here is an example of NgModule:

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent }  from './app.component';

@NgModule({
 imports:      [ BrowserModule ],
 declarations: [ AppComponent, Component1, Component2 ],
 bootstrap:    [ AppComponent ],
 entryComponents:    [ EntryComponent ],
 providers: [ Services/Providers ],
})
export class AppModule { }

The @NgModule decorator defines the metadata for the module. This page takes an intuitive approach to understanding the metadata and fills in details as it progresses. The metadata imports a single helper module, BrowserModule, which every browser app must import. BrowserModule registers critical application service providers. It also includes common directives like NgIf and NgFor, which become immediately visible and usable in any of this module's component templates.

The bootstrap list identifies the components (in this case AppComponent), called root component. When an Angular 2 application is starting, root component (s) are first one that gets attached/rendered in HTML DOM, this is the line that does this:

platformBrowserDynamic().bootstrapModule(AppModule);
All other components can be used within the scope of root component (i.e. other components must be loaded in root component). Here is a example HTML code where AppComponent will be bootstrapped:

<html>
<head>

</head>
<body>
<app></app>
<body>
</html>

Note that, and Angular 2 application can have more than one root component too that will have its own unique selector (e.g. ).

Now, let’s say we want to render “Component1” (with selector ) outside of the root component (app) then it’ll not render and this is where we need “ComponentFactoryResolver” to dynamically load component. This is just one scenario where you’ll need dynamic component loading. Think of another situation where you are getting HTML from some external service and that HTML contains some selectors that needs to be rendered as Angular 2 components then again we’ll need dynamic component loading.

So, how do we load components dynamically?

We need 2 main things:
  1. A dynamic component loader class that make use of
  2. A hook during application bootstrap which will use dynamic component loader

  1. Dynamic Component Loader class: DynamicNg2Loader class that will load Angular 2 components dynamically at runtime outside of the root component. This is needed because in AEM a component (Angular 2) can be used many time and that too outside of root component's scope. If a component is outside of root component's scope Angular 2 will ignore it and will not render and that's why we need this DynamicNg2Loader. Here is sample code for DynamicNg2Loader:
import { REMOVED_FOR_SIMPLICITY} from '@angular/core';

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

   constructor(private ngModuleRef:NgModuleRef<any>) {
       this.injector = ngModuleRef.injector;
       this.appRef = this.injector.get(ApplicationRef);
       this.zone = this.injector.get(NgZone);
       this.componentFactoryResolver = this.injector.get(ComponentFactoryResolver);
       console.log(this.componentFactoryResolver);
   }

   /**
    * Render component in DOM
    */
   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;
   }
}

  1. Hook that will use Dynamic Component Loader: Bootstrap AppModule and use DynamicNg2Loader loader to render other components which are outside of root/bootstrapped component's scope. Here is sample code:

const componentList = {'text-area': TextAreaComponent, 'task-list': TaskListComponent, 'about': AboutComponent, 'task': TaskListComponent};

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

   Object.keys(componentList).forEach(function(selector) {
       let container = document.getElementsByTagName(selector);
       if (container) {
           for (let i = 0; i < container.length; i++) {
               let element = container.item(i);
              let compRef = ng2Loader.loadComponentAtDom(componentList[selector], element, (instance) => {
                   console.log('Text Area Component Loaded');
               });
           }
       }
   });
});


I hope you have enjoyed reading this post!

Comments

Unknown said…
Thanks, helped a lot!
Unknown said…
This comment has been removed by the author.

Popular posts from this blog

AEM as a Cloud Service (AEMaaCS) – Architecture Overview

Custom synchronisation or rollout action for blueprint ?

AEM as a Cloud Service (AEMaaCS) – Quick Introduction

Generating URL based on AEM RUN mode using AEM Externalizer Service