Skip to content

Dynamic Components ๐Ÿ”„

Dynamic components allow you to load components at runtime based on user actions, data, or application state. This powerful feature enables flexible, data-driven UIs and plugin architectures.

Dynamic components are components that are created and inserted into the DOM at runtime, rather than being declared in templates at compile time. Theyโ€™re perfect for:

  • Modal dialogs - Show different content based on context
  • Tab systems - Load tab content on demand
  • Plugin architectures - Load features dynamically
  • Dashboard widgets - User-configurable layouts
  • Form builders - Dynamic form fields
  • Notification systems - Different notification types
import { Component, ViewContainerRef, inject } from '@angular/core';
import { WelcomeComponent } from './welcome.component';
@Component({
selector: 'app-container',
template: `
<div class="container">
<h2>Dynamic Component Container</h2>
<button (click)="loadComponent()">Load Component</button>
<button (click)="clear()">Clear</button>
</div>
`
})
export class ContainerComponent {
private viewContainer = inject(ViewContainerRef);
loadComponent() {
// Clear existing components
this.viewContainer.clear();
// Create and insert the component
const componentRef = this.viewContainer.createComponent(WelcomeComponent);
// Optionally set inputs
componentRef.setInput('username', 'John Doe');
}
clear() {
this.viewContainer.clear();
}
}
import { Component, input } from '@angular/core';
@Component({
selector: 'app-welcome',
template: `
<div class="welcome">
<h3>Welcome, {{ username() }}!</h3>
<p>This component was loaded dynamically.</p>
</div>
`
})
export class WelcomeComponent {
username = input<string>('Guest');
}

Letโ€™s build a flexible modal system that can display different content dynamically.

Modal Service:

import { Injectable, inject, signal, Type } from '@angular/core';
export interface ModalConfig {
component: Type<any>;
data?: any;
title?: string;
}
@Injectable({ providedIn: 'root' })
export class ModalService {
private modalConfig = signal<ModalConfig | null>(null);
isOpen = signal(false);
config = this.modalConfig.asReadonly();
open(config: ModalConfig) {
this.modalConfig.set(config);
this.isOpen.set(true);
}
close() {
this.isOpen.set(false);
setTimeout(() => this.modalConfig.set(null), 300); // Animation delay
}
}

Modal Container Component:

import { Component, ViewContainerRef, effect, inject } from '@angular/core';
import { ModalService } from './modal.service';
@Component({
selector: 'app-modal-container',
template: `
@if (modalService.isOpen()) {
<div class="modal-overlay" (click)="modalService.close()">
<div class="modal-content" (click)="$event.stopPropagation()">
<div class="modal-header">
<h3>{{ modalService.config()?.title || 'Modal' }}</h3>
<button (click)="modalService.close()">&times;</button>
</div>
<div class="modal-body">
<!-- Dynamic component loads here -->
</div>
</div>
</div>
}
`,
})
export class ModalContainerComponent {
modalService = inject(ModalService);
private viewContainer = inject(ViewContainerRef);
constructor() {
effect(() => {
const config = this.modalService.config();
if (config && this.modalService.isOpen()) {
this.loadComponent(config);
} else {
this.viewContainer.clear();
}
});
}
private loadComponent(config: ModalConfig) {
this.viewContainer.clear();
const componentRef = this.viewContainer.createComponent(config.component);
// Pass data to the component
if (config.data) {
Object.keys(config.data).forEach(key => {
componentRef.setInput(key, config.data[key]);
});
}
}
}

Example Modal Content Components:

// Confirmation Modal
@Component({
selector: 'app-confirm-modal',
template: `
<div class="confirm-content">
<p>{{ message() }}</p>
<div class="actions">
<button (click)="onConfirm()">Confirm</button>
<button (click)="onCancel()">Cancel</button>
</div>
</div>
`
})
export class ConfirmModalComponent {
private modalService = inject(ModalService);
message = input<string>('Are you sure?');
onConfirmCallback = input<() => void>(() => {});
onConfirm() {
this.onConfirmCallback()();
this.modalService.close();
}
onCancel() {
this.modalService.close();
}
}
// User Details Modal
@Component({
selector: 'app-user-details-modal',
template: `
<div class="user-details">
<img [src]="user().avatar" [alt]="user().name">
<h4>{{ user().name }}</h4>
<p>{{ user().email }}</p>
<p>{{ user().bio }}</p>
</div>
`
})
export class UserDetailsModalComponent {
user = input.required<User>();
}

Usage:

@Component({
selector: 'app-dashboard',
imports: [ModalContainerComponent],
template: `
<div class="dashboard">
<button (click)="showConfirmation()">Delete Item</button>
<button (click)="showUserDetails()">View Profile</button>
</div>
<app-modal-container />
`
})
export class DashboardComponent {
private modalService = inject(ModalService);
showConfirmation() {
this.modalService.open({
component: ConfirmModalComponent,
title: 'Confirm Deletion',
data: {
message: 'Are you sure you want to delete this item?',
onConfirmCallback: () => this.deleteItem()
}
});
}
showUserDetails() {
this.modalService.open({
component: UserDetailsModalComponent,
title: 'User Profile',
data: {
user: {
name: 'John Doe',
email: 'john@example.com',
avatar: '/assets/avatar.jpg',
bio: 'Software Developer'
}
}
});
}
deleteItem() {
console.log('Item deleted!');
}
}

Letโ€™s create a tab system that loads tab content dynamically.

Tab Configuration:

import { Type } from '@angular/core';
export interface TabConfig {
id: string;
label: string;
component: Type<any>;
data?: any;
}

Tab Container Component:

import { Component, input, signal, ViewContainerRef, effect, inject } from '@angular/core';
import { TabConfig } from './tab-config';
@Component({
selector: 'app-tabs',
template: `
<div class="tabs">
<div class="tab-headers">
@for (tab of tabs(); track tab.id) {
<button
class="tab-header"
[class.active]="activeTabId() === tab.id"
(click)="selectTab(tab.id)">
{{ tab.label }}
</button>
}
</div>
<div class="tab-content">
<!-- Dynamic tab content loads here -->
</div>
</div>
`,
})
export class TabsComponent {
private viewContainer = inject(ViewContainerRef);
tabs = input.required<TabConfig[]>();
activeTabId = signal<string>('');
constructor() {
// Set first tab as active by default
effect(() => {
const tabsList = this.tabs();
if (tabsList.length > 0 && !this.activeTabId()) {
this.activeTabId.set(tabsList[0].id);
}
});
// Load component when active tab changes
effect(() => {
const activeId = this.activeTabId();
const tab = this.tabs().find(t => t.id === activeId);
if (tab) {
this.loadTabContent(tab);
}
});
}
selectTab(tabId: string) {
this.activeTabId.set(tabId);
}
private loadTabContent(tab: TabConfig) {
this.viewContainer.clear();
const componentRef = this.viewContainer.createComponent(tab.component);
// Pass data to the tab component
if (tab.data) {
Object.keys(tab.data).forEach(key => {
componentRef.setInput(key, tab.data[key]);
});
}
}
}

Example Tab Content Components:

// Profile Tab
@Component({
selector: 'app-profile-tab',
template: `
<div class="profile">
<h3>Profile Information</h3>
<p>Name: {{ user().name }}</p>
<p>Email: {{ user().email }}</p>
</div>
`
})
export class ProfileTabComponent {
user = input.required<User>();
}
// Settings Tab
@Component({
selector: 'app-settings-tab',
template: `
<div class="settings">
<h3>Settings</h3>
<label>
<input type="checkbox" [checked]="notifications()">
Enable Notifications
</label>
</div>
`
})
export class SettingsTabComponent {
notifications = input<boolean>(true);
}
// Activity Tab
@Component({
selector: 'app-activity-tab',
template: `
<div class="activity">
<h3>Recent Activity</h3>
@for (activity of activities(); track activity.id) {
<div class="activity-item">
<span>{{ activity.action }}</span>
<span>{{ activity.timestamp }}</span>
</div>
}
</div>
`
})
export class ActivityTabComponent {
activities = input.required<Activity[]>();
}

Usage:

@Component({
selector: 'app-user-dashboard',
imports: [TabsComponent],
template: `
<app-tabs [tabs]="tabConfigs()" />
`
})
export class UserDashboardComponent {
private user = signal({
name: 'John Doe',
email: 'john@example.com'
});
tabConfigs = signal<TabConfig[]>([
{
id: 'profile',
label: 'Profile',
component: ProfileTabComponent,
data: { user: this.user() }
},
{
id: 'settings',
label: 'Settings',
component: SettingsTabComponent,
data: { notifications: true }
},
{
id: 'activity',
label: 'Activity',
component: ActivityTabComponent,
data: {
activities: [
{ id: 1, action: 'Logged in', timestamp: '2 hours ago' },
{ id: 2, action: 'Updated profile', timestamp: '1 day ago' }
]
}
}
]);
}

Letโ€™s build a dynamic form system that creates form fields based on configuration.

Form Field Configuration:

export type FieldType = 'text' | 'email' | 'number' | 'select' | 'checkbox' | 'textarea';
export interface FieldConfig {
type: FieldType;
name: string;
label: string;
value?: any;
required?: boolean;
options?: { label: string; value: any }[];
placeholder?: string;
}

Field Components:

// Text Input Component
@Component({
selector: 'app-text-field',
imports: [ReactiveFormsModule],
template: `
<div class="form-field">
<label [for]="config().name">
{{ config().label }}
@if (config().required) {
<span class="required">*</span>
}
</label>
<input
[id]="config().name"
[type]="config().type"
[formControl]="control()"
[placeholder]="config().placeholder || ''"
/>
</div>
`
})
export class TextFieldComponent {
config = input.required<FieldConfig>();
control = input.required<FormControl>();
}
// Select Component
@Component({
selector: 'app-select-field',
imports: [ReactiveFormsModule],
template: `
<div class="form-field">
<label [for]="config().name">{{ config().label }}</label>
<select [id]="config().name" [formControl]="control()">
<option value="">Select...</option>
@for (option of config().options; track option.value) {
<option [value]="option.value">{{ option.label }}</option>
}
</select>
</div>
`
})
export class SelectFieldComponent {
config = input.required<FieldConfig>();
control = input.required<FormControl>();
}

Dynamic Form Component:

import { Component, input, signal, ViewContainerRef, effect, inject } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-dynamic-form',
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="form-fields">
<!-- Dynamic fields load here -->
</div>
<button type="submit" [disabled]="form.invalid">
Submit
</button>
</form>
`
})
export class DynamicFormComponent {
private fb = inject(FormBuilder);
private viewContainer = inject(ViewContainerRef);
fields = input.required<FieldConfig[]>();
form!: FormGroup;
constructor() {
effect(() => {
this.buildForm();
this.loadFields();
});
}
private buildForm() {
const group: any = {};
this.fields().forEach(field => {
const validators = field.required ? [Validators.required] : [];
group[field.name] = [field.value || '', validators];
});
this.form = this.fb.group(group);
}
private loadFields() {
this.viewContainer.clear();
this.fields().forEach(fieldConfig => {
const component = this.getComponentForType(fieldConfig.type);
const componentRef = this.viewContainer.createComponent(component);
componentRef.setInput('config', fieldConfig);
componentRef.setInput('control', this.form.get(fieldConfig.name));
});
}
private getComponentForType(type: FieldType) {
const componentMap = {
text: TextFieldComponent,
email: TextFieldComponent,
number: TextFieldComponent,
select: SelectFieldComponent,
checkbox: CheckboxFieldComponent,
textarea: TextareaFieldComponent
};
return componentMap[type];
}
onSubmit() {
if (this.form.valid) {
console.log('Form submitted:', this.form.value);
}
}
}

Usage:

@Component({
selector: 'app-registration',
imports: [DynamicFormComponent],
template: `
<h2>User Registration</h2>
<app-dynamic-form [fields]="formFields()" />
`
})
export class RegistrationComponent {
formFields = signal<FieldConfig[]>([
{
type: 'text',
name: 'firstName',
label: 'First Name',
required: true,
placeholder: 'Enter your first name'
},
{
type: 'text',
name: 'lastName',
label: 'Last Name',
required: true
},
{
type: 'email',
name: 'email',
label: 'Email',
required: true,
placeholder: 'your@email.com'
},
{
type: 'select',
name: 'country',
label: 'Country',
required: true,
options: [
{ label: 'United States', value: 'us' },
{ label: 'United Kingdom', value: 'uk' },
{ label: 'Canada', value: 'ca' }
]
}
]);
}

Letโ€™s create a customizable dashboard where users can add/remove widgets.

Widget Configuration:

export interface WidgetConfig {
id: string;
type: string;
component: Type<any>;
title: string;
data?: any;
position?: { row: number; col: number };
}

Dashboard Component:

import { Component, signal, ViewContainerRef, inject } from '@angular/core';
@Component({
selector: 'app-dashboard',
template: `
<div class="dashboard">
<div class="dashboard-header">
<h2>My Dashboard</h2>
<button (click)="addWidget()">Add Widget</button>
</div>
<div class="widget-grid">
@for (widget of widgets(); track widget.id) {
<div class="widget-container">
<div class="widget-header">
<h3>{{ widget.title }}</h3>
<button (click)="removeWidget(widget.id)">&times;</button>
</div>
<div class="widget-content" #widgetContent></div>
</div>
}
</div>
</div>
`,
})
export class DashboardComponent {
widgets = signal<WidgetConfig[]>([
{
id: '1',
type: 'stats',
component: StatsWidgetComponent,
title: 'Statistics',
data: { value: 1234, label: 'Total Users' }
},
{
id: '2',
type: 'chart',
component: ChartWidgetComponent,
title: 'Sales Chart',
data: { chartData: [10, 20, 30, 40] }
}
]);
addWidget() {
const newWidget: WidgetConfig = {
id: Date.now().toString(),
type: 'stats',
component: StatsWidgetComponent,
title: 'New Widget',
data: { value: 0, label: 'New Metric' }
};
this.widgets.update(w => [...w, newWidget]);
}
removeWidget(id: string) {
this.widgets.update(w => w.filter(widget => widget.id !== id));
}
}
// โœ… Good - Signals for reactive state
@Component({
selector: 'app-container',
})
export class ContainerComponent {
activeComponent = signal<Type<any> | null>(null);
componentData = signal<any>(null);
}
// โŒ Avoid - Plain properties
@Component({
selector: 'app-container',
})
export class ContainerComponent {
activeComponent: Type<any> | null = null;
componentData: any = null;
}
@Component({
selector: 'app-container',
})
export class ContainerComponent {
private componentRef?: ComponentRef<any>;
loadComponent(component: Type<any>) {
// Clean up previous component
this.componentRef?.destroy();
this.componentRef = this.viewContainer.createComponent(component);
}
ngOnDestroy() {
this.componentRef?.destroy();
}
}
// โœ… Good - Type-safe configuration
interface ComponentConfig<T = any> {
component: Type<T>;
data: T extends { data: infer D } ? D : any;
}
// โŒ Avoid - Untyped data
interface ComponentConfig {
component: any;
data: any;
}
// โœ… Good - inject() function
@Component({
selector: 'app-container',
})
export class ContainerComponent {
private viewContainer = inject(ViewContainerRef);
private componentFactoryResolver = inject(ComponentFactoryResolver);
}
:::caution[Legacy Syntax]
In older Angular versions, dependencies were injected via the constructor:
```ts
constructor(
private viewContainer: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver
) {}

This syntax is still supported but not recommended for new projects. Note that ComponentFactoryResolver is deprecated โ€” use ViewContainerRef.createComponent() directly. :::

### 5. **Handle Component Lifecycle**
```typescript
@Component({
selector: 'app-container',
})
export class ContainerComponent {
private componentRef?: ComponentRef<any>;
loadComponent(component: Type<any>) {
const ref = this.viewContainer.createComponent(component);
// Subscribe to component outputs
if (ref.instance.closed) {
ref.instance.closed.subscribe(() => {
this.removeComponent();
});
}
this.componentRef = ref;
}
removeComponent() {
this.componentRef?.destroy();
this.componentRef = undefined;
}
}
  • Modal/Dialog Systems - Show different content types
  • Wizard/Stepper Forms - Load steps dynamically
  • Plugin Architecture - Load features on demand
  • Dashboard Widgets - User-customizable layouts
  • Notification System - Different notification types
  • Content Management - Dynamic page builders
  • A/B Testing - Load variants dynamically
  • Feature Flags - Conditional component loading
  • Understand ViewContainerRef and component creation
  • Load components dynamically at runtime
  • Pass data to dynamic components using setInput()
  • Handle component lifecycle and cleanup
  • Build reusable dynamic component systems
  • Implement modal and tab systems
  • Create dynamic forms
  • Manage component references properly
  1. Content Projection - Create flexible component APIs
  2. Advanced DI Patterns - Master dependency injection
  3. Custom Directives - Add behavior to elements

Pro Tip: Dynamic components are powerful but add complexity. Use them when you truly need runtime flexibility. For most cases, structural directives like @if and @switch are simpler and more performant! ๐ŸŽฏ