Using Dynamic Components in Angular: A Comprehensive Guide to Runtime UI Creation

Dynamic components in Angular allow developers to create and render components at runtime, enabling flexible and adaptive user interfaces that can respond to changing data or user interactions. Unlike static components defined in templates, dynamic components are instantiated programmatically, making them ideal for scenarios like dashboards, modals, or plugin-based systems. This in-depth guide explores how to use dynamic components in Angular, covering the ComponentFactoryResolver, ViewContainerRef, and modern approaches like createComponent. Through a practical example of a task management app with dynamically loaded task detail components, you’ll learn to harness dynamic components to build powerful, modular Angular applications.

What Are Dynamic Components?

Dynamic components are Angular components created and inserted into the DOM at runtime, rather than being declared statically in a template. This is achieved using Angular’s APIs to instantiate components programmatically, allowing you to control when, where, and how components are rendered. Dynamic components are particularly useful when the type or number of components depends on runtime conditions, such as user input, configuration data, or API responses.

For example, a dashboard might load different widget components based on a user’s preferences, or a modal might display varying content based on the context.

Why Use Dynamic Components?

  • Flexibility: Render components based on runtime conditions, enabling adaptive UIs.
  • Modularity: Load only the components needed, improving performance and scalability.
  • Extensibility: Support plugin-like architectures where components are added dynamically.
  • Reusability: Create generic containers that host different components. See [Create Reusable Components](/angular/components/create-reusable-components).
  • Dynamic UIs: Build dashboards, tabbed interfaces, or modals with varying content.

To understand Angular components fundamentally, see Angular Component.

Prerequisites

Before starting, ensure you have: 1. Node.js and npm: Version 16.x or later. Verify with:

node --version
   npm --version
  1. Angular CLI: Install globally:
npm install -g @angular/cli

Check with ng version. See Mastering the Angular CLI. 3. Angular Project: Create one if needed:

ng new task-app

Select Yes for routing and CSS for styling. Navigate to cd task-app. Learn more in Angular Create a New Project. 4. Basic Knowledge: Familiarity with HTML, CSS, JavaScript, and TypeScript. Knowledge of Angular components, services, and directives is helpful. See Angular Tutorial.

Step-by-Step Guide: Using Dynamic Components in Angular

We’ll build a task management application where a task list displays tasks, and clicking a task dynamically loads a detail component to show additional information. We’ll use both the legacy ComponentFactoryResolver approach (for compatibility with older Angular versions) and the modern createComponent API (introduced in Angular 14) for a streamlined experience.

Step 1: Set Up the Angular Project

Create a new project:

ng new task-app
cd task-app
  • Select Yes for routing and CSS for styling.

Step 2: Install Bootstrap (Optional)

To enhance the UI, install Bootstrap:

npm install bootstrap

Update src/styles.css:

@import "~bootstrap/dist/css/bootstrap.min.css";

For detailed setup, see Angular Install and Use Bootstrap.

Step 3: Create Components

We’ll need:

  • A TaskListComponent to display tasks.
  • A TaskDetailComponent to show task details dynamically.
  • A TaskPriorityWidget to demonstrate multiple dynamic components.

Generate the components:

ng generate component task-list
ng generate component task-detail
ng generate component task-priority-widget

Step 4: Define the Task Detail Component

The TaskDetailComponent will display detailed task information.

Update task-detail.component.ts:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-task-detail',
  templateUrl: './task-detail.component.html',
  styleUrls: ['./task-detail.component.css']
})
export class TaskDetailComponent {
  @Input() task: any = {};
}

Update task-detail.component.html:

{ { task.name }}
    Description: { { task.description }}
    Priority: { { task.priority | titlecase }}
    Due: { { task.dueDate | date: 'mediumDate' }}
  • Explanation:
    • @Input() task: Receives task data from the parent.
    • Bootstrap Card: Displays task details with formatting.
    • titlecase and date pipes: Format priority and date. See [Angular Pipes](/angular/pipes/angular-pipes).

Step 5: Define the Task Priority Widget

The TaskPriorityWidget will display a task’s priority with a visual indicator.

Update task-priority-widget.component.ts:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-task-priority-widget',
  templateUrl: './task-priority-widget.component.html',
  styleUrls: ['./task-priority-widget.component.css']
})
export class TaskPriorityWidgetComponent {
  @Input() task: any = {};
}

Update task-priority-widget.component.html:

Priority: { { task.priority | titlecase }}
  • Explanation:
    • [ngClass]: Applies Bootstrap alert classes based on priority. See [Use NgClass in Templates](/angular/directives/use-ng-class-in-templates).
    • Displays a colored alert for the task’s priority.

Step 6: Implement the Task List Component

The TaskListComponent will render a list of tasks and dynamically load TaskDetailComponent or TaskPriorityWidget when a task is selected.

Update task-list.component.ts:

import { Component, ViewChild, ViewContainerRef, ComponentRef } from '@angular/core';
import { TaskDetailComponent } from '../task-detail/task-detail.component';
import { TaskPriorityWidgetComponent } from '../task-priority-widget/task-priority-widget.component';

@Component({
  selector: 'app-task-list',
  templateUrl: './task-list.component.html',
  styleUrls: ['./task-list.component.css']
})
export class TaskListComponent {
  tasks = [
    { id: 1, name: 'Learn Angular', description: 'Study dynamic components', priority: 'high', dueDate: new Date(2025, 6, 1) },
    { id: 2, name: 'Build Task App', description: 'Create a task management app', priority: 'medium', dueDate: new Date(2025, 6, 15) },
    { id: 3, name: 'Deploy Project', description: 'Deploy to production', priority: 'low', dueDate: new Date(2025, 7, 1) }
  ];

  @ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;
  componentRef: ComponentRef | null = null;

  loadComponent(task: any, componentType: string) {
    if (this.container) {
      this.container.clear(); // Remove previous component
      let componentClass: any;
      if (componentType === 'details') {
        componentClass = TaskDetailComponent;
      } else if (componentType === 'priority') {
        componentClass = TaskPriorityWidgetComponent;
      }
      if (componentClass) {
        this.componentRef = this.container.createComponent(componentClass);
        this.componentRef.instance.task = task;
      }
    }
  }

  clearComponent() {
    if (this.container) {
      this.container.clear();
      this.componentRef = null;
    }
  }

  trackById(index: number, task: any) {
    return task.id;
  }
}
  • Explanation:
    • tasks: Array of task objects with id, name, description, priority, and dueDate.
    • @ViewChild: References a ViewContainerRef (a DOM placeholder) where dynamic components are inserted.
    • loadComponent: Creates a TaskDetailComponent or TaskPriorityWidget based on componentType, sets the task input, and inserts it into the container.
    • clearComponent: Removes the dynamic component.
    • trackById: Optimizes ngFor. See [Use NgFor for List Rendering](/angular/directives/use-ngfor-for-list-rendering).

Update task-list.component.html:

Task Manager
  
    
      
        
          { { task.name }}
        
      
      Show Priority Widget
      Clear
  • Key Features:
    • ngFor**: Renders a list of tasks as clickable Bootstrap list group items.
    • [ngClass]: Applies priority-based styles. See [Use NgClass in Templates](/angular/directives/use-ng-class-in-templates).
    • (click): Loads TaskDetailComponent when a task is clicked or TaskPriorityWidget when the button is clicked.
    • : Placeholder for dynamic components, referenced by @ViewChild.
    • Buttons: Load the priority widget or clear the container.
    • Bootstrap Grid: Splits the UI into a task list (4 columns) and detail area (8 columns).

Update task-list.component.css:

.list-group-item {
  margin-bottom: 0.5rem;
}

Step 7: Update the App Module

Ensure all components are declared in app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { TaskListComponent } from './task-list/task-list.component';
import { TaskDetailComponent } from './task-detail/task-detail.component';
import { TaskPriorityWidgetComponent } from './task-priority-widget/task-priority-widget.component';

@NgModule({
  declarations: [
    AppComponent,
    TaskListComponent,
    TaskDetailComponent,
    TaskPriorityWidgetComponent
  ],
  imports: [BrowserModule, AppRoutingModule],
  bootstrap: [AppComponent]
})
export class AppModule {}

Update app.component.html:

Step 8: Test the Modern createComponent API (Angular 14+)

The createComponent API simplifies dynamic component creation, removing the need for ComponentFactoryResolver. Update task-list.component.ts to use createComponent (ensure Angular version is 14 or later):

import { Component, ViewChild, ViewContainerRef, EnvironmentInjector } from '@angular/core';
import { TaskDetailComponent } from '../task-detail/task-detail.component';
import { TaskPriorityWidgetComponent } from '../task-priority-widget/task-priority-widget.component';

@Component({
  selector: 'app-task-list',
  templateUrl: './task-list.component.html',
  styleUrls: ['./task-list.component.css']
})
export class TaskListComponent {
  tasks = [
    { id: 1, name: 'Learn Angular', description: 'Study dynamic components', priority: 'high', dueDate: new Date(2025, 6, 1) },
    { id: 2, name: 'Build Task App', description: 'Create a task management app', priority: 'medium', dueDate: new Date(2025, 6, 15) },
    { id: 3, name: 'Deploy Project', description: 'Deploy to production', priority: 'low', dueDate: new Date(2025, 7, 1) }
  ];

  @ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;

  constructor(private injector: EnvironmentInjector) {}

  loadComponent(task: any, componentType: string) {
    if (this.container) {
      this.container.clear();
      let componentClass: any;
      if (componentType === 'details') {
        componentClass = TaskDetailComponent;
      } else if (componentType === 'priority') {
        componentClass = TaskPriorityWidgetComponent;
      }
      if (componentClass) {
        const componentRef = this.container.createComponent(componentClass, { injector: this.injector });
        componentRef.instance.task = task;
      }
    }
  }

  clearComponent() {
    if (this.container) {
      this.container.clear();
    }
  }

  trackById(index: number, task: any) {
    return task.id;
  }
}
  • Key Changes:
    • Removed ComponentFactoryResolver.
    • Injected EnvironmentInjector for component creation.
    • Used createComponent with the component class and injector.
    • createComponent is simpler and type-safe, ideal for modern Angular apps.

Run the app:

ng serve --open
  • Visit http://localhost:4200 to see the task list:
    • Click a task to load TaskDetailComponent dynamically in the right panel.
    • Click “Show Priority Widget” to load TaskPriorityWidget.
    • Click “Clear” to remove the dynamic component.
    • The UI is responsive, with Bootstrap styling.

Step 9: Add Dynamic Component Configuration

To make the app more flexible, use a service to manage component mappings. Generate a service:

ng generate service component-loader

Update component-loader.service.ts:

import { Injectable, Type } from '@angular/core';
import { TaskDetailComponent } from './task-detail/task-detail.component';
import { TaskPriorityWidgetComponent } from './task-priority-widget/task-priority-widget.component';

@Injectable({
  providedIn: 'root'
})
export class ComponentLoaderService {
  private componentMap: { [key: string]: Type } = {
    details: TaskDetailComponent,
    priority: TaskPriorityWidgetComponent
  };

  getComponent(type: string): Type | null {
    return this.componentMap[type] || null;
  }
}

Update task-list.component.ts:

import { Component, ViewChild, ViewContainerRef, EnvironmentInjector } from '@angular/core';
import { ComponentLoaderService } from '../component-loader.service';

@Component({
  selector: 'app-task-list',
  templateUrl: './task-list.component.html',
  styleUrls: ['./task-list.component.css']
})
export class TaskListComponent {
  tasks = [
    { id: 1, name: 'Learn Angular', description: 'Study dynamic components', priority: 'high', dueDate: new Date(2025, 6, 1) },
    { id: 2, name: 'Build Task App', description: 'Create a task management app', priority: 'medium', dueDate: new Date(2025, 6, 15) },
    { id: 3, name: 'Deploy Project', description: 'Deploy to production', priority: 'low', dueDate: new Date(2025, 7, 1) }
  ];

  @ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;

  constructor(
    private injector: EnvironmentInjector,
    private componentLoader: ComponentLoaderService
  ) {}

  loadComponent(task: any, componentType: string) {
    if (this.container) {
      this.container.clear();
      const componentClass = this.componentLoader.getComponent(componentType);
      if (componentClass) {
        const componentRef = this.container.createComponent(componentClass, { injector: this.injector });
        componentRef.instance.task = task;
      }
    }
  }

  clearComponent() {
    if (this.container) {
      this.container.clear();
    }
  }

  trackById(index: number, task: any) {
    return task.id;
  }
}
  • Explanation:
    • ComponentLoaderService maps component types to strings, making it easy to add new dynamic components.
    • loadComponent uses the service to resolve the component class.

Step 10: Test the Application

Run unit tests:

ng test
  • Test TaskListComponent:
  • import { ComponentFixture, TestBed } from '@angular/core/testing';
      import { TaskListComponent } from './task-list.component';
      import { ComponentLoaderService } from '../component-loader.service';
      import { TaskDetailComponent } from '../task-detail/task-detail.component';
      import { TaskPriorityWidgetComponent } from '../task-priority-widget/task-priority-widget.component';
    
      describe('TaskListComponent', () => {
        let component: TaskListComponent;
        let fixture: ComponentFixture;
    
        beforeEach(async () => {
          await TestBed.configureTestingModule({
            declarations: [TaskListComponent, TaskDetailComponent, TaskPriorityWidgetComponent],
            providers: [ComponentLoaderService]
          }).compileComponents();
        });
    
        beforeEach(() => {
          fixture = TestBed.createComponent(TaskListComponent);
          component = fixture.componentInstance;
          fixture.detectChanges();
        });
    
        it('should load TaskDetailComponent dynamically', () => {
          component.loadComponent(component.tasks[0], 'details');
          fixture.detectChanges();
          const detail = fixture.nativeElement.querySelector('app-task-detail');
          expect(detail).toBeTruthy();
        });
    
        it('should clear dynamic component', () => {
          component.loadComponent(component.tasks[0], 'details');
          component.clearComponent();
          fixture.detectChanges();
          const detail = fixture.nativeElement.querySelector('app-task-detail');
          expect(detail).toBeNull();
        });
      });
  • Learn more in [Test Components with Jasmine](/angular/testing/test-components-with-jasmine).

Best Practices for Dynamic Components

  • Use Modern APIs: Prefer createComponent (Angular 14+) over ComponentFactoryResolver for simplicity and type safety.
  • Centralize Component Mapping: Use a service like ComponentLoaderService to manage component types, improving maintainability.
  • Clean Up Resources: Call container.clear() or destroy component references to prevent memory leaks.
  • Type Safety: Define interfaces for component inputs to ensure correct data passing:
  • interface TaskComponent {
        task: any;
      }
  • Optimize Performance: Avoid creating excessive dynamic components in loops; use *ngIf or lazy loading for conditional rendering. See [Optimize Change Detection](/angular/advanced/optimize-change-detection).
  • Test Dynamic Behavior: Write unit tests to verify component creation, input binding, and destruction.

Advanced Dynamic Component Techniques

Lazy Loading Components

Load components dynamically with lazy modules for better performance:

loadComponent(task: any) {
  import('./task-detail/task-detail.component').then(({ TaskDetailComponent }) => {
    const componentRef = this.container.createComponent(TaskDetailComponent, { injector: this.injector });
    componentRef.instance.task = task;
  });
}
  • See [Angular Lazy Loading](/angular/routing/angular-lazy-loading).

Dynamic Component Inputs and Outputs

Bind outputs dynamically:

const componentRef = this.container.createComponent(TaskDetailComponent, { injector: this.injector });
componentRef.instance.task = task;
componentRef.instance.someEvent.subscribe((data: any) => console.log(data));

Component Libraries

Package dynamic components in a library for reuse across projects. See Create Component Libraries.

Troubleshooting Common Issues

  • Component Not Rendering:
    • Ensure @ViewChild is correctly set up (#container and { read: ViewContainerRef }).
    • Verify the component is declared in the module.
  • Cannot read property 'createComponent':
    • Check Angular version (14+ for createComponent) or use ComponentFactoryResolver for older versions.
    • Ensure container is initialized (use *ngIf or ngAfterViewInit).
  • Input Not Updating:
    • Set inputs after component creation: componentRef.instance.task = task.
    • Trigger change detection with componentRef.changeDetectorRef.detectChanges().
  • Memory Leaks:
    • Call container.clear() when removing components.
    • Destroy component references in ngOnDestroy.
  • Styles Not Applied:
    • Ensure Bootstrap or component styles are included. See [Use View Encapsulation](/angular/components/use-view-encapsulation).

FAQs

What are dynamic components in Angular?

Dynamic components are created and rendered at runtime using APIs like createComponent or ComponentFactoryResolver, allowing flexible UI generation based on data or user actions.

When should I use dynamic components?

Use dynamic components for UIs that change at runtime, such as dashboards, modals, or plugin systems, where the component type or number is determined dynamically.

What’s the difference between createComponent and ComponentFactoryResolver?

createComponent (Angular 14+) is a simpler, type-safe API for creating components, while ComponentFactoryResolver is the legacy approach for older Angular versions.

How do I optimize dynamic components?

Clear containers to prevent memory leaks, use trackBy in *ngFor, and consider lazy loading for large components. See Optimize Change Detection.

Why isn’t my dynamic component rendering?

Ensure the ViewContainerRef is set up, the component is declared, and createComponent is called after the view is initialized (e.g., in ngAfterViewInit).

Conclusion

Dynamic components in Angular empower developers to build flexible, runtime-driven user interfaces that adapt to diverse requirements. This guide has shown you how to create dynamic components using both createComponent and ComponentFactoryResolver, implement them in a task management app, and optimize with a service-based approach. By following best practices and leveraging Angular’s powerful APIs, you can craft modular, scalable applications that deliver dynamic, engaging user experiences. With these skills, you’re ready to explore advanced dynamic component scenarios, integrate lazy loading, or build extensible UI systems.

Start using dynamic components in your Angular projects today, and unlock the potential of runtime UI creation!