AngularJSDynamic Components

Dynamic Components in Angular

Dynamic components are components created and inserted into the DOM at runtime — after the initial render — rather than through static template declarations. They are used for:

  • Modal dialogs and overlay panels
  • Toast/notification systems
  • Plugin architectures where component type is determined at runtime
  • Dynamic forms built from configuration
  • Dashboards where widgets can be added/removed by the user
The Modern Approach: NgComponentOutlet

For most cases, NgComponentOutlet is the simplest way to render a component dynamically from a reference.

TS
// app.component.ts
import { Component } from '@angular/core';
import { NgComponentOutlet } from '@angular/common';
import { AlertComponent } from './alert.component';
import { CardComponent } from './card.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [NgComponentOutlet],
  template: `
    <ng-container [ngComponentOutlet]="currentComponent" />

    <button (click)="showAlert()">Show Alert</button>
    <button (click)="showCard()">Show Card</button>
  `,
})
export class AppComponent {
  currentComponent: any = null;

  showAlert(): void { this.currentComponent = AlertComponent; }
  showCard(): void { this.currentComponent = CardComponent; }
}
Passing Inputs with NgComponentOutlet

TS
// Angular 16+ — pass inputs directly through ngComponentOutlet
@Component({
  selector: 'app-host',
  standalone: true,
  imports: [NgComponentOutlet],
  template: `
    <ng-container
      [ngComponentOutlet]="component"
      [ngComponentOutletInputs]="inputs"
    />
  `,
})
export class HostComponent {
  component = UserCardComponent;
  inputs = { userId: 42, showAvatar: true };
}
Note
ngComponentOutletInputs was added in Angular 16. For earlier versions, you need ViewContainerRef with manual input binding.
ViewContainerRef — Full Programmatic Control

ViewContainerRef gives you imperative control over creating, configuring, and destroying components at a specific location in the DOM.

TS
// src/app/components/toast/toast-host.component.ts
import {
  Component, ViewChild, ViewContainerRef, ComponentRef, OnDestroy
} from '@angular/core';
import { ToastComponent } from './toast.component';

export interface ToastConfig {
  message: string;
  type: 'success' | 'error' | 'info';
  duration?: number;
}

@Component({
  selector: 'app-toast-host',
  standalone: true,
  template: `<ng-template #container></ng-template>`,
})
export class ToastHostComponent implements OnDestroy {
  @ViewChild('container', { read: ViewContainerRef, static: true })
  private container!: ViewContainerRef;

  private toasts: ComponentRef<ToastComponent>[] = [];

  show(config: ToastConfig): void {
    // Create the component dynamically
    const ref = this.container.createComponent(ToastComponent);

    // Set inputs on the created component
    ref.setInput('message', config.message);
    ref.setInput('type', config.type);

    // Listen for the close output
    ref.instance.closed.subscribe(() => this.remove(ref));

    this.toasts.push(ref);

    // Auto-remove after duration
    const duration = config.duration ?? 3000;
    setTimeout(() => this.remove(ref), duration);
  }

  remove(ref: ComponentRef<ToastComponent>): void {
    const index = this.toasts.indexOf(ref);
    if (index !== -1) {
      this.toasts.splice(index, 1);
      ref.destroy(); // destroys the component and removes it from the DOM
    }
  }

  ngOnDestroy(): void {
    this.toasts.forEach(ref => ref.destroy());
  }
}
The Toast Component

TS
// src/app/components/toast/toast.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-toast',
  standalone: true,
  template: `
    <div class="toast" [class]="type">
      <span>{{ message }}</span>
      <button (click)="closed.emit()">✕</button>
    </div>
  `,
  styles: [`
    .toast { padding: 12px 16px; border-radius: 4px; display: flex; gap: 8px; }
    .success { background: #4caf50; color: white; }
    .error { background: #f44336; color: white; }
    .info { background: #2196f3; color: white; }
  `],
})
export class ToastComponent {
  @Input() message = '';
  @Input() type: 'success' | 'error' | 'info' = 'info';
  @Output() closed = new EventEmitter<void>();
}
Global Toast Service

A service-based approach lets any component trigger toasts without direct DOM access.

TS
// src/app/services/toast.service.ts
import { Injectable, ApplicationRef, createComponent, EnvironmentInjector } from '@angular/core';
import { ToastComponent } from '../components/toast/toast.component';

@Injectable({ providedIn: 'root' })
export class ToastService {
  constructor(
    private appRef: ApplicationRef,
    private injector: EnvironmentInjector
  ) {}

  show(message: string, type: 'success' | 'error' | 'info' = 'info'): void {
    // Create the component outside any view container
    const ref = createComponent(ToastComponent, {
      environmentInjector: this.injector,
    });

    ref.setInput('message', message);
    ref.setInput('type', type);

    // Attach to the Angular change detection tree
    this.appRef.attachView(ref.hostView);

    // Append to the document body
    const domElem = (ref.hostView as any).rootNodes[0] as HTMLElement;
    document.body.appendChild(domElem);

    // Clean up after 3 seconds
    ref.instance.closed.subscribe(() => {
      this.appRef.detachView(ref.hostView);
      domElem.remove();
      ref.destroy();
    });

    setTimeout(() => {
      this.appRef.detachView(ref.hostView);
      domElem.remove();
      ref.destroy();
    }, 3000);
  }
}

// Usage in any component
@Injectable()
export class SomeComponent {
  toast = inject(ToastService);

  save(): void {
    this.http.post('/api/save', data).subscribe({
      next: () => this.toast.show('Saved!', 'success'),
      error: () => this.toast.show('Save failed.', 'error'),
    });
  }
}
Dynamic Form Builder from Config

A common use case is rendering different form field components based on a JSON configuration.

TS
// src/app/dynamic-form/dynamic-form.component.ts
import {
  Component, Input, ViewChild, ViewContainerRef,
  OnInit, Type
} from '@angular/core';
import { TextInputComponent } from './fields/text-input.component';
import { SelectInputComponent } from './fields/select-input.component';
import { CheckboxComponent } from './fields/checkbox.component';

export interface FieldConfig {
  type: 'text' | 'select' | 'checkbox';
  name: string;
  label: string;
  options?: string[];
}

const FIELD_MAP: Record<string, Type<any>> = {
  text: TextInputComponent,
  select: SelectInputComponent,
  checkbox: CheckboxComponent,
};

@Component({
  selector: 'app-dynamic-form',
  standalone: true,
  template: `
    <form>
      @for (field of fields; track field.name) {
        <ng-container #fieldContainer></ng-container>
      }
    </form>
  `,
})
export class DynamicFormComponent implements OnInit {
  @Input() fields: FieldConfig[] = [];
  @ViewChild('fieldContainer', { read: ViewContainerRef }) container!: ViewContainerRef;

  ngOnInit(): void {
    this.fields.forEach(field => {
      const component = FIELD_MAP[field.type];
      if (component) {
        const ref = this.container.createComponent(component);
        ref.setInput('config', field);
      }
    });
  }
}
Component Portal Pattern

For modals and overlays, Angular CDK's Portal is the cleanest approach.

TS
// Install Angular CDK: npm install @angular/cdk

// src/app/services/dialog.service.ts
import { Injectable, inject } from '@angular/core';
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { ConfirmDialogComponent } from '../components/confirm-dialog.component';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class DialogService {
  private overlay = inject(Overlay);

  confirm(message: string): Observable<boolean> {
    return new Observable(observer => {
      const overlayRef: OverlayRef = this.overlay.create({
        hasBackdrop: true,
        positionStrategy: this.overlay.position()
          .global().centerHorizontally().centerVertically(),
      });

      const portal = new ComponentPortal(ConfirmDialogComponent);
      const ref = overlayRef.attach(portal);

      ref.instance.message = message;
      ref.instance.result.subscribe((result: boolean) => {
        observer.next(result);
        observer.complete();
        overlayRef.dispose();
      });

      overlayRef.backdropClick().subscribe(() => {
        observer.next(false);
        observer.complete();
        overlayRef.dispose();
      });
    });
  }
}
Tip
Angular CDK Overlay handles z-index stacking, backdrop clicks, scroll locking, and accessibility (focus trapping) automatically — use it instead of building your own modal infrastructure.
Common Dynamic Component Patterns

Pattern

Recommended Tool

Simple conditional component

NgComponentOutlet

Toast / notification system

ViewContainerRef + service

Modal / dialog overlay

Angular CDK Overlay + ComponentPortal

Dynamic form fields

ViewContainerRef in ngAfterViewInit

Plugin system

Lazy-loaded modules + ViewContainerRef

Dashboard widgets

NgComponentOutlet + ngComponentOutletInputs

Best Practices
  • Prefer NgComponentOutlet for simple dynamic rendering — it is declarative

  • Use ViewContainerRef when you need programmatic control (inputs, outputs, lifecycle)

  • Always call ref.destroy() when done — leaked components cause memory leaks

  • Use ApplicationRef.attachView() when creating components outside a view tree

  • Prefer Angular CDK Overlay for modal dialogs — it handles focus and accessibility

  • Use createComponent() (Angular 14+) instead of the deprecated ComponentFactoryResolver

  • Keep dynamic component logic in services, not in host components

Summary

Dynamic components unlock powerful runtime UI patterns like toast notifications, modals, and configurable dashboards. The modern toolkit includes NgComponentOutlet for declarative cases, ViewContainerRef.createComponent() for programmatic control, and Angular CDK Overlay for production-grade overlays. Always destroy component refs when they are no longer needed to prevent memory leaks.