> ## 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.

# 28. Dynamic Components

> Create and manage dynamic components at runtime in Angular

<Frame>
  <img src="https://mintcdn.com/devweeekends/AEOaWh79Ur7CdHHv/images/courses/angular-crash-course/component-anatomy.svg?fit=max&auto=format&n=AEOaWh79Ur7CdHHv&q=85&s=41123a24e19f14765f2ad4d1bf7a6278" alt="Dynamic Components" width="900" height="500" data-path="images/courses/angular-crash-course/component-anatomy.svg" />
</Frame>

## Dynamic Components Overview

<Info>
  **Estimated Time**: 2 hours | **Difficulty**: Advanced | **Prerequisites**: Components, Dependency Injection
</Info>

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

```typescript theme={null}
// 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.

<Tip>
  **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.
</Tip>

```typescript theme={null}
// 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.

```typescript theme={null}
// 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();
    }
  }
}
```

***

## Component Factory with Inputs

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.

<Warning>
  **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.
</Warning>

```typescript theme={null}
// 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)

```typescript theme={null}
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;
  }
}
```

***

## Form Builder Pattern

```typescript theme={null}
// 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

<CardGroup cols={2}>
  <Card title="Clean Up References" icon="broom">
    Always destroy component refs to prevent memory leaks
  </Card>

  <Card title="Use setInput()" icon="plug">
    Prefer setInput() over direct instance access for signals support
  </Card>

  <Card title="Lazy Load" icon="download">
    Dynamically import components to reduce bundle size
  </Card>

  <Card title="Type Safety" icon="shield">
    Use generics to maintain type safety with dynamic components
  </Card>
</CardGroup>

***

<Card title="Next: Custom Schematics" icon="arrow-right" href="/courses/angular-crash-course/29-custom-schematics">
  Learn to create custom Angular CLI schematics
</Card>
