Documentation Index Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Dynamic Components Overview
Estimated Time : 2 hours | Difficulty : Advanced | Prerequisites : Components, Dependency Injection
Dynamic components are created programmatically at runtime, enabling powerful patterns like modal systems, plugin architectures, and configurable UI builders. Think of it like a restaurant menu versus a food truck: normal components are menu items that exist at compile time, while dynamic components are dishes the chef invents on the fly based on what ingredients arrive. The Angular runtime creates the component, wires up dependency injection, runs lifecycle hooks, and plugs it into change detection — all happening at runtime rather than being declared in a template.
When to reach for dynamic components : Modal/dialog systems, toast notifications, configurable dashboards where users choose which widgets to display, form builders driven by JSON configuration, and plugin architectures where third-party code provides components your shell did not know about at build time.
┌─────────────────────────────────────────────────────────────────────────┐
│ Dynamic Component Lifecycle │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Define Component 2. Get Reference 3. Create │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐│
│ │ @Component({ │ │ ViewContainerRef │ │ createComponent │
│ │ template:... │ ───► │ or │ ───► │ () ││
│ │ }) │ │ ViewChild │ │ ││
│ │ class Dynamic │ │ │ │ ││
│ └─────────────────┘ └─────────────────┘ └─────────────────┘│
│ │ │
│ 6. Destroy 5. Interact 4. Set Inputs │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐│
│ │ componentRef │ │ Subscribe to │ │ setInput() ││
│ │ .destroy() │ ◄─── │ outputs │ ◄─── │ or ││
│ │ │ │ │ │ instance.prop ││
│ └─────────────────┘ └─────────────────┘ └─────────────────┘│
│ │
└─────────────────────────────────────────────────────────────────────────┘
Creating Dynamic Components
Using ViewContainerRef
// alert.component.ts
@ Component ({
selector: 'app-alert' ,
standalone: true ,
template: `
<div class="alert" [class]="'alert-' + type()">
<span class="icon">{{ icon() }}</span>
<span class="message">{{ message() }}</span>
<button class="close" (click)="closed.emit()">×</button>
</div>
` ,
styles: `
.alert {
display: flex;
align-items: center;
padding: 1rem;
border-radius: 8px;
margin-bottom: 0.5rem;
}
.alert-success { background: #d4edda; color: #155724; }
.alert-error { background: #f8d7da; color: #721c24; }
.alert-warning { background: #fff3cd; color: #856404; }
.alert-info { background: #d1ecf1; color: #0c5460; }
`
})
export class AlertComponent {
type = input < 'success' | 'error' | 'warning' | 'info' >( 'info' );
message = input . required < string >();
icon = input ( 'ℹ️' );
closed = output < void >();
}
// alert-container.component.ts
@ Component ({
selector: 'app-alert-container' ,
standalone: true ,
template: `
<div class="alert-container">
<ng-container #alertContainer />
</div>
`
})
export class AlertContainerComponent {
@ ViewChild ( 'alertContainer' , { read: ViewContainerRef })
container !: ViewContainerRef ;
private alerts = new Map < ComponentRef < AlertComponent >, number >();
showAlert ( config : AlertConfig ) {
// createComponent instantiates the component, runs its constructor,
// wires up dependency injection, and inserts it into the DOM at
// the container's location. It returns a ComponentRef that gives
// you a handle to set inputs, subscribe to outputs, and destroy it.
const componentRef = this . container . createComponent ( AlertComponent );
// setInput() is the safe way to pass data to dynamic components.
// Unlike directly setting instance properties, setInput() works
// correctly with signal-based inputs and triggers OnPush change detection.
componentRef . setInput ( 'type' , config . type );
componentRef . setInput ( 'message' , config . message );
componentRef . setInput ( 'icon' , config . icon ?? this . getDefaultIcon ( config . type ));
// Subscribe to the component's output to handle the close event.
componentRef . instance . closed . subscribe (() => {
this . removeAlert ( componentRef );
});
// Auto-dismiss
if ( config . duration !== 0 ) {
const timeout = setTimeout (() => {
this . removeAlert ( componentRef );
}, config . duration ?? 5000 );
this . alerts . set ( componentRef , timeout );
}
return componentRef ;
}
private removeAlert ( ref : ComponentRef < AlertComponent >) {
const timeout = this . alerts . get ( ref );
if ( timeout ) clearTimeout ( timeout );
ref . destroy ();
this . alerts . delete ( ref );
}
private getDefaultIcon ( type : string ) : string {
const icons : Record < string , string > = {
success: '✅' ,
error: '❌' ,
warning: '⚠️' ,
info: 'ℹ️'
};
return icons [ type ] ?? 'ℹ️' ;
}
}
interface AlertConfig {
type : 'success' | 'error' | 'warning' | 'info' ;
message : string ;
icon ?: string ;
duration ?: number ;
}
NgComponentOutlet
NgComponentOutlet is the declarative alternative to ViewContainerRef.createComponent(). Instead of imperatively creating components in TypeScript, you declare them in the template. This is simpler for the common case of “render component X with inputs Y” — you do not need ViewChild, AfterViewInit, or manual lifecycle management. The trade-off is less control: you cannot subscribe to outputs or get a ComponentRef for fine-grained manipulation.
When to use which : Use NgComponentOutlet for simple “swap this component” scenarios (tab content, role-based rendering). Use ViewContainerRef.createComponent() when you need to manage component lifecycle, subscribe to outputs, or create multiple instances.
// Simple dynamic component rendering
@ Component ({
selector: 'app-dynamic-host' ,
standalone: true ,
imports: [ NgComponentOutlet ],
template: `
<ng-container *ngComponentOutlet="currentComponent()" />
<!-- With inputs (Angular 16+) -->
<ng-container
*ngComponentOutlet="currentComponent(); inputs: componentInputs()"
/>
<!-- With injector -->
<ng-container
*ngComponentOutlet="currentComponent(); injector: customInjector"
/>
`
})
export class DynamicHostComponent {
currentComponent = signal < Type < any >>( DefaultComponent );
componentInputs = signal < Record < string , unknown >>({});
customInjector = Injector . create ({
providers: [
{ provide: CUSTOM_TOKEN , useValue: 'custom value' }
],
parent: inject ( Injector )
});
async loadComponent ( name : string ) {
switch ( name ) {
case 'chart' :
const { ChartComponent } = await import ( './chart.component' );
this . currentComponent . set ( ChartComponent );
this . componentInputs . set ({ data: this . chartData });
break ;
case 'table' :
const { TableComponent } = await import ( './table.component' );
this . currentComponent . set ( TableComponent );
this . componentInputs . set ({ rows: this . tableData });
break ;
}
}
}
Modal Service Pattern
The modal service pattern is the canonical use case for dynamic components. The idea: instead of littering every page component with @if (showModal) conditionals and modal markup, you create a centralized service that can open any component as a modal from anywhere in the app. The service returns a ModalRef with a result$ observable, so callers can await the user’s decision — just like window.confirm() but with rich UI.
This is exactly how Angular Material’s MatDialog works internally. Building your own teaches you the mechanics and gives you full control over styling and behavior.
// modal.service.ts
@ Injectable ({ providedIn: 'root' })
export class ModalService {
private viewContainerRef !: ViewContainerRef ;
private activeModals = new Map < string , ComponentRef < ModalWrapperComponent >>();
setContainer ( vcr : ViewContainerRef ) {
this . viewContainerRef = vcr ;
}
open < T , R = any >(
component : Type < T >,
config ?: ModalConfig < T >
) : ModalRef < R > {
const id = crypto . randomUUID ();
// Create wrapper component
const wrapperRef = this . viewContainerRef . createComponent ( ModalWrapperComponent );
wrapperRef . setInput ( 'config' , config );
// Create content component inside wrapper
const contentRef = wrapperRef . instance . createContent ( component );
// Set inputs on content component
if ( config ?. data ) {
Object . entries ( config . data ). forEach (([ key , value ]) => {
contentRef . setInput ( key , value );
});
}
this . activeModals . set ( id , wrapperRef );
const modalRef = new ModalRef < R >( id , () => this . close ( id ));
// Handle close events
wrapperRef . instance . closed . subscribe (( result ) => {
modalRef . setResult ( result );
this . close ( id );
});
return modalRef ;
}
close ( id : string ) {
const ref = this . activeModals . get ( id );
if ( ref ) {
ref . destroy ();
this . activeModals . delete ( id );
}
}
closeAll () {
this . activeModals . forEach (( ref , id ) => this . close ( id ));
}
}
// modal-ref.ts
export class ModalRef < R = any > {
private resultSubject = new Subject < R | undefined >();
readonly result$ = this . resultSubject . asObservable ();
constructor (
readonly id : string ,
private closeFn : () => void
) {}
close ( result ?: R ) {
this . setResult ( result );
this . closeFn ();
}
setResult ( result ?: R ) {
this . resultSubject . next ( result );
this . resultSubject . complete ();
}
}
// modal-wrapper.component.ts
@ Component ({
selector: 'app-modal-wrapper' ,
standalone: true ,
template: `
<div class="modal-backdrop" (click)="onBackdropClick()">
<div
class="modal-content"
[class]="config()?.panelClass"
(click)="$event.stopPropagation()"
>
@if (config()?.showHeader !== false) {
<header class="modal-header">
<h2>{{ config()?.title }}</h2>
<button (click)="close()">×</button>
</header>
}
<div class="modal-body">
<ng-container #contentContainer />
</div>
</div>
</div>
` ,
animations: [
trigger ( 'fadeIn' , [
transition ( ':enter' , [
style ({ opacity: 0 }),
animate ( '150ms' , style ({ opacity: 1 }))
])
])
]
})
export class ModalWrapperComponent {
@ ViewChild ( 'contentContainer' , { read: ViewContainerRef })
contentContainer !: ViewContainerRef ;
config = input < ModalConfig < any >>();
closed = output < any >();
createContent < T >( component : Type < T >) : ComponentRef < T > {
return this . contentContainer . createComponent ( component );
}
close ( result ?: any ) {
this . closed . emit ( result );
}
onBackdropClick () {
if ( this . config ()?. disableClose !== true ) {
this . close ();
}
}
}
// Usage
@ Component ({
template: `
<button (click)="openConfirmDialog()">Delete Item</button>
`
})
export class ItemActionsComponent {
private modal = inject ( ModalService );
async openConfirmDialog () {
const modalRef = this . modal . open ( ConfirmDialogComponent , {
title: 'Confirm Delete' ,
data: {
message: 'Are you sure you want to delete this item?' ,
confirmText: 'Delete' ,
cancelText: 'Cancel'
}
});
const confirmed = await firstValueFrom ( modalRef . result$ );
if ( confirmed ) {
this . deleteItem ();
}
}
}
The factory-registry pattern is essential for building configurable dashboards, CMS-driven UIs, or plugin architectures. The idea: register component types by name at startup, then create instances by name at runtime based on user configuration or backend data. This decouples “what widgets exist” from “which widgets to display” — the user (or admin) controls the layout, and the factory handles the instantiation.
Memory leak pitfall : Every createComponent() call creates a component that Angular will not destroy automatically. You must call ref.destroy() when removing a dynamic component, or call container.clear() before re-rendering. Otherwise, each render cycle adds components without removing old ones, leading to memory leaks and duplicated event handlers.
// widget-factory.service.ts
@ Injectable ({ providedIn: 'root' })
export class WidgetFactory {
private registry = new Map < string , Type < any >>();
register ( type : string , component : Type < any >) {
this . registry . set ( type , component );
}
create < T >(
type : string ,
container : ViewContainerRef ,
inputs ?: Partial < T >
) : ComponentRef < T > | null {
const component = this . registry . get ( type );
if ( ! component ) {
console . warn ( `Widget type " ${ type } " not registered` );
return null ;
}
const ref = container . createComponent ( component );
if ( inputs ) {
Object . entries ( inputs ). forEach (([ key , value ]) => {
ref . setInput ( key , value );
});
}
return ref ;
}
}
// dashboard.component.ts
@ Component ({
selector: 'app-dashboard' ,
standalone: true ,
template: `
<div class="dashboard-grid">
@for (widget of widgets(); track widget.id) {
<div class="widget-cell" [style.grid-area]="widget.position">
<ng-container #widgetHost />
</div>
}
</div>
`
})
export class DashboardComponent implements AfterViewInit {
@ ViewChildren ( 'widgetHost' , { read: ViewContainerRef })
widgetHosts !: QueryList < ViewContainerRef >;
private factory = inject ( WidgetFactory );
widgets = input . required < WidgetConfig []>();
ngAfterViewInit () {
this . renderWidgets ();
// Re-render when widgets change
effect (() => {
this . widgets ();
this . renderWidgets ();
});
}
private renderWidgets () {
const hosts = this . widgetHosts . toArray ();
this . widgets (). forEach (( widget , index ) => {
const host = hosts [ index ];
if ( host ) {
host . clear ();
this . factory . create ( widget . type , host , widget . config );
}
});
}
}
interface WidgetConfig {
id : string ;
type : string ;
position : string ;
config : Record < string , any >;
}
Portal Pattern (CDK)
import { PortalModule , CdkPortal , DomPortalOutlet } from '@angular/cdk/portal' ;
// Using CdkPortal
@ Component ({
selector: 'app-tooltip' ,
standalone: true ,
imports: [ PortalModule ],
template: `
<ng-template cdkPortal #tooltipPortal>
<div class="tooltip" [style.top.px]="y()" [style.left.px]="x()">
<ng-content />
</div>
</ng-template>
`
})
export class TooltipComponent implements OnInit , OnDestroy {
@ ViewChild ( CdkPortal ) portal !: CdkPortal ;
x = input ( 0 );
y = input ( 0 );
private outlet !: DomPortalOutlet ;
constructor (
private appRef : ApplicationRef ,
private injector : Injector
) {}
ngOnInit () {
// Create outlet at document body
this . outlet = new DomPortalOutlet (
document . body ,
null ,
this . appRef ,
this . injector
);
}
show () {
this . outlet . attach ( this . portal );
}
hide () {
this . outlet . detach ();
}
ngOnDestroy () {
this . outlet . dispose ();
}
}
// Overlay Service using CDK Overlay
import { Overlay , OverlayRef } from '@angular/cdk/overlay' ;
import { ComponentPortal } from '@angular/cdk/portal' ;
@ Injectable ({ providedIn: 'root' })
export class OverlayService {
private overlay = inject ( Overlay );
openDropdown < T >(
trigger : ElementRef ,
component : Type < T >,
inputs ?: Record < string , any >
) : OverlayRef {
const positionStrategy = this . overlay
. position ()
. flexibleConnectedTo ( trigger )
. withPositions ([
{
originX: 'start' ,
originY: 'bottom' ,
overlayX: 'start' ,
overlayY: 'top'
}
]);
const overlayRef = this . overlay . create ({
positionStrategy ,
hasBackdrop: true ,
backdropClass: 'cdk-overlay-transparent-backdrop'
});
const portal = new ComponentPortal ( component );
const componentRef = overlayRef . attach ( portal );
if ( inputs ) {
Object . entries ( inputs ). forEach (([ key , value ]) => {
componentRef . setInput ( key , value );
});
}
overlayRef . backdropClick (). subscribe (() => overlayRef . dispose ());
return overlayRef ;
}
}
// dynamic-form.component.ts
@ Component ({
selector: 'app-dynamic-form' ,
standalone: true ,
imports: [ ReactiveFormsModule ],
template: `
<form [formGroup]="form()" (ngSubmit)="onSubmit()">
@for (field of fields(); track field.key) {
<div class="form-field">
<label [for]="field.key">{{ field.label }}</label>
<ng-container #fieldHost />
</div>
}
<button type="submit" [disabled]="form().invalid">Submit</button>
</form>
`
})
export class DynamicFormComponent implements AfterViewInit {
@ ViewChildren ( 'fieldHost' , { read: ViewContainerRef })
fieldHosts !: QueryList < ViewContainerRef >;
fields = input . required < FormFieldConfig []>();
form = input . required < FormGroup >();
submitted = output < Record < string , any >>();
private componentMap : Record < string , Type < any >> = {
text: TextInputComponent ,
number: NumberInputComponent ,
select: SelectInputComponent ,
checkbox: CheckboxInputComponent ,
date: DateInputComponent ,
textarea: TextareaInputComponent
};
ngAfterViewInit () {
this . renderFields ();
}
private renderFields () {
const hosts = this . fieldHosts . toArray ();
this . fields (). forEach (( field , index ) => {
const host = hosts [ index ];
const component = this . componentMap [ field . type ];
if ( host && component ) {
host . clear ();
const ref = host . createComponent ( component );
ref . setInput ( 'field' , field );
ref . setInput ( 'control' , this . form (). get ( field . key ));
}
});
}
onSubmit () {
if ( this . form (). valid ) {
this . submitted . emit ( this . form (). value );
}
}
}
interface FormFieldConfig {
key : string ;
type : 'text' | 'number' | 'select' | 'checkbox' | 'date' | 'textarea' ;
label : string ;
placeholder ?: string ;
options ?: { value : any ; label : string }[];
validators ?: ValidatorFn [];
}
// text-input.component.ts
@ Component ({
selector: 'app-text-input' ,
standalone: true ,
imports: [ ReactiveFormsModule ],
template: `
<input
type="text"
[id]="field().key"
[formControl]="control()"
[placeholder]="field().placeholder ?? ''"
/>
@if (control().invalid && control().touched) {
<span class="error">{{ getErrorMessage() }}</span>
}
`
})
export class TextInputComponent {
field = input . required < FormFieldConfig >();
control = input . required < FormControl >();
getErrorMessage () : string {
const errors = this . control (). errors ;
if ( errors ?.[ 'required' ]) return 'This field is required' ;
if ( errors ?.[ 'minlength' ]) return `Minimum ${ errors [ 'minlength' ]. requiredLength } characters` ;
return 'Invalid value' ;
}
}
Best Practices
Clean Up References Always destroy component refs to prevent memory leaks
Use setInput() Prefer setInput() over direct instance access for signals support
Lazy Load Dynamically import components to reduce bundle size
Type Safety Use generics to maintain type safety with dynamic components
Next: Custom Schematics Learn to create custom Angular CLI schematics