Setting the Stage: A Note Before We Start

First, a few clarifications. Angular's fundamental approach to dynamic component creation hasn't shifted. The evolution is in the API surface. Everything covered in this piece comes from a preview build — angular-next-1 — and details could still shift before Angular version 20 officially lands.

Introduction: The Dynamic Creation Landscape

Angular offers developers multiple paths for instantiating components on the fly and connecting them to the view renderer and the change detection pipeline. Historically, a notable gap existed though: certain APIs, including createComponent, lacked a straightforward way to handle familiar tasks like binding to component inputs or wiring up listeners for component events.

The following sections examine how the createComponent function is evolving, while also quickly revisiting the alternative dynamic creation APIs currently available.

The Declarative Option: Exploring NgComponentOutlet

For developers who prefer a template-centric approach, the NgComponentOutlet directive offers a clean path to view dynamic components in action. It's a declarative tool that asks for one crucial input at minimum: the type of component you wish to instantiate.

@Component({ 
  select: 'app-parent',
  template: `
    <ng-container *ngComponentOutlet="dynamicComponent" />
    <button type="button" (click)="loadComponent()">Load</button>`,
  imports: [NgComponentOutlet]
})
export class ParentComponent {
 component: Type<HelloComponent> | null = null;

 async loadComponent(): Promise<void> {
   component = (await import('./hello.component')).HelloComponent;
 }
}
Enter fullscreen mode Exit fullscreen mode

This snippet illustrates the basic template for ngComponentOutlet. A button click triggers the loadComponent method. This is where lazy-loading comes in: the HelloComponent is fetched on-demand, a small but welcome performance boost. Its instance is stored in the component variable.

The directive takes this component instance via its ngComponentOutlet input and proceeds to insert the component's HostView directly into the current view's container.

With the release of Angular version 16, this directive gained a meaningful upgrade. The new ngComponentOutletInputs input allows you to automatically connect and set inputs on the dynamic component right at creation time.

You supply an object where keys correspond to the dynamic component's input names and values to the data you want to pass.

@Component({ 
  template: `Hello {{ name }}`
})
export class HelloComponent {
  name = input('DevTo');
}


@Component({ 
  selector: 'app-parent',
  template: `
    <ng-container *ngComponentOutlet="dynamicComponent; inputs: { 
      name: 'This is Angular' }" />
    <button type="button" (click)="loadComponent()">Load</button>`,
  imports: [NgComponentOutlet]
})
export class ParentComponent {
 component: Type<HelloComponent> | null = null;

 async loadComponent(): Promise<void> {
   component = (await import('./hello.component')).HelloComponent;
 }
}
Enter fullscreen mode Exit fullscreen mode

In this particular example, we're providing a value for the name input of HelloComponent. Once the component's HostView is integrated into the view, the rendered output will be Hello This is Angular.

This declarative approach shines when you know exactly which component you're going to render. However, what happens when the choice is more dynamic? Let's expand the loadComponent method to decide between two components:

  • HelloComponent, which requires a name input
  • AngularComponent, which expects none
... 
 component: Type<HelloComponent | AngularComponent> | null = null;
 isAngular = signal(false);

 async loadComponent(): Promise<void> {
   if(isAngular()) {
     component = (await import('./angular.component')).AngularComponent;
   }else {
     component = (await import('./hello.component')).HelloComponent;
   }
 }
Enter fullscreen mode Exit fullscreen mode

Now, a problem emerges. If we fill our component object with an instance of AngularComponent, the build will fail. Angular's compiler will throw an error, complaining about a missing name input — even though AngularComponent has no use for it whatsoever.

A second constraint exists: the parent component currently has no straightforward way to subscribe to events emitted through the dynamically created child's outputs. This is the limitation that pushes us toward the programmatic createComponent function.

The Programmatic Powerhouse: createComponent Gets an Upgrade

Keep in mind, all of this is based on the angular-next-1 version, so the API may still evolve.

If you're interested in the implementation details, you can look at:

Traditionally, developers who prefer a more functional, less declarative style would turn to the createComponent method available on the ViewContainerRef class. The following code shows how it's been used.

@Component({ 
  select: 'app-parent',
  template: `
    <section #dynamicSection></section>
    <button type="button" (click)="loadComponent()">Load</button>`,
  imports: [NgComponentOutlet]
})
export class ParentComponent {
 dynamicSection = viewChild<ViewContainerRef>('dynamicSection', { read: ViewContainerRef } );

 async loadComponent(): Promise<void> {
   const helloComponent = (await import('./hello.component')).HelloComponent
   this.dynamicSection.createComponent(helloComponent);
 }
}
Enter fullscreen mode Exit fullscreen mode

Here, the template defines a container element with a template reference variable dynamicSection as the intended anchor point. User interaction with the button invokes loadComponent. This method uses createComponent to do a few things at once:

  • Instantiate HelloComponent.
  • Incorporate its host view into the current view.
  • Automatically register the new component with the change detector.

This registration is crucial: it means when the parent component is cleaned up, the dynamic component's onDestroy hook fires correctly, preventing memory leaks.

The result of calling createComponent is a ComponentRef instance. This object has provided the setInput method to get data into the dynamic component. While this was a step forward, it still left one question open: how to listen for and react to the dynamic component's events from the parent?

Angular version 20 changes the game. The updated API for createComponent steps far beyond simple input assignment. Now, developers can:

  • bind values to component inputs
  • connect component outputs to parent methods
  • establish true two-way binding between parent and dynamically created child
  • apply host directives to the dynamic component, enabling the binding of their inputs and outputs as well

This expanded functionality arrives thanks to the broad adoption of standalone components and the central role signals now play in Angular's reactivity model.

@Component({ 
  template: `Hello {{ name }}
    <button type="button" (click)="refresh()">Refresh</button>
  `
})
export class HelloComponent {
  name = input('DevTo');
  refreshName = output<void>();

  refresh(): void {
    this.refreshName.emit()
  }
}

@Component({ 
  select: 'app-parent',
  template: `
    <section #dynamicSection></section>
    <button type="button" (click)="loadComponent()">Load</button>`,
  imports: [NgComponentOutlet]
})
export class ParentComponent {
 name = signal('DevTo');
 dynamicSection = viewChild<ViewContainerRef>('dynamicSection', { read: ViewContainerRef } );

 onRefresh = () => this.name.set('This is Angular');

 async loadComponent(): Promise<void> {
   const helloComponent = (await import('./hello.component')).HelloComponent
   this.dynamicSection.createComponent(helloComponent,{
     bindings: [
       inputBinding('name', this.name),
       outputBinding('refreshName', this.onRefresh),
     ]
   });
 }
}
Enter fullscreen mode Exit fullscreen mode

In this code sample, the new API is used to wire up HelloComponent at the moment of creation. The name input is assigned the string This is Angular, while the refreshName output is connected to the onRefresh method.

Ultimately, this means a user clicking a “Refresh” button inside the dynamic component will trigger the onRefresh callback in the parent.

One important detail: utilize an arrow function for the output handler to ensure it retains the correct execution context.

The new capabilities don't stop with simple component bindings. It's now also feasible to attach directives to the dynamically created component. Let's construct a simple scenario where a directive is tasked with adding a CSS class to the component's host element, turning the entire thing red when a particular input value is active.


@Directive({ 
  selector: '[rainbow]',
  host: {
   '[class.red]'= 'isRed()';
  }
})
export class ColorDirective {
  name = input<string>('');
  isRed = computed(() => this.name() === 'This is Angular');
}

@Component({ 
  template: `Hello {{ name }}
    <button type="button" (click)="refresh()">Refresh</button>
  `
})
export class HelloComponent {
  name = input('DevTo');
  refreshName = output<void>();

  refresh(): void {
    this.refreshName.emit()
  }
}

@Component({ 
  select: 'app-parent',
  template: `
    <section #dynamicSection></section>
    <button type="button" (click)="loadComponent()">Load</button>`,
  imports: [NgComponentOutlet]
})
export class ParentComponent {
 name = signal('DevTo');
 dynamicSection = viewChild<ViewContainerRef>('dynamicSection', { read: ViewContainerRef } );

 onRefresh = () => this.name.set('This is Angular');

 async loadComponent(): Promise<void> {
   const helloComponent = (await import('./hello.component')).HelloComponent
   this.dynamicSection.createComponent(helloComponent,{
     bindings: [
       inputBinding('name', () => 'This is Angular'),
       outputBinding('refreshName', this.onRefresh),
     ],
     directives: [
       { type: ColorDirective,
         bindings: [
           inputBinding('name', this.name),
         ]
       }
     ]
   });
 }
}
Enter fullscreen mode Exit fullscreen mode

The snippet above demonstrates this in action. The ColorDirective is not only applied to the dynamic HelloComponent, but it's also configured by binding its own name input.

This is where signals make the system tick. In this setup, a signal named name typically holds the value DevTo, which computes to a isRed variable of false.

Clicking that refresh button changes things. The onRefresh function kicks in, updating the signal to This is Angular. This recomputes isRed, making it true and dynamically adding the red CSS class to the component's host element.

Wrapping Up: A New Era of Component Flexibility

The path to Angular 20 simplifies dynamic component creation, especially when inputs and outputs are involved. The reimagined createComponent definition brings a welcome degree of freedom and flexibility to the process.

Use cases that require rendering a component based on a set of dynamic conditions become less cumbersome. We can even start to think about the possibilities of "composing our dynamic components" in a more modular fashion. Looking ahead, applying the principles of composition in Angular applications is poised to become an increasingly effortless and standard practice.