I was talking with one of my clients and they wondered how they could build individual components in Angular dynamically (or based on data/metadata). I knew this was possible but I hadn't done this myself so I thought I'd try to dig into it.
One of the interesting things is that you can get caught up in Angular's Reactive Forms which is an important technology but not really what I needed. What I wanted to be able to do was to create the layout from components. Turned out it wasn't that hard.
I started out with a new Angular project. I created a simple little component to show a bar with a percent complete (really simple):
import { Component } from "@angular/core";
@Component({
template: `<div class="border rounded border-gray-300 m-1 p-1">
<div>% Complete</div>
<div [innerHtml]="'█'.repeat(this.val)"></div>
</div>`
})
export class Gauge {
val = 0;
}
One of these looks like this:
I wanted to be able to create a number of these dynamically. Two things were required:
- Needed a way to get a container to inject the component into.
- Needed a way to generate a component (just calling new Gauge() wouldn't work).
Getting the Container
If you want to just get access to the top container in your template, you can just inject a ViewContainerRef object into your constructor:
@Component({
selector: 'app-root',
template: `
<div class="container mx-auto bg-white">
<div class="text-xl">Dashboard</div>
<div class="grid grid-cols-4">
</div>
</div>
`,
styles: []
})
export class AppComponent implements OnDestroy {
components: Array<ComponentRef<Gauge>> = [];
constructor(private ViewContainerRef container) { }
The problem with this approach is that I didn't want the top-level container, I wanted to inject it further inside the markup. I wanted to inject them into the the grid div. To do this, I added an ng-template inside the div:
<div class="container mx-auto bg-white">
<div class="text-xl">Dashboard</div>
<div class="grid grid-cols-4">
<ng-template #gauges ></ng-template>
</div>
</div>
Note that I used the #gauges to name the container so I could grab it. I did this with the @ViewChild decorator:
@ViewChild("gauges", { read: ViewContainerRef }) container: ViewContainerRef;
This wires up the container member as a ViewContainerRef (like the constructor inject did above) but for this specific element. Note, that for this to be wired up, you need to wait until after the View is initialized:
ngAfterViewInit(): void {
// container is now valid, ngOnInit is too early
}
So we have our container, how do we create new Gauge components?
Getting a Component Factory
To get a factory that can create the Gauge, we need a factory resolver which we can inject into our constructor:
constructor(private resolver: ComponentFactoryResolver) { }
With this resolver, we can resolve a factory for our component:
// Get a factory for a known component
const factory: ComponentFactory<Gauge> =
this.resolver.resolveComponentFactory(Gauge);
This gives us a factory to that can be used to generate the component. Then we can dynamically create a number of them:
// Dynamic creating them
for (let x = 0; x < 20; ++x) {
this.container.createComponent(factory);
}
The call to createComponent will create an insert it into our container. Notice that this is a method on the container that accepts the factory. To make sure we don't have an issue, we'll need to keep a handle on the component so that we can destroy it with onDestroy:
// Dynamic creating them
for (let x = 0; x < 20; ++x) {
const gauge = this.container.createComponent(factory);
// Keep a copy for destruction
this.myGauges.push(gauge);
}
Then just destroy them:
ngOnDestroy(): void {
for (let x = 0; x < this.myGauges.length; ++x) {
this.myGauges[x].destroy();
}
}
This works fine, but what if we need to set some state. Remember our Gauge has a val property to show the percentage. To do this, we can set properties on the gauge itself by looking at the instance (remember, the guage returned here is just a Ref to the component):
// Dynamic creating them
for (let x = 0; x < 20; ++x) {
const gauge = this.container.createComponent(factory);
// Set instance properties
gauge.instance.val = Math.ceil(Math.random() * Math.floor(20));
// Ensure that change detection happens once
gauge.changeDetectorRef.detectChanges();
// Keep a copy for destruction
this.myGauges.push(gauge);
}
In this case, I'm just setting a random number to each Gauge. But if you change the state after it's been created by the component, you'll need to tell the changeDetector to wire up the changes. Without that line, we get an consistency of change detection:
That's it.
You can get the complete code here:
This work by Shawn Wildermuth is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.
Based on a work at wildermuth.com.
If you liked this article, see Shawn's courses on Pluralsight.
Top comments (0)