Angular Dynamic Components and Host Directives

Angular Tutorial: Beginner to Advanced · 14. Directives and dynamic UI · 4 min read · Updated

Sometimes you do not know at build time which component to show: dashboard widgets, form fields built from configuration, or steps in a wizard. Angular can create components at run time. Host directives help you combine small behaviours into one component.

What you will learn

  • How to render a component chosen at run time with NgComponentOutlet
  • How to pass inputs to dynamic components
  • How hostDirectives composes behaviour

Widgets to choose from

// src/app/dynamic/widgets.ts
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-stat-widget',
  template: `<p><strong>{{ label() }}</strong>: {{ value() }}</p>`,
})
export class StatWidget {
  readonly label = input('Users');
  readonly value = input(0);
}

@Component({
  selector: 'app-note-widget',
  template: `<p>Note: {{ text() }}</p>`,
})
export class NoteWidget {
  readonly text = input('');
}

NgComponentOutlet

// src/app/dynamic/widget-board.ts
import { Component, Type, signal } from '@angular/core';
import { NgComponentOutlet } from '@angular/common';
import { NoteWidget, StatWidget } from './widgets';

interface WidgetConfig {
  id: number;
  component: Type<unknown>;
  inputs: Record<string, unknown>;
}

@Component({
  selector: 'app-widget-board',
  imports: [NgComponentOutlet],
  template: `
    <button type="button" (click)="addStat()">Add stat</button>
    <button type="button" (click)="addNote()">Add note</button>

    @for (w of widgets(); track w.id) {
      <div class="widget">
        <ng-container *ngComponentOutlet="w.component; inputs: w.inputs" />
        <button type="button" (click)="remove(w.id)">Remove</button>
      </div>
    } @empty {
      <p>No widgets yet.</p>
    }
  `,
  styles: `.widget { border: 1px solid #ddd; padding: .5rem 1rem; margin: .5rem 0; }`,
})
export class WidgetBoard {
  private nextId = 1;
  protected readonly widgets = signal<WidgetConfig[]>([]);

  protected addStat(): void {
    this.widgets.update((list) => [
      ...list,
      { id: this.nextId++, component: StatWidget, inputs: { label: 'Orders', value: this.nextId * 10 } },
    ]);
  }

  protected addNote(): void {
    this.widgets.update((list) => [
      ...list,
      { id: this.nextId++, component: NoteWidget, inputs: { text: 'Hello from a dynamic widget' } },
    ]);
  }

  protected remove(id: number): void {
    this.widgets.update((list) => list.filter((w) => w.id !== id));
  }
}

The inputs object sets the dynamic component's signal inputs by name. Store a reference to the class (the Type) in your data, and Angular creates it for you.

For lazy loading, make the config hold a loader function instead: component: () => import('./big-widget').then((m) => m.BigWidget), and await it before adding it to the list. The chunk is then downloaded only when the user adds that widget.

Host directives: compose behaviour

A host directive attaches directives to a component from inside its decorator. The component gets their behaviour, and you choose which inputs and outputs to expose.

// src/app/dynamic/focus-ring.ts
import { Directive } from '@angular/core';

@Directive({
  selector: '[appFocusRing]',
  host: {
    '[style.outline]': '"none"',
    '(focus)': 'onFocus($event)',
    '(blur)': 'onBlur($event)',
  },
})
export class FocusRing {
  protected onFocus(event: FocusEvent): void {
    (event.target as HTMLElement).style.boxShadow = '0 0 0 3px #4c9ffe';
  }

  protected onBlur(event: FocusEvent): void {
    (event.target as HTMLElement).style.boxShadow = 'none';
  }
}
// src/app/dynamic/fancy-button.ts
import { Component } from '@angular/core';
import { FocusRing } from './focus-ring';

@Component({
  selector: 'button[appFancy]',
  hostDirectives: [FocusRing],
  template: `<ng-content />`,
  styles: `:host { padding: .5rem 1rem; border-radius: 2rem; border: 0; background: #0b6cff; color: white; }`,
})
export class FancyButton {}
// src/app/app.ts
import { Component } from '@angular/core';
import { WidgetBoard } from './dynamic/widget-board';
import { FancyButton } from './dynamic/fancy-button';

@Component({
  imports: [WidgetBoard, FancyButton],
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
})
export class App {}
<!-- src/app/app.html -->
<main>
  <h1>Dynamic UI</h1>
  <button appFancy type="button">Fancy button</button>
  <app-widget-board />
</main>

Every <button appFancy> now has a visible focus ring from FocusRing, without the user adding that directive. Host directives replace most cases where you might have used class inheritance.

The key parts

  • NgComponentOutlet is the simplest way to render a component that is chosen at run time. Use inputs to pass values.
  • Component classes are values, so you can keep them in arrays, maps and configuration.
  • For full control (creating on click, destroying later) use ViewContainerRef.createComponent().
  • hostDirectives composes behaviour. It also supports inputs and outputs to expose directive APIs on the host.
  • Dynamic UI is harder to type-check and test. Use it where the flexibility is really needed.

Try it yourself

  1. Add a third widget type and a button to create it.
  2. Save the widget list in localStorage, storing a string key such as 'stat' and mapping it back to the class when loading.
  3. Expose an input from a host directive using hostDirectives: [{ directive: X, inputs: ['name'] }].

Quick check

  1. How do you pass inputs to a component rendered by NgComponentOutlet?
  2. What do host directives replace?
  3. Why map string keys to classes before saving widget configuration?

Next, continue with Angular ElementRef, Renderer2, HostBinding, HostListener and Legacy Decorators.

Frequently asked questions

Is NgComponentOutlet standalone-friendly?

Yes. Import NgComponentOutlet into the component that uses it, and the dynamic component only needs to be a standalone component.

When should I use ViewContainerRef?

When you need to create components at specific moments from code, for example a modal service, and to hold the returned component reference to destroy or update it later.