Angular Dynamic Components

Search for the answer to the question, "Can I render a server-side component in Angular 2 (Angular)?"  You will find answers to questions about Angular 2 Universal.  Universals apps ... 

Manage your application lifecycle and serialize changes while on the server to be sent to the browser

This article is not about universal apps. It's about your component definitions

  • already fetched from the server;
  • as dependencies of your app's bootstrapped module, and 
  • loaded dynamically.  

I took the ToDo app created from Gary Simon's Pluralsight course "Working with Angular 2 Animations", and added a Tips panel below the item list.  Each tip is created by a TipService class and rendered dynamically.  

Tip Model


import
{ Type } from '@angular/core'; export class Tip { constructor(public component: Type<any>, public data: any) { } }  

 

TipService


import
{ Injectable } from '@angular/core'; import { Tip } from './tip'; import { TipComponent } from './tip/tip.component' @Injectable() export class TipService { constructor() { } getTips() { return [ new Tip(TipComponent, { tip: "A dynamically loaded tip just arrived." }), new Tip(TipComponent, { tip: "Yet another dynamically loaded tip worked flawlessly." }) ] } } 

 

Here are the dynamically loaded, animated tip components below.  You can interact with the live demo here: https://dkemper01.github.io/todos-animated-dynamic-components/. The repo is here: https://github.com/dkemper01/todos-animated-dynamic-components/.

 

Definitions

The definition for ViewContainerRef was taken from https://angular.io/docs/ts/latest/

ComponentFactory: A core Angular class that can both create a component (via its create method) or can be used by a ViewContainerRef instance to create a component.

ComponentFactoryResolver: A class used to resolve a ComponentFactory via method resolveComponentFactory(component: Type<T>), for each specific component, more specifically each component’s type. The component factory is needed to create an instance of the component. ComponentFactories are generated by the Angular compiler. Generally the compiler will generate a component factory for any component referenced in a template.

ViewContainerRef: Represents a container where one or more Views can be attached. The container can contain two kinds of Views. Host Views, created by instantiating a Component via createComponent, and Embedded Views, created by instantiating an Embedded Template via createEmbeddedView. The location of the View Container within the containing View is specified by the Anchor element. Each View Container can have only one Anchor Element and each Anchor Element can only have a single View Container. Root elements of Views attached to this container become siblings of the Anchor Element in the Rendered View. To access a ViewContainerRef of an Element, you can either place a Directive injected with ViewContainerRef on the Element, or you obtain it via a ViewChild query (https://angular.io/docs/ts/latest/api/core/index/ViewContainerRef-class.html#!#createComponent-anchor).

@ViewChild: A component decorator that, according to the documentation, configures a view query.  This means you can use the decorator as a syntactic element of the component class which acts like a type used to define a member variable which references your dynamic view.  The @ViewChild(…) decorator can take either a type name or a type.  For example: 

@ViewChild(LoadDynamicDirective) componentHost: LoadDynamicDirective;

Coding for Dynamic Components

Aside from the lightweight directive you will create which takes the ViewContainerRef instance as a dependency, it is the component you will create that takes the ComponentFactoryResolver as a dependency that does the actual work of loading the component dynamically. In essence, you need both a way to create a Component from its corresponding type, and a place to put that component.

See the Pen The Magic of Font Awesome and GSAP by Daniel Kemper (@dotComb) on CodePen.

A ComponentFactory object at work: a type goes in, and a component is returned. 

 


import
{ Directive, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[load-dynamic]' }) export class LoadDynamicDirective { // Inject ViewContainerRef to gain access to the view container of the element that // will become the host of the dynamically added component. // constructor(public viewContainerRef: ViewContainerRef) { } }

 

The reason a ViewContainerRef instance is needed, however, is because the ViewContainerRef.createComponent method here:

createComponent(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][]) : ComponentRef<C>

does the following;

...instantiates a single Component and inserts its Host View into the dynamic loader component container at the specified index.

This effectively creates and loads a component into the component tree of the app at runtime. If index is not specified, the new View will be inserted as the last View in the container. The component is instantiated using its ComponentFactory which can be obtained via ComponentFactoryResolver.

 


import
{ Component, Input, AfterContentInit, AfterViewInit, ViewChild, ComponentFactoryResolver, OnInit, OnDestroy } from '@angular/core'; import { LoadDynamicDirective } from '../load-dynamic.directive'; import { TipComponent } from '../tip/tip.component'; import { Tip } from '../tip'; @Component({ selector: 'dynamic-loader', templateUrl: './dynamic-loader.component.html', styleUrls: ['./dynamic-loader.component.scss'] }) export class DynamicLoaderComponent implements OnInit, AfterViewInit, OnDestroy { @Input() tips: Array<Tip>; currentTipIndex: number = -1; @ViewChild(LoadDynamicDirective) componentHost: LoadDynamicDirective; subscription: any; interval: any; constructor(private _componentFactoryResolver: ComponentFactoryResolver) { } ngOnInit() { } ngAfterContentInit() { this.loadComponent(); } ngAfterViewInit() { this.getTips(); } ngOnDestroy() { clearInterval(this.interval); } loadComponent() { this.currentTipIndex = (this.currentTipIndex + 1) % this.tips.length; let tip = this.tips[this.currentTipIndex]; let componentFactory = this._componentFactoryResolver.resolveComponentFactory(tip.component); let viewContainerRef = this.componentHost.viewContainerRef; viewContainerRef.clear(); let componentRef = viewContainerRef.createComponent(componentFactory); (<TipComponent>componentRef.instance).data = tip.data; } getTips() { this.interval = setInterval(() => { this.loadComponent(); }, 3000); } }