Skip to the solution
HardFrontend9 min read

How do you dynamically create components?

454 views

The problem

Question presented to candidate: "A dashboard lets users add widgets whose type is only known at runtime, and a toast service needs to show a component attached to document.body. How do you create components dynamically in modern Angular, pass them inputs and listen to their outputs?"

What a strong answer should cover:

  • ViewContainerRef.createComponent(Type) inserts a component at an anchor (often <ng-container #slot> read as a ViewContainerRef) and returns a ComponentRef.
  • Pass inputs with ref.setInput(name, value) (marks OnPush views dirty) or, since v20, declare bindings: [inputBinding(...), outputBinding(...), twoWayBinding(...)] at creation; signals bound this way stay live.
  • Clean up with ref.destroy() or container.clear(); subscriptions to outputs end with the component.
  • NgComponentOutlet (*ngComponentOutlet="type; inputs: {...}") is the declarative option for "render whichever component this signal holds".
  • The standalone createComponent() function with environmentInjector and hostElement creates a component outside any container; you must appRef.attachView(ref.hostView) for it to update.
  • ComponentFactoryResolver is gone from the v22 public API; the component class is passed directly.

Clarifying questions expected:

  • "Does the component need to live inside the current view, or at document.body like an overlay?"
  • "Are the components known at build time, or lazy-loaded (import()), for example from a remote?"

Code / implementation expected: Yes β€” a container-based example with setInput and NgComponentOutlet, plus one using bindings and the standalone createComponent().

  • dynamic-components

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 11 sections

Read the code

AngularViewContainerRef.createComponent with setInput, outputs and destroy, plus NgComponentOutlet (verified AOT + playground on Angular 22.2.3)
import {
  Component, ComponentRef, EventEmitter, Input, Output, Type,
  ViewChild, ViewContainerRef, signal,
} from "@angular/core";
import { NgComponentOutlet } from "@angular/common";

// Two widgets the dashboard does not know about at compile time.
@Component({
  selector: "app-weather",
  template: `<p class="w">Weather in {{ city }}: 18 C <button (click)="closed.emit(city)">x</button></p>`,
})
export class WeatherWidget {
  @Input() city = "?";
  @Output() closed = new EventEmitter<string>();
}

@Component({
  selector: "app-stocks",
  template: `<p class="s">{{ symbol }} is up 2%</p>`,
})
export class StocksWidget {
  @Input() symbol = "?";
}

@Component({
  selector: "app-root",
  imports: [NgComponentOutlet],
  template: `
    <h3>1. ViewContainerRef.createComponent</h3>
    <button id="add" (click)="addWeather()">Add weather</button>
    <button id="clear" (click)="clear()">Clear</button>
    <ng-container #slot />
    <p id="log">{{ log() }}</p>

    <h3>2. NgComponentOutlet</h3>
    <button id="swap" (click)="swap()">Swap widget</button>
    <ng-container *ngComponentOutlet="current().type; inputs: current().inputs" />
  `,
})
export class AppComponent {
  // The anchor where dynamic views are inserted. static: true = available in ngOnInit.
  @ViewChild("slot", { read: ViewContainerRef, static: true }) slot!: ViewContainerRef;
  private refs: ComponentRef<WeatherWidget>[] = [];
  private cities = ["Oslo", "Lima", "Pune"];
  log = signal("no widgets");

  addWeather() {
    const city = this.cities[this.refs.length % this.cities.length];
    const ref = this.slot.createComponent(WeatherWidget);
    ref.setInput("city", city); // goes through Angular, so OnPush children are marked dirty
    ref.instance.closed.subscribe((c) => this.remove(ref, c));
    this.refs.push(ref);
    this.log.set(`added ${city}; container length = ${this.slot.length}`);
  }

  remove(ref: ComponentRef<WeatherWidget>, city: string) {
    ref.destroy(); // removes it from the container and runs its ngOnDestroy
    this.refs = this.refs.filter((r) => r !== ref);
    this.log.set(`closed ${city}; container length = ${this.slot.length}`);
  }

  clear() {
    this.slot.clear();
    this.refs = [];
    this.log.set(`cleared; container length = ${this.slot.length}`);
  }

  private widgets: { type: Type<unknown>; inputs: Record<string, unknown> }[] = [
    { type: StocksWidget, inputs: { symbol: "ACME" } },
    { type: WeatherWidget, inputs: { city: "Rome" } },
  ];
  current = signal(this.widgets[0]);
  swap() {
    this.current.set(this.current() === this.widgets[0] ? this.widgets[1] : this.widgets[0]);
  }
}
AngularCreation-time bindings (inputBinding, twoWayBinding, outputBinding) and the standalone createComponent() on document.body (AOT-verified; signal APIs need the compiler)
import {
  ApplicationRef, Component, ComponentRef, EnvironmentInjector, ViewContainerRef,
  createComponent, inject, input, inputBinding, model, output, outputBinding,
  signal, twoWayBinding, viewChild,
} from "@angular/core";

// A modern child: signal input, model() and output().
@Component({
  selector: "app-toast",
  template: `<div class="toast">{{ message() }} (seen {{ count() }}) <button (click)="dismiss.emit()">ok</button></div>`,
})
export class Toast {
  message = input.required<string>();
  count = model(0);
  dismiss = output<void>();
}

@Component({
  selector: "app-root",
  template: `
    <button id="show" (click)="show()">Show toast in container</button>
    <button id="bump" (click)="text.set('Saved again')">Change text</button>
    <ng-container #host />
    <p id="state">text={{ text() }} seen={{ seen() }} dismissed={{ dismissed() }}</p>
    <button id="popup" (click)="popup()">Popup on body</button>
  `,
})
export class AppComponent {
  private host = viewChild.required("host", { read: ViewContainerRef });
  private appRef = inject(ApplicationRef);
  private env = inject(EnvironmentInjector);

  text = signal("Saved");
  seen = signal(1);
  dismissed = signal(0);
  private ref?: ComponentRef<Toast>;

  show() {
    this.host().clear();
    // Bindings are declared once at creation and stay live: no setInput calls later.
    this.ref = this.host().createComponent(Toast, {
      bindings: [
        inputBinding("message", this.text),       // signal -> input, kept in sync
        twoWayBinding("count", this.seen),        // like [(count)]="seen"
        outputBinding("dismiss", () => {          // like (dismiss)="..."
          this.dismissed.update((n) => n + 1);
          this.seen.update((n) => n + 1);
        }),
      ],
    });
  }

  popup() {
    // No ViewContainerRef: the standalone createComponent() function + a host element.
    const hostElement = document.createElement("section");
    document.body.appendChild(hostElement);
    const ref = createComponent(Toast, {
      environmentInjector: this.env,
      hostElement,
      bindings: [inputBinding("message", () => "Popup on body")],
    });
    this.appRef.attachView(ref.hostView); // without this it never updates
    ref.changeDetectorRef.detectChanges();
  }
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 104 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextHard

How do you avoid memory leaks from subscriptions?

Continue