Introduction
This article will introduce you how to create dynamic angular components based on the backend api responses. It will covers the basic dynamic component templates creation.
This article is quite in-depth, based on the dynamic component loader, please check out Angular Dynamic component loader documentation.
Component templates are not always keep static in design. An application might need to load new dynamic templates without tightly coupling the fixed reference in the component templates.
Prerequisites
If you would like to follow along with this tutorial.
- Consider installing @angular/cli
- Use @angular/cli to create a new project
Note: This tutorial was verified with latest @angular/core v14.0.0 and @angular/cli v14.0.0
Host directive
Before the kick-start, you have to define an anchor point that tells where to render all dynamic components in the DOM.
Let’s say you have a AppComponentHostDirective. In the @Directive decorator, selector name appDynamicComponentHost; that we will use to apply directive to the element.
import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appDynamicComponentHost]'
})
export class AppComponentHostDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}
In above AppComponentHostDirective directive which injects ViewContainerRef into constructor to gain access for the view container of the element that will host the dynamically added component.
Dynamic components container
Most of the dynamic components loading functionality is in app-ui-builder.component.ts.
<div class="app-ui-builder-content">
<ng-template appDynamicComponentHost></ng-template>
</div>
The <ng-template> element is where you apply the directive you just made in app-component-host.directive.ts. Now Angular knows the container where to dynamically load components.
App component
<div class="container mt-4">
<ul>
<li><a routerLink="/project-overview" routerLinkActive="active">Project Overview</a></li>
<li><a routerLink="/project-detail">Project Detail</a></li>
</ul>
<router-outlet></router-outlet>
</div>
App routing module
Please take a closer look at app-routing.module.ts module.
The routing module defines two routes called /project-overview, /project-detail. Here each route using resolver guard to fetch data before route activation i.e AppResolver.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AppDynamicComponent } from './app-dynamic-component/app-dynamic.component';
import { AppResolver } from './shared/app.resolver';
const routes: Routes = [
{
path: 'project-overview',
component: AppDynamicComponent,
resolve: {
page: AppResolver
}
},
{
path: 'project-detail',
component: AppDynamicComponent,
resolve: {
page: AppResolver
}
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Note: For each route using the same AppDynamicComponent, inside the template passing components list from AppResolver as @input() to app-ui-builder.component.ts
<app-ui-builder [components]="components"></app-ui-builder>
App resolving page
The AppResolver, a angular service that can be used with router to resolve data during navigation. The Resolve interface defines a resolve() method that is invoked when navigation start.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
Resolve,
RouterStateSnapshot,
ActivatedRouteSnapshot
} from '@angular/router';
import { map, Observable } from 'rxjs';
import { PageBuilderService } from './page-builder.service';
import { Routes } from './routes';
@Injectable({
providedIn: 'root'
})
export class AppResolver implements Resolve<any> {
constructor(
private http: HttpClient,
private pageBuilderService: PageBuilderService
) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> {
const pageName = route.url[0].path;
const option = {
url: Routes.fetchLocalResponse(pageName),
// url: Routes.fetchServerResponse(pageName)
};
return this.http.get(option.url).pipe(
map(data => this.onSuccess(data))
);
}
private onSuccess(data: any) {
return this.pageBuilderService.buildPage(data.pageElements);
}
}
How AppResolver loads component data
1. The resolve() get invoked automatically when navigation start.
2. Get current activated route url from ActivatedRouteSnapshot i.e line 24.
3. Make http call for fetching data using HttpClient i.e line 31.
4. Build page based on data received from the api using PageBuilderService i.e line 37.
Page builder service
All components implements a common AddComponent to define the API for passing data to the components.
import { Type } from '@angular/core';
export class AddComponent {
constructor(
public component: Type<any>,
public config: any,
public data: string
) {}
}
import { Injectable } from '@angular/core';
import { AddComponent } from '../add.component';
import { CheckboxComponent } from '../app-dynamic-component/checkbox/checkbox.component';
import { DropdownComponent } from '../app-dynamic-component/dropdown/dropdown.component';
import { LabelComponent } from '../app-dynamic-component/label/label.component';
import { RadioComponent } from '../app-dynamic-component/radio/radio.component';
import { TextareaComponent } from '../app-dynamic-component/textarea/textarea.component';
import { TextboxComponent } from '../app-dynamic-component/textbox/textbox.component';
@Injectable({
providedIn: 'root'
})
export class PageBuilderService {
constructor() { }
buildPage(pageElements: any): any {
let pageDetails: Array<any> = [];
pageElements.forEach((page: any) => {
pageDetails.push(this.getPageElement(page));
})
return pageDetails;
}
private getPageElement(page: any): AddComponent | void {
if (page.componentType === 'label') {
return new AddComponent(LabelComponent, page, page.data);
} else if (page.componentType === 'textbox') {
return new AddComponent(TextboxComponent, page, page.value);
} else if (page.componentType === 'dropdown') {
return new AddComponent(DropdownComponent, page, page.value);
} else if (page.componentType === 'checkbox') {
return new AddComponent(CheckboxComponent, page, page.value);
} else if (page.componentType === 'radio') {
return new AddComponent(RadioComponent, page, page.value);
} else if (page.componentType === 'textarea') {
return new AddComponent(TextareaComponent, page, page.value);
}
}
}
Note: Inside buildPage() method, looping through component list, which in turns calling getPageElement() method which returns AddComponent instance by setting respective component i.e via parameters based on componentType.
Route listeners
Inside AppDynamicComponent, using ActivatedRoute service to get information from a route inside ngOnInit() method and track page parameter (i.e as key) at resolved data from the AppResolver, and assigned it to the components array.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { AddComponent } from '../add.component';
@Component({
selector: 'app-dynamic-component',
templateUrl: './app-dynamic.component.html',
styleUrls: []
})
export class AppDynamicComponent implements OnInit {
components: AddComponent[] = [];
constructor(
private route: ActivatedRoute
) {}
ngOnInit(): void {
this.route.data.subscribe(data => {
this.components = data['page'];
});
}
}
Note: Here binding components array as input to app-dynamic.component.html
<app-ui-builder [components]="components"></app-ui-builder>
Page builder component
Most of the dynamic component loading implementation is in app-ui-builder.component.ts.
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core';
import { AddComponent } from '../add.component';
import { AppComponentHostDirective } from '../app-component-host.directive';
@Component({
selector: 'app-ui-builder',
templateUrl: './app-ui-builder.component.html',
styleUrls: []
})
export class AppUIBuilderComponent implements OnInit, OnDestroy, OnChanges {
@Input() components!: AddComponent[];
@ViewChild(AppComponentHostDirective, { static: true, read: AppComponentHostDirective })
componentHost!: AppComponentHostDirective;
constructor() {}
ngOnChanges(changes: SimpleChanges): void {
this.buildComponents();
}
ngOnInit(): void {
this.buildComponents();
}
private buildComponents() {
this.componentHost.viewContainerRef.clear();
for (const component of this.components) {
if (!!component) {
const viewContainerRef = this.componentHost.viewContainerRef;
const componentRef = viewContainerRef.createComponent<AddComponent>(component.component);
componentRef.instance.config = component.config;
componentRef.instance.data = component.config.data;
}
}
}
ngOnDestroy(): void {
this.components = [];
}
}
How Page builder component works
1. AppUIBuilderComponent takes an array of AddComponent as a input, which ultimately comes from PageBuilderService i.e line 11.
2. The buildComponents() doing a heavy job here, like clearing view container, setting input configuration and adding components. It’s calling inside ngOnInit, ngOnChanges life-cycle hooks methods.
3. componentHost refers to the directive which created earlier to tell Angular where to load dynamic components, recall AppComponentHostDirective injects ViewContainerRef into its constructor. Using this approach, directive accesses the element to host the dynamic component.
4. To add component to the template, call createComponent() on directive viewContainerRef which resolve instance of loaded component, then we can make configuration for component.
Page responses format
In this article, used sample json responses from the assets/pages directory. Here each object inside pageElements is the configuration for the component componentType property defines component type like label, text-box, text-area, dropdown, checkbox, radio.
Note: We can modify the configuration based on use cases
{
"pageElements": [
{
"id": "projectDetailTitle",
"name": "projectDetailTitle",
"componentType": "label",
"visible": true,
"data": "Dynamic component loader",
"heading": true
},
{
"id": "projectDetailDescription",
"name": "projectDetailDescription",
"componentType": "label",
"style": {"color": "red", "font-style": "italic" },
"visible": true,
"data": "Component templates are not always fixed. An application might need to load new components at runtime. This cookbook shows you how to add components dynamically.",
"heading": false
},
{
"id": "projectCloudTechnology",
"name": "projectCloudTechnology",
"componentType": "radio",
"label": "Project cloud technology",
"styleClass": "mt-1",
"labelClass": "ms-2",
"value": "2",
"radioOptionList": [
{ "key": "1", "value": "AWS" },
{ "key": "2", "value": "MS Azure" },
{ "key": "3", "value": "GCP" }
],
"visible": true,
"disabled": false
},
{
"id": "projectFeedback",
"name": "projectFeedback",
"componentType": "textarea",
"label": "Project feedback",
"placeholder": "Enter project feedback",
"styleClass": "col-md-6 mt-3",
"rows": 5,
"visible": true,
"data": "This project is developed in Angular v14"
}
]
}
{
"pageElements": [
{
"id": "projectTitle",
"name": "projectTitle",
"componentType": "label",
"visible": true,
"data": "Dynamic component loader",
"heading": true
},
{
"id": "projectDescription",
"name": "projectDescription",
"componentType": "label",
"visible": true,
"data": "Component templates are not always fixed. An application might need to load new components at runtime. This cookbook shows you how to add components dynamically.",
"heading": false
},
{
"id": "projectName",
"name": "projectName",
"componentType": "textbox",
"label": "Project Name",
"placeholder": "Enter project name",
"styleClass": "col-md-6 mt-3",
"visible": true,
"disabled": false,
"value": "Dynamic component angular app"
},
{
"id": "projectStatus",
"name": "projectStatus",
"componentType": "dropdown",
"label": "Project Status",
"styleClass": "col-md-6 mt-3",
"visible": true,
"disabled": false,
"value": "2",
"dropdownList": [
{ "key": "1", "value": "Not Started" },
{ "key": "2", "value": "Kick off" },
{ "key": "3", "value": "In Progress" },
{ "key": "4", "value": "On Hold" },
{ "key": "5", "value": "Completed" }
]
},
{
"id": "projectApproved",
"name": "projectApproved",
"componentType": "checkbox",
"label": "Is project approved from the business?",
"styleClass": "mt-3",
"labelClass": "ms-2",
"visible": true,
"disabled": true,
"value": true
}
]
}
Configuration for API Url
Inside app.resolver.ts file, uncomment the fetchServerResponse when to use actual backend response configuration and commented out fetchLocalResponse . Here pageName defines api url path name like project-overview, project-detail etc.
const option = {
url: Routes.fetchLocalResponse(pageName),
// url: Routes.fetchServerResponse(pageName)
};
export const environment = {
production: false,
apiUrl: '/assets/pages',
};
Conclusion
In this article, used Angular Dynamic component loader functionality to create basic dynamic component creation based on the backend responses. All data and information provided in this article are for informational purposes only.
Code can be found on StackBlitz.