Friday, 13 January, 2017 UTC


Summary

This is a guest post from Sean Landsman at ag-Grid. Sean is the lead engineer on ag-Grid's Angular integration. -- Stephen Fluin


Motivationag-Grid is an enterprise datagrid that works with Angular. As ag-Grid works with many frameworks, the internals of the grid had to allow for Angular rendering inside the grid despite ag-Grid not being written in Angular itself. This was done using Angular Dynamic Components and we managed to do it while still supporting AOT. This blog details what we learnt along the way.

The SetupTo explain we present a simple sample application that isolates what we are trying to do. In our example below we are going to develop two main Modules - one will be a Library (in our case this was ag-Grid) that will display an array of dynamically created Components (similar to how ag-Grid displays Angular components inside the grid's cells), and the other will be our actual Application.
The end result will be look like this:




You can find all the code for this example over at GitHub, and the live example over at GitHub.io
One further note - when we return to "user" below, we are referring to a user (or client) of the Library we're writing.

The Library

Our Library is going to be a simple one - all it does is display an array of dynamically created Angular Components. The main component looks like this:

@Component({
selector: 'grid-component',
template: `
<div class="row" *ngFor="let cellComponentType of cellComponentTypes">
<div class="col-lg-12">
<grid-cell [componentType]="cellComponentType"></grid-cell>
</div>
</div>
`
})
export class Grid {
@Input() componentTypes: any;

cellComponentTypes: any[] = [];

addDynamicCellComponent(selectedComponentType:any) {
this.cellComponentTypes.push(selectedComponentType);
}
}

As you can see it's a pretty simple component - all it does is display the current cellComponentTypes. These are the user supplied components, and they can be any Angular Component.

The interesting part of the Library is in the Cell Component:

@Component({
selector: 'grid-cell',
template: ''
})
export class Cell implements OnInit {
@Input() componentType: any;

constructor(private viewContainerRef: ViewContainerRef,
private cfr: ComponentFactoryResolver) {
}

ngOnInit() {
let compFactory = this.cfr.resolveComponentFactory(this.componentType);
this.viewContainerRef.createComponent(compFactory);
}
}
You'll notice that we don't have a template here - that's deliberate as the Cell doesn't have any content of its own - all it does is serve up the user supplied Component. The important part of this Component are these two lines:

let compFactory = this.cfr.resolveComponentFactory(this.componentType);
This line asks the ComponentFactoryResolver to find the ComponentFactory for the provided Component. We'll use this factory next to create the actual component:

this.viewContainerRef.createComponent(compFactory);
And that's all there is to it from the Library Component side of things - we find the factory for the Component, and then create a new instance of the Component. Easy!

For this to work we need to tell Angular's AOT Compiler to create factories for the user provided Components, or ComponentFactoryResolver won't find them. We can make use of NgModule.entryComponents for this - this will ensure that the AOT compiler creates the necessary factories, but for you purposes there is an easier way, especially from a users perspective:

@NgModule({
imports: [
BrowserModule,
FormsModule
],
declarations: [
Grid,
Cell
],
exports: [
Grid
]
})
export class GridModule {
static withComponents(components: any[]) {
return {
ngModule: GridModule,
providers: [
{provide: ANALYZE_FOR_ENTRY_COMPONENTS, useValue: components, multi: true}
]
}
}
}
By making use of ANALYZE_FOR_ENTRY_COMPONENTS here, we are able to add multiple components to the NgModule.entryComponents entry dynamically, in a user friendly way.

The Application

From the application side of things, the first thing we need to do is create the components we want to use in the Library - these can be any valid Angular Component. In our case we have three similar Components:

@Component({
selector: 'dynamic-component',
template: '<div class="img-rounded" style="background-color: lightskyblue;margin: 5px"> Blue Dynamic Component! </div>',
})
export class BlueDynamicComponent {
}
All these components do is display a little styled text.
To register these in both our Application, and in the Library, we need to switch to the Application Module:

@NgModule({
imports: [
BrowserModule,
FormsModule,
GridModule.withComponents([
BlueDynamicComponent,
GreenDynamicComponent,
RedDynamicComponent
])
],
declarations: [
AppComponent,
BlueDynamicComponent,
GreenDynamicComponent,
RedDynamicComponent
],
bootstrap: [AppComponent]
})
export class AppModule {
}
We declare our Components in the usual way, but we additionally need to register them with the Library (remember, this is the part where they'll be added to the Library's NgModule.entryComponent entry). We do this in this part of the module:

GridModule.withComponents([
BlueDynamicComponent,
GreenDynamicComponent,
RedDynamicComponent
])
Finally, we can take a look at the main Application Component:

@Component({
selector: 'my-app',
template: `
<div class="container-fluid">
<div class="page-header">
<h1>Creating AOT Friendly Dynamic Components with Angular
</div>
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-heading">Application Code</div>
<div class="panel-body">
<div class="input-group">
<span class="input-group-btn">
<button type="button" class="btn btn-primary" (click)="grid.addDynamicCellComponent(selectedComponentType)">Add Dynamic Grid component
</span>

<select class="form-control" [(ngModel)]="selectedComponentType">
<option *ngFor="let cellComponentType of componentTypes" [ngValue]="cellComponentType">{{cellComponentType.name}}
</select>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-heading">Library Code</div>
<div class="panel-body">
<grid-component #grid></grid-component>
</div>
</div>
</div>
</div>
</div>
`
})
export class AppComponent implements OnInit {
@Input() componentTypes: any[] = [BlueDynamicComponent, GreenDynamicComponent, RedDynamicComponent];
@Input() selectedComponentType: any;

ngOnInit(): void {
// default to the first available option
this.selectedComponentType = this.componentTypes ? this.componentTypes[0] : null;
}
}

It may look like theres a lot going on here, but the bulk of the template is to make it look pretty. The key parts of this Component are:

<button type="button" class="btn btn-primary" (click)="grid.addDynamicCellComponent(selectedComponentType)">Add Dynamic Grid component

This will ask the Library to add a create a new instance of the supplied Component, and in turn render it.

<grid-component #grid></grid-component>
And this line is our Library Component.

That's it - easy to write and use (from both an Application and Library perspective), and AOT (and JIT!) friendly.

Benefits of using AOTThe speed and size of the resulting application when using AOT can be significant. In our ag-grid-ng2-example project, we estimate the size of the resulting application went from 3.9Mb down to 2.4Mb - a reduction of just under 40%, without optimising for size or being particularly aggressive with rollup.

Speed-wise, the loading time when using AOT is significantly more responsive on startup - this makes sense given that Angular doesn't have to compile all the code once again. Take a look at the examples project and try both the JIT and AOT versions out for yourself!

There's so much more you can do if you decide to combine Angular Components with ag-Grid - powerful functionality, fast grid and easy configuration. What are you waiting for?!