Angular Dynamic Components Explained — Runtime Component Creation
In this tutorial, you will learn about Angular Dynamic Components Explained. We cover key concepts, practical examples, and best practices to help you master this topic.
Angular dynamic components are components created and rendered at runtime rather than being declared in a template, enabling flexible UIs that adapt based on data or user actions.
What You'll Learn
- How dynamic components differ from declarative components
- How to use ViewContainerRef and ComponentRef
- How to create components dynamically
- How to pass inputs and handle outputs dynamically
- How to destroy dynamic components properly
Why It Matters
Dynamic components let you build systems like modal dialogs, tab panels, dashboards with draggable widgets, and notification toasts that appear and disappear at runtime. They are essential for component-based plugin architectures.
Real-World Use
Durga Antivirus Pro's alert system creates dynamic notification components when threats are detected. Each alert component is instantiated, positioned on screen, and destroyed when dismissed. This avoids keeping hundreds of hidden components in memory.
flowchart LR
A[Trigger Event] --> B[Component Factory]
B --> C[ViewContainerRef]
C --> D[Created Component]
D --> E[DOM Insertion]
E --> F[Dismiss]
F --> G[Destroy Component]
style A fill:#f97316,color:#fff
Creating Components Dynamically
Use ViewContainerRef to create components at runtime:
import { Component, ViewChild, ViewContainerRef, ComponentRef, inject } from "@angular/core";
@Component({
selector: "app-toast-container",
standalone: true,
template: `<div #container></div>`
})
export class ToastContainerComponent {
@ViewChild("container", { read: ViewContainerRef }) container!: ViewContainerRef;
async showToast(message: string, type: "success" | "error" | "info") {
const { ToastComponent } = await import("./toast.component");
const componentRef: ComponentRef<ToastComponent> =
this.container.createComponent(ToastComponent);
componentRef.instance.message = message;
componentRef.instance.type = type;
componentRef.instance.dismiss.subscribe(() => {
componentRef.destroy();
});
return componentRef;
}
}
Expected output: A toast notification appears on screen when showToast is called. Clicking dismiss removes it.
ViewContainerRef.createComponent() creates a new component instance and appends its view to the container. The returned ComponentRef provides access to the component instance, inputs, and outputs.
Dynamic Component with Inputs and Outputs
A reusable dynamic component loader:
import { Component, Input, Output, EventEmitter } from "@angular/core";
@Component({
selector: "app-alert",
standalone: true,
template: `
<div class="alert" [style.border-color]="borderColor">
<h4>{{ title }}</h4>
<p>{{ description }}</p>
<button (click)="close.emit()">Dismiss</button>
</div>
`,
styles: [`
.alert { border: 2px solid; padding: 16px; border-radius: 8px; margin: 8px 0; }
`]
})
export class AlertComponent {
@Input() title = "";
@Input() description = "";
@Input() type: "info" | "warning" | "error" = "info";
@Output() close = new EventEmitter<void>();
get borderColor() {
return this.type === "error" ? "#ef4444"
: this.type === "warning" ? "#f97316"
: "#3b82f6";
}
}
Dynamic loader component:
@Component({
selector: "app-alert-manager",
standalone: true,
imports: [CommonModule],
template: `
<button (click)="addAlert()">Show Info</button>
<button (click)="addWarning()">Show Warning</button>
<div #alertContainer></div>
`
})
export class AlertManagerComponent {
@ViewChild("alertContainer", { read: ViewContainerRef })
alertContainer!: ViewContainerRef;
private alertCount = 0;
async addAlert() {
this.showAlert("Info Alert", "This is an informational message.", "info");
}
async addWarning() {
this.showAlert("Warning Alert", "This action cannot be undone.", "warning");
}
private async showAlert(title: string, description: string, type: "info" | "warning" | "error") {
const { AlertComponent } = await import("./alert.component");
const ref = this.alertContainer.createComponent(AlertComponent);
ref.setInput("title", `${title} #${++this.alertCount}`);
ref.setInput("description", description);
ref.setInput("type", type);
ref.instance.close.subscribe(() => ref.destroy());
}
}
Expected output: Clicking the buttons creates alert components dynamically. Each alert has a dismiss button that destroys the component.
ref.setInput() sets component inputs and triggers change detection. The close subscription destroys the component, removing its view from the DOM and cleaning up resources.
Component Outlet with ngComponentOutlet
For declarative dynamic components in templates:
import { Component, Injectable, Type } from "@angular/core";
import { CommonModule } from "@angular/common";
@Component({
selector: "app-welcome-widget",
standalone: true,
template: `<h3>Welcome back!</h3>`
})
export class WelcomeWidgetComponent {}
@Component({
selector: "app-stats-widget",
standalone: true,
template: `<div>Stats: 1,234 threats blocked today</div>`
})
export class StatsWidgetComponent {}
@Injectable({ providedIn: "root" })
export class WidgetService {
getWidget(type: string): Type<any> {
switch (type) {
case "welcome": return WelcomeWidgetComponent;
case "stats": return StatsWidgetComponent;
default: return WelcomeWidgetComponent;
}
}
}
@Component({
selector: "app-dashboard",
standalone: true,
imports: [CommonModule],
template: `
<div *ngFor="let widget of widgets">
<ng-container *ngComponentOutlet="widgetService.getWidget(widget)" />
</div>
`
})
export class DashboardComponent {
widgets = ["welcome", "stats", "welcome"];
constructor(public widgetService: WidgetService) {}
}
Expected output: A dashboard with welcome and stats widgets rendered dynamically based on the string array.
ngComponentOutlet is a structural directive that creates a component dynamically from a component class. When the component class changes, Angular destroys the old component and creates the new one.
Dynamic Component with Data Injection
Pass data using injection tokens:
import { Component, Inject, InjectionToken, ViewContainerRef } from "@angular/core";
export interface ModalConfig {
title: string;
content: string;
onConfirm: () => void;
}
export const MODAL_CONFIG = new InjectionToken<ModalConfig>("modal.config");
@Component({
selector: "app-dynamic-modal",
standalone: true,
template: `
<div class="modal">
<h2>{{ config.title }}</h2>
<p>{{ config.content }}</p>
<button (click)="config.onConfirm()">Confirm</button>
<button (click)="destroy()">Cancel</button>
</div>
`
})
export class DynamicModalComponent {
constructor(@Inject(MODAL_CONFIG) public config: ModalConfig) {}
destroy() {
// handled by the creator
}
}
// Creator
@Component({
selector: "app-modal-service",
standalone: true,
template: `<div #modalContainer></div>`
})
export class ModalServiceComponent {
@ViewChild("modalContainer", { read: ViewContainerRef })
container!: ViewContainerRef;
showModal(config: ModalConfig) {
import("./dynamic-modal.component").then(m => {
const ref = this.container.createComponent(m.DynamicModalComponent, {
injector: Injector.create({
providers: [{ provide: MODAL_CONFIG, useValue: config }]
})
});
});
}
}
Expected output: A modal with title, content, confirm, and cancel buttons. The confirm action is injected through the Dependency Injection system.
Using an InjectionToken to pass data to dynamic components is cleaner than setting inputs after creation, especially for complex configuration.
Common Mistakes
Memory leaks from not destroying components — Dynamic components persist in memory even if not visible. Always call
componentRef.destroy()when done.Creating components without Lazy Loading — Importing all components eagerly defeats the purpose. Use dynamic
import()to load components lazily.Forgetting ViewContainerRef from ViewChild —
ViewChildwith{ read: ViewContainerRef }is required. Without it,createComponentwill fail.Setting inputs after change detection — If you set inputs without
setInput()and the component uses OnPush, the view may not update.Overusing dynamic components — For most use cases, declarative components with
*ngIfand*ngForare simpler and more performant.
Practice Questions
What is a dynamic component? A component created and rendered at runtime using
ViewContainerRef.createComponent()rather than declared in a template.How do you destroy a dynamic component? Call
componentRef.destroy(). This removes the view and callsngOnDestroyon the component.What is ngComponentOutlet? A structural directive that creates a component dynamically from a component class reference.
How do you pass inputs to a dynamic component? Use
ref.setInput("inputName", value)or setref.instance.inputName = valuedirectly.Why lazy-load dynamic components? To reduce the initial bundle size by only loading components when they are needed.
Challenge
Build a NotificationSystemComponent that manages a stack of toast notifications. Each notification is a dynamic component with title, message, type (success/error/info), and auto-dismiss after 5 seconds. Support a max visible count (stack the rest). Use lazy loading for the notification component.
FAQ
Mini Project
Build a WidgetDashboardComponent where users can add, remove, and rearrange dashboard widgets. Each widget is a dynamic component loaded from a registry. Support three widget types: WeatherWidget, ClockWidget, and StatsWidget. Use ViewContainerRef to create widgets, store their ComponentRefs in an array, and implement drag-to-reorder by destroying and recreating widgets in new positions.
What's Next
Continue with animations and forms:
Angular Animations, Angular Forms Reactive, Angular Content Projection
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro