- Developer and Blogger

In the following article I will show how to use ComponentFactoryResolver in Angular to insert components dynamically into the DOM

In Angular we typically load a component as a tag in the markup, perhaps assisted by some conditionals or for loops to make the loading more dynamic. Generally this is the recommended approach, but there are cases where you don't know the DOM structure at compile time. In these cases we need to be able to add components on demand in arbitrary locations. It might be tempting to fall back on jQuery for this, but Angular provides a ComponentFactoryResolver that we can use instead.

To demo this feature I have created a graph component where I am letting the user connect vertices with edges to form a graph. I am modeling the edges as separate components, but since the user can connect any pair of nodes, there is no way to know ahead of time where to put the edge components.

I decided to model this using three components; Graph, Vertex and Edge. Graph and Vertex instances are statically placed in the template to form a selection of disconnected nodes. By clicking on two different vertices, an edge is drawn to connect them. This is where ComponentFactoryResolver comes into play by allowing me to load components into the DOM.

The code for the three components looks like the following:

import {Component, ElementRef, ViewContainerRef, ViewChild} from '@angular/core';
import {EdgeService} from './edge-service';
import {Coordinates} from './coordinates';
@Component({
selector: 'vertex',
inputs:['value'],
template: `<div #vertex class="vertex" (click)="setCoordinates()"><span class="vertex-text">{{value}}</span></div>`
})
export class Vertex {
@ViewChild('vertex') element: ElementRef;
value:string;
constructor(private edgeService:EdgeService, private vc: ViewContainerRef){}
setCoordinates(){
let offsetLeft = this.element.nativeElement.offsetLeft;
let offsetTop = this.element.nativeElement.offsetTop;
this.edgeService.next(new Coordinates(offsetLeft, offsetTop, this.vc));
}
}

Vertex defines a node in the graph and exposes the x,y coordinates of the node in the DOM. When a vertex is clicked, the coordinates are emitted using rxjs via the EdgeService.

Next up is the Edge component.

import {Component, Input} from '@angular/core';
import {Coordinates} from './coordinates';
@Component({
selector: 'edge',
template: '<div class="edge" [ngStyle]="style"></div>'
})
export class Edge {
style = {};
x1:number;
y1:number;
x2:number;
y2:number;
setCoordinates(first, second){
this.x1 = first.x;
this.y1 = first.y;
this.x2 = second.x;
this.y2 = second.y;
this.drawLine(this.x1,this.y1,this.x2,this.y2);
}
drawLine(x1, y1, x2, y2){
var length = Math.sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2));
var angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
var transform = 'rotate('+angle+'deg)';
this.style = {
'position': 'absolute',
'transform': transform,
'width':length
};
}
}

Basically the purpose of this component is to draw a line between two sets of coordinates. Part of this involves calculating the angle of the line between the points which can be found using atan2 and some supporting calculations.

Graph is the container component and the part that ties it all together.

import {Component, ComponentFactoryResolver, ViewChild, OnInit} from '@angular/core';
import {Edge} from './edge';
import {Vertex} from './vertex';
import {EdgeService} from './edge-service';
@Component({
selector: 'graph',
templateUrl: './components/algorithms/graph/graph.html',
providers:[EdgeService]
})
export class Graph implements OnInit {
constructor(private componentfactoryResolver: ComponentFactoryResolver, private edgeService:EdgeService){}
ngOnInit(){
this.edgeService.getCoordinates().subscribe(coordinates => {
let factory = this.componentfactoryResolver.resolveComponentFactory(Edge);
let res = coordinates.first.viewContainer.createComponent(factory);
res.instance.setCoordinates(coordinates.first, coordinates.second);
});
}
}

The graph component subscribes to the coordinates from the clicked vertices via an rxjs observable. The stream is set up to buffer and not return anything until two vertices have been clicked.

EdgeService is pretty simple with some buffering logic to ensure we have two sets of coordinates before we send the values off to the subscriber.

import {Subject } from 'rxjs/Subject';
import {Coordinates} from './coordinates';
export class EdgeService extends Subject<Coordinates>{
getCoordinates(){
return this.asObservable().bufferCount(2).map(buffer => {return {first:buffer[0], second:buffer[1]}});
}
}

ComponentFactoryResolver allows us to locate a component factory by component type, but before we can resolve a factory for the Edge component, we have to tell Angular to generate one by listing Edge as an entryComponent in the current NgModule.

We use the component factory in combination with a ViewContainerRef to load the component into the DOM. coordinates.first.viewContainer is a ViewContainerRef and serves as a reference point for where to place the component. coordinates.first.viewContainer refers to the first vertex we clicked.<div>
<h1>Create a Graph</h1>
<div class="alert alert-info" role="alert">
Click any pair of nodes to connect them with an edge
</div>
<vertex [value]="'A'"></vertex>
<table class="graph-table">
<tr>
<td><vertex [value]="'B'" /><td><vertex [value]="'C'" /></td><td><vertex [value]="'D'" /></td>
</tr>
<tr>
<td><vertex [value]="'E'" /></td><td><vertex [value]="'F'" /></td>
</tr>
<tr>
<td><vertex [value]="'G'" /></td><td><vertex [value]="'H'" /></td><td><vertex [value]="'I'" /></td>
</tr>
</table>
</div>

As you can tell from the screenshot below clicking pairs of nodes will enable you to connect any pair of nodes.

As always the code is available on Github and a live demo is of course also available.