Skip to main content

Angular Dynamic Components

Introduction

Dynamic components are a powerful feature in Angular that allows you to load components programmatically at runtime rather than defining them statically in your templates. This capability is particularly useful when you need to create flexible and adaptable user interfaces where components need to be added, removed, or swapped based on user interactions or application state.

In traditional Angular component usage, you declare components in your templates using their selectors. With dynamic components, you can instead create components on-the-fly, insert them into specific locations in your application, and even pass data to them dynamically.

This guide will walk you through the process of working with dynamic components in Angular, starting from the basics and gradually exploring more advanced techniques.

Prerequisites

Before diving into dynamic components, you should be familiar with:

  • Basic Angular concepts
  • Component architecture
  • Angular modules
  • Component communication

Understanding Dynamic Components

Dynamic components differ from regular components in how they are instantiated and rendered:

Static ComponentsDynamic Components
Declared in templates using selectorsCreated programmatically at runtime
Present in the DOM when the parent component loadsAdded to or removed from the DOM as needed
Data binding defined in templatesData passed programmatically

Core Concepts for Dynamic Component Loading

To work with dynamic components in Angular, you'll need to understand these key concepts:

  1. ComponentFactoryResolver: Service used to create component factories
  2. ViewContainerRef: Container where dynamic components can be inserted
  3. ComponentRef: Reference to a created component instance

Basic Implementation Steps

Let's walk through the process of implementing dynamic components in your Angular application:

Step 1: Create a Component to Load Dynamically

First, let's create a simple component that we'll load dynamically:

typescript
// alert-box.component.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'app-alert-box',
template: `
<div class="alert" [ngClass]="{'alert-success': type === 'success',
'alert-danger': type === 'error',
'alert-warning': type === 'warning'}">
<h4>{{title}}</h4>
<p>{{message}}</p>
<button (click)="closeAlert()">Close</button>
</div>
`,
styles: [`
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-success {
color: #3c763d;
background-color: #dff0d8;
border-color: #d6e9c6;
}
.alert-danger {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
.alert-warning {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
}
`]
})
export class AlertBoxComponent {
@Input() type: 'success' | 'error' | 'warning' = 'success';
@Input() title: string = '';
@Input() message: string = '';

closeAlert() {
// We'll implement this later
console.log('Alert closed');
}
}

Step 2: Create a Dynamic Component Loader Service

Next, let's create a service to handle loading our dynamic components:

typescript
// dynamic-component.service.ts
import { ComponentFactoryResolver, Injectable, ViewContainerRef } from '@angular/core';
import { AlertBoxComponent } from './alert-box.component';

@Injectable({
providedIn: 'root'
})
export class DynamicComponentService {

constructor(private componentFactoryResolver: ComponentFactoryResolver) { }

loadComponent(viewContainerRef: ViewContainerRef, type: 'success' | 'error' | 'warning',
title: string, message: string) {

// Clear the container
viewContainerRef.clear();

// Create a factory for the AlertBoxComponent
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(AlertBoxComponent);

// Create the component using the factory and the container
const componentRef = viewContainerRef.createComponent(componentFactory);

// Set input properties on the component instance
componentRef.instance.type = type;
componentRef.instance.title = title;
componentRef.instance.message = message;

// We can also subscribe to component events
componentRef.instance.closeAlert = () => {
// Remove the component from the DOM
componentRef.destroy();
};

return componentRef;
}
}

Step 3: Set Up Host Component with ViewChild

Now let's create a host component that will contain our dynamic component:

typescript
// host.component.ts
import { Component, ViewChild, ViewContainerRef } from '@angular/core';
import { DynamicComponentService } from './dynamic-component.service';

@Component({
selector: 'app-host',
template: `
<div class="container">
<h2>Dynamic Component Example</h2>

<div class="buttons">
<button (click)="createSuccessAlert()">Show Success</button>
<button (click)="createErrorAlert()">Show Error</button>
<button (click)="createWarningAlert()">Show Warning</button>
</div>

<!-- This is where our dynamic component will be inserted -->
<ng-container #alertContainer></ng-container>
</div>
`,
styles: [`
.container {
padding: 20px;
}
.buttons {
margin-bottom: 20px;
}
button {
margin-right: 10px;
padding: 8px 16px;
}
`]
})
export class HostComponent {
@ViewChild('alertContainer', { read: ViewContainerRef })
alertContainer!: ViewContainerRef;

constructor(private dynamicComponentService: DynamicComponentService) { }

createSuccessAlert() {
this.dynamicComponentService.loadComponent(
this.alertContainer,
'success',
'Success!',
'The operation was completed successfully.'
);
}

createErrorAlert() {
this.dynamicComponentService.loadComponent(
this.alertContainer,
'error',
'Error!',
'Something went wrong. Please try again.'
);
}

createWarningAlert() {
this.dynamicComponentService.loadComponent(
this.alertContainer,
'warning',
'Warning!',
'Please review your information before proceeding.'
);
}
}

Step 4: Register Components in the Module

Make sure to register the components in your module:

typescript
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { AlertBoxComponent } from './alert-box.component';
import { HostComponent } from './host.component';

@NgModule({
declarations: [
AppComponent,
AlertBoxComponent,
HostComponent
],
imports: [
BrowserModule
],
// Note: entryComponents is not required in Angular 9+ as it's deprecated
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Advanced Dynamic Component Techniques

Now that we've covered the basics, let's explore some more advanced techniques:

Using ComponentFactoryResolver with Angular 13+

With Angular 13 and above, the approach has been simplified:

typescript
// modern-dynamic-component.service.ts
import { Injectable, ViewContainerRef, createComponent, Type } from '@angular/core';
import { AlertBoxComponent } from './alert-box.component';

@Injectable({
providedIn: 'root'
})
export class ModernDynamicComponentService {

createComponent<T>(viewContainerRef: ViewContainerRef, componentType: Type<T>,
inputs: Partial<T> = {}) {
// Clear container
viewContainerRef.clear();

// Create component using the new API
const componentRef = createComponent(componentType, {
environmentInjector: viewContainerRef.injector
});

// Set inputs
Object.assign(componentRef.instance, inputs);

// Insert into DOM
viewContainerRef.insert(componentRef.hostView);

return componentRef;
}

createAlert(viewContainerRef: ViewContainerRef, type: 'success' | 'error' | 'warning',
title: string, message: string) {

return this.createComponent(viewContainerRef, AlertBoxComponent, {
type,
title,
message
});
}
}

Dynamic Content Projection

You can also dynamically project content into your components:

typescript
// content-projection.service.ts
import { Injectable, ViewContainerRef, TemplateRef, Type } from '@angular/core';

@Injectable({
providedIn: 'root'
})
export class ContentProjectionService {

createComponentWithContent<T>(
viewContainerRef: ViewContainerRef,
componentType: Type<T>,
contentTemplate: TemplateRef<any>,
contentContext: any = {},
inputs: Partial<T> = {}
) {
viewContainerRef.clear();

// Create an embedded view from the template
const embeddedView = viewContainerRef.createEmbeddedView(contentTemplate, contentContext);

// Temporarily detach it
embeddedView.detach();

// Create the component
const componentRef = createComponent(componentType, {
environmentInjector: viewContainerRef.injector,
projectableNodes: [embeddedView.rootNodes]
});

// Set inputs
Object.assign(componentRef.instance, inputs);

// Insert into DOM
viewContainerRef.insert(componentRef.hostView);

return componentRef;
}
}

Real-world Example: Dynamic Form Builder

Let's create a practical example of a dynamic form builder that loads different form components based on a configuration:

typescript
// form-field.interface.ts
export interface FormField {
type: string;
name: string;
label: string;
options?: string[];
validators?: any[];
defaultValue?: any;
}
typescript
// text-input.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
selector: 'app-text-input',
template: `
<div class="form-group">
<label [for]="name">{{label}}</label>
<input
[id]="name"
type="text"
class="form-control"
[formControl]="control"
(blur)="onTouched.emit()"
>
<div *ngIf="control.invalid && control.touched" class="error-message">
<div *ngIf="control.errors?.required">This field is required</div>
<div *ngIf="control.errors?.email">Please enter a valid email</div>
</div>
</div>
`
})
export class TextInputComponent {
@Input() name: string = '';
@Input() label: string = '';
@Input() control: FormControl = new FormControl();
@Output() onTouched = new EventEmitter<void>();
}
typescript
// dropdown-input.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
selector: 'app-dropdown-input',
template: `
<div class="form-group">
<label [for]="name">{{label}}</label>
<select
[id]="name"
class="form-control"
[formControl]="control"
(blur)="onTouched.emit()"
>
<option value="">Select an option</option>
<option *ngFor="let option of options" [value]="option">
{{option}}
</option>
</select>
<div *ngIf="control.invalid && control.touched" class="error-message">
<div *ngIf="control.errors?.required">This field is required</div>
</div>
</div>
`
})
export class DropdownInputComponent {
@Input() name: string = '';
@Input() label: string = '';
@Input() options: string[] = [];
@Input() control: FormControl = new FormControl();
@Output() onTouched = new EventEmitter<void>();
}
typescript
// dynamic-form.component.ts
import { Component, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { FormField } from './form-field.interface';
import { TextInputComponent } from './text-input.component';
import { DropdownInputComponent } from './dropdown-input.component';

@Component({
selector: 'app-dynamic-form',
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<ng-container #formFields></ng-container>

<div class="form-actions">
<button type="submit" [disabled]="form.invalid">Submit</button>
</div>
</form>
`
})
export class DynamicFormComponent implements OnInit {
@Input() formConfig: FormField[] = [];
@ViewChild('formFields', { read: ViewContainerRef, static: true })
formFieldsContainer!: ViewContainerRef;

form: FormGroup;

constructor(private fb: FormBuilder) {
this.form = this.fb.group({});
}

ngOnInit() {
this.createFormControls();
this.renderFormFields();
}

createFormControls() {
this.formConfig.forEach(field => {
const validators = field.validators || [];
this.form.addControl(
field.name,
this.fb.control(field.defaultValue || '', validators)
);
});
}

renderFormFields() {
this.formFieldsContainer.clear();

this.formConfig.forEach(field => {
const componentType = this.getComponentType(field.type);

if (componentType) {
const componentRef = createComponent(componentType, {
environmentInjector: this.formFieldsContainer.injector
});

// Set input properties
componentRef.instance.name = field.name;
componentRef.instance.label = field.label;
componentRef.instance.control = this.form.get(field.name);

if (field.type === 'dropdown' && field.options) {
componentRef.instance.options = field.options;
}

// Handle events
componentRef.instance.onTouched.subscribe(() => {
this.form.get(field.name)?.markAsTouched();
});

// Insert into DOM
this.formFieldsContainer.insert(componentRef.hostView);
}
});
}

getComponentType(fieldType: string) {
switch (fieldType) {
case 'text':
case 'email':
case 'password':
return TextInputComponent;
case 'dropdown':
return DropdownInputComponent;
default:
return null;
}
}

onSubmit() {
if (this.form.valid) {
console.log('Form submitted with values:', this.form.value);
} else {
this.markFormGroupTouched(this.form);
}
}

markFormGroupTouched(formGroup: FormGroup) {
Object.values(formGroup.controls).forEach(control => {
control.markAsTouched();
});
}
}

Usage example:

typescript
// app.component.ts
import { Component } from '@angular/core';
import { Validators } from '@angular/forms';
import { FormField } from './form-field.interface';

@Component({
selector: 'app-root',
template: `
<div class="container">
<h1>Dynamic Form Example</h1>
<app-dynamic-form [formConfig]="formConfig"></app-dynamic-form>
</div>
`
})
export class AppComponent {
formConfig: FormField[] = [
{
type: 'text',
name: 'firstName',
label: 'First Name',
validators: [Validators.required]
},
{
type: 'text',
name: 'lastName',
label: 'Last Name',
validators: [Validators.required]
},
{
type: 'email',
name: 'email',
label: 'Email Address',
validators: [Validators.required, Validators.email]
},
{
type: 'dropdown',
name: 'country',
label: 'Country',
options: ['United States', 'Canada', 'United Kingdom', 'Australia'],
validators: [Validators.required]
}
];
}

Best Practices for Dynamic Components

When working with dynamic components, keep these best practices in mind:

  1. Memory Management: Always destroy component references when they're no longer needed to prevent memory leaks.

  2. Error Handling: Implement proper error handling when dynamically creating components.

  3. Performance: Be mindful of performance when creating and destroying many dynamic components.

  4. Component Communication: Establish clear patterns for communication between dynamic components and their hosts.

  5. Type Safety: Use TypeScript generics to ensure type safety when working with dynamic components.

  6. Testing: Create specific tests for your dynamic component loading logic.

Common Pitfalls and Solutions

PitfallSolution
Memory leaksAlways call componentRef.destroy() when removing components
Type errorsUse proper TypeScript typing for component inputs
Component not foundEnsure component is declared in the right module
Styling issuesUse Angular's view encapsulation properly
Change detection issuesManually trigger change detection if needed using ChangeDetectorRef

Summary

Dynamic components in Angular provide a powerful way to create flexible, adaptable user interfaces. They allow you to:

  • Load components programmatically at runtime
  • Create modular, reusable UI elements
  • Dynamically change your application's appearance and behavior
  • Implement advanced patterns like modals, tooltips, and context menus

In this guide, we covered:

  • Basic dynamic component loading using ComponentFactoryResolver
  • Modern approach using createComponent
  • Advanced techniques like content projection
  • A real-world example with dynamic forms

By mastering dynamic components, you'll have another powerful tool in your Angular development toolkit that allows you to create more flexible and dynamic user interfaces.

Additional Resources

Exercises

  1. Create a dynamic tooltip component that can be attached to any element.
  2. Build a tab system where tabs can be added and removed dynamically.
  3. Implement a dynamic modal service that can display different types of content.
  4. Create a notification system that shows different styles of notifications using dynamic components.
  5. Extend the dynamic form example to support more input types and custom validation.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)