Angular Dynamic Components
Or how to escape the never ending *ngIf build up in your Angular4+ templates. Demo included!
Every web app I have worked on had the scenario where you have a data set and you need to present each item from that set in a slightly different way.
Usually there is a for loop which goes through the data set and renders that data inside the HTML template. And what tends to happen is that you have a huge template controlled by a lot of IF statements to be able to present each data item in a different way.
This gets very ugly very fast. And before you know it you have something like this — and it keeps growing. You’re adding template variations, classes where you need them, new markup etc.
<div *ngFor="let item of items">
<div *ngIf="item.type === 'type1'">
<h1>{{item.title}}</h1>
<p>{{item.text}}</p>
<img src="{{item.img}}">
</div>
<div *ngIf="item.type === 'type2'">
<p style="text-align: center">{{item.text}}</p>
<p class="caption">{{item.subtext}}</p>
</div>
<div *ngIf="item.type === 'type3'">
<h2>{{item.title}}</h2>
<p style="text-align: right">{{item.text}}</p>
<p class="caption">{{item.subtext}}</p>
</div>
<div *ngIf="item.type === 'type4'">
<img src="{{item.src}}">
<h2>{{item.title}}</h2>
<p style="text-align: right">{{item.text}}</p>
<p class="caption">{{item.subtext}}</p>
</div>
<div *ngIf="item.type === 'type5'">
<div class="float-left">
<img src="{{item.src}}">
<h2>{{item.title}}</h2>
<p style="text-align: right">{{item.text}}</p>
<p class="caption">{{item.subtext}}</p>
</div>
<div class="float-right">
<img src="{{item.icon}}">
</div>
</div>
</div>
Angular is providing us with a great solution to this problem called Dynamic Component Loader which uses the ComponentFactoryResolver.
The example — grid view with dynamic components
To showcase this I have created a small Angular app via the Angular CLI. The idea is to have a grid view where every item in the grid would be it’s own component.
When thinking about the real world scenario I wanted to achieve the following:
- avoid *ngIf inside the template
- have small, reusable templates (in this case components) which would be responsible for presenting the data items in the grid
- be able to easily add new templates
- the templates need to render dynamic data
For the example I’m using Bootstrap 4.0.0-beta to style it. It’s not that relevant to the case, but it makes the example look sweet.
Let’s start with the app.component and the data set
In our app.component.ts we define the data set. Each item in the data set has some pieces of information we’ll want to show inside the corresponding grid item. And it holds a key cardType which will be used to define which template to match with the given item.
data = [
{
title: 'Worlds best boss?',
text: 'Michael Gary Scott is the worlds best boss. According to himself.',
img: 'https://s3-eu-west-1.amazonaws.com/calyx-test-bucket/michael.jpg',
cardType: 'cardStyle1'
},
{
text: 'Always the padawan, never the jedi.',
subtext: 'Dwight Schrute',
cardType: 'cardStyle2'
},
{
title: 'Just as hot as Jan, but in a different way',
text: 'An entry from a personal diary from one Michael G. Scott',
img: 'https://s3-eu-west-1.amazonaws.com/calyx-test-bucket/ryan.gif',
cardType: 'cardStyle3'
},
{
text: 'You miss 100% of shots you don\'t take. --Wayne Gretzky',
subtext: 'Michael Scott',
cardType: 'cardStyle4'
},
{
title: 'YOLO',
text: 'False, you live everyday. You only die once. YODO.',
cardType: 'cardStyle5'
},
{
img: 'https://s3-eu-west-1.amazonaws.com/calyx-test-bucket/creed.jpg',
cardType: 'cardStyle6'
}
];
The app.component.html is pretty straightforward. The only important line there is:
<app-grid [items]="data"></app-grid>
It calls the app-grid component and passes the data to the grid.
Grid component
<div class="card-columns">
<app-grid-item *ngFor="let item of items" [data]="item"></app-grid-item>
</div>
What we’re saying here is that we want to present an app-grid-item component for each item inside the data set. And we pass the item to the app-grid-item component.
Grid item component
The template of the grid-item component is dead simple. Just a ng-container with an element reference.
<ng-container #container></ng-container>
This is because we’ll use the ComponentFactoryResolver to dynamically insert our components and render their HTML.
The real magic happens in the GridItemComponent class.
@Component({
selector: 'app-grid-item',
templateUrl: './grid-item.component.html'
})
export class GridItemComponent implements OnInit {
@Input() data: any;
@ViewChild('container', {read: ViewContainerRef}) private container: ViewContainerRef;
readonly templateMapper = {
cardStyle1: CardStyle1Component,
cardStyle2: CardStyle2Component,
cardStyle3: CardStyle3Component,
cardStyle4: CardStyle4Component,
cardStyle5: CardStyle5Component,
cardStyle6: CardStyle6Component
};
constructor(private componentFactoryResolver: ComponentFactoryResolver) {}
ngOnInit() {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponentForCardType(this.data.cardType));
const viewContainerRef = this.container;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(componentFactory);
(<CardTemplateBaseComponent>componentRef.instance).data = this.data;
}
private getComponentForCardType(cardType) {
return this.templateMapper[cardType];
}
}
There are few things going on here.
First we have our templateMapper. Which maps the data set item to the appropriate Component.
Then in the ngOnInit we’re using the ComponentFactoryResolver to create the component factory, and we’re using the ViewContainerRef to our ng-container and .createComponent method to instantiate the component from our component factory.
The last step we’re doing is passing the data to the dynamically created component.
You’ll notice that we’re typesetting the component instance to CardTemplateBaseComponent. This is our base component which every other card template component will exted. It has an empty template as it is not intended to be used directly.
@Component({
selector: 'app-card-template-base',
template: '',
})
export class CardTemplateBaseComponent {
@Input() data: any;
}
Card templates
Each of the components which is to be used inside the grid should extend the CardTemplateBaseComponent class. That way we know each one of them has the data input property.
The components each define their own HTML template and present the data in their unique ways.
In our case we have six Card template components. Here is how one of them looks like:
// card-style-1.component.ts@Component({
selector: 'app-card-style-1',
templateUrl: './card-style-1.component.html',
styleUrls: ['./card-style-1.component.css']
})
export class CardStyle1Component extends CardTemplateBaseComponent {}// card-style-1.component.html<div class="card">
<img class="card-img-top" [attr.src]="data.img">
<div class="card-body">
<h4 class="card-title">{{data.title}}</h4>
<p class="card-text">{{data.text}}</p>
</div>
</div>
Important! If you want to have Components dynamically loaded you need to provide them as entryComponents in the Module.
entryComponents: [CardStyle1Component, CardStyle2Component, CardStyle3Component, CardStyle4Component, CardStyle5Component, CardStyle6Component]
If you don’t do that, the app will not be able to run. But if you’re using Angular CLI to build it is smart enough to warn you of that issue.
Clone the demo app and play around. I for one will surely use this to build cleaner apps, with more reusable templates instead of getting stuck with a ton of *ngIfs and copy pasted code.
If you have some questions or issues in your Angular app feel free to reach me on Twitter — Marko Francekovic.