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.
๐ฏ What are Dynamic Components?
Section titled โ๐ฏ What are Dynamic Components?โ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
๐ Basic Dynamic Component Loading
Section titled โ๐ Basic Dynamic Component LoadingโUsing ViewContainerRef
Section titled โUsing ViewContainerRefโ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(); }}Component to Load
Section titled โComponent to Loadโ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');}๐จ Real-World Examples
Section titled โ๐จ Real-World Examplesโ1. Dynamic Modal System
Section titled โ1. Dynamic Modal Systemโ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()">×</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!'); }}2. Dynamic Tab System
Section titled โ2. Dynamic Tab Systemโ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' } ] } } ]);}3. Dynamic Form Builder
Section titled โ3. Dynamic Form Builderโ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' } ] } ]);}4. Dynamic Dashboard Widgets
Section titled โ4. Dynamic Dashboard Widgetsโ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)">×</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)); }}โ Best Practices
Section titled โโ Best Practicesโ1. Use Signals for State Management
Section titled โ1. Use Signals for State Managementโ// โ
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;}2. Clean Up Component References
Section titled โ2. Clean Up Component Referencesโ@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(); }}3. Type-Safe Component Data
Section titled โ3. Type-Safe Component Dataโ// โ
Good - Type-safe configurationinterface ComponentConfig<T = any> { component: Type<T>; data: T extends { data: infer D } ? D : any;}
// โ Avoid - Untyped datainterface ComponentConfig { component: any; data: any;}4. Use inject() Function
Section titled โ4. Use inject() Functionโ// โ
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:```tsconstructor( 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; }}๐ฏ Common Use Cases
Section titled โ๐ฏ Common Use Casesโ- 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
๐ Learning Checklist
Section titled โ๐ Learning Checklistโ- 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
๐ Next Steps
Section titled โ๐ Next Stepsโ- Content Projection - Create flexible component APIs
- Advanced DI Patterns - Master dependency injection
- 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! ๐ฏ