Lesson 17-Angular Dynamic Components and Content Projection

Dynamic Components

Dynamic component loading in Angular is an advanced feature that allows you to decide which components to render at runtime. This is useful in scenarios such as displaying different interfaces based on user permissions, rendering components based on data results, or implementing modular loading to improve performance.

Preparation

Ensure you have an Angular environment. If not, create a new project using Angular CLI:

ng new dynamic-components-demo
cd dynamic-components-demo

Creating Components

To demonstrate dynamic component loading, create three components:

ng generate component component-a
ng generate component component-b
ng generate component component-c

Creating a Container Component

Dynamic components typically require a container component to manage loading and unloading. Create a container component:

ng generate component container

In container.component.html, add a placeholder for dynamically loaded components:

<!-- container.component.html -->
<div #dynamicContainer></div>

Using ViewContainerRef

ViewContainerRef is a key Angular interface for dynamically inserting and removing views. In container.component.ts, inject ViewContainerRef and create a method to load components dynamically:

import { Component, ViewContainerRef, ComponentFactoryResolver, OnInit } from '@angular/core';
import { ComponentAComponent } from '../component-a/component-a.component';
import { ComponentBComponent } from '../component-b/component-b.component';
import { ComponentCComponent } from '../component-c/component-c.component';

@Component({
  selector: 'app-container',
  templateUrl: './container.component.html',
  styleUrls: ['./container.component.css']
})
export class ContainerComponent implements OnInit {
  constructor(
    private viewContainerRef: ViewContainerRef,
    private componentFactoryResolver: ComponentFactoryResolver
  ) {}

  ngOnInit(): void {
    this.loadComponent('ComponentAComponent');
  }

  loadComponent(componentName: string): void {
    let componentFactory;
    switch (componentName) {
      case 'ComponentAComponent':
        componentFactory = this.componentFactoryResolver.resolveComponentFactory(ComponentAComponent);
        break;
      case 'ComponentBComponent':
        componentFactory = this.componentFactoryResolver.resolveComponentFactory(ComponentBComponent);
        break;
      case 'ComponentCComponent':
        componentFactory = this.componentFactoryResolver.resolveComponentFactory(ComponentCComponent);
        break;
      default:
        throw new Error(`Unknown component: ${componentName}`);
    }

    this.viewContainerRef.clear(); // Clear existing components
    const componentRef = this.viewContainerRef.createComponent(componentFactory);
  }
}

Ensure you import the components (ComponentAComponent, ComponentBComponent, ComponentCComponent) as shown.

Dynamically Loading Components

The loadComponent method enables dynamic loading of components based on user actions or data results.

Dynamic Component Loading with Routing

Another common approach is using Angular routing for dynamic component loading. In app-routing.module.ts, define routes for dynamic loading:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ContainerComponent } from './container/container.component';

const routes: Routes = [
  { path: 'component-a', component: ContainerComponent, data: { component: 'ComponentAComponent' } },
  { path: 'component-b', component: ContainerComponent, data: { component: 'ComponentBComponent' } },
  { path: 'component-c', component: ContainerComponent, data: { component: 'ComponentCComponent' } },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

In container.component.ts, use route data to determine which component to load:

import { Component, ViewContainerRef, ComponentFactoryResolver, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ComponentAComponent } from '../component-a/component-a.component';
import { ComponentBComponent } from '../component-b/component-b.component';
import { ComponentCComponent } from '../component-c/component-c.component';

@Component({
  selector: 'app-container',
  templateUrl: './container.component.html',
  styleUrls: ['./container.component.css']
})
export class ContainerComponent implements OnInit {
  constructor(
    private route: ActivatedRoute,
    private viewContainerRef: ViewContainerRef,
    private componentFactoryResolver: ComponentFactoryResolver
  ) {}

  ngOnInit(): void {
    this.route.data.subscribe(data => {
      this.loadComponent(data['component']);
    });
  }

  loadComponent(componentName: string): void {
    let componentFactory;
    switch (componentName) {
      case 'ComponentAComponent':
        componentFactory = this.componentFactoryResolver.resolveComponentFactory(ComponentAComponent);
        break;
      case 'ComponentBComponent':
        componentFactory = this.componentFactoryResolver.resolveComponentFactory(ComponentBComponent);
        break;
      case 'ComponentCComponent':
        componentFactory = this.componentFactoryResolver.resolveComponentFactory(ComponentCComponent);
        break;
      default:
        throw new Error(`Unknown component: ${componentName}`);
    }

    this.viewContainerRef.clear();
    const componentRef = this.viewContainerRef.createComponent(componentFactory);
  }
}

Using Lazy Loading

Lazy loading is a performance optimization technique that loads modules or components only when needed. In the routing configuration, use loadChildren for lazy loading:

const routes: Routes = [
  { path: 'lazy', loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule) },
];

In lazy.module.ts, define the lazy-loaded component:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LazyComponent } from './lazy.component';

@NgModule({
  declarations: [LazyComponent],
  imports: [
    CommonModule,
  ],
  exports: [LazyComponent]
})
export class LazyModule { }

Summary

Dynamic component loading is a powerful and flexible Angular feature, enabling complex UI scenarios. Using ViewContainerRef and ComponentFactoryResolver, you can load and unload components at runtime. Combined with Angular routing and lazy loading, you can further optimize application performance and user experience.

Content Projection with ng-content

Content projection is a key Angular feature that allows parent components to inject arbitrary content into child components. This enhances component flexibility and reusability, as components can accept external content without knowing its specifics. The ng-content directive facilitates content projection in Angular.

Basic Concepts

Content projection involves inserting content from a parent component into a child component’s template, typically using the ng-content directive in the child’s template.

Using ng-content

The ng-content directive selects content to project. In a child component’s template, the <ng-content> tag indicates where projected content should be inserted.

For example, create a HighlightBoxComponent with a styled box to display arbitrary content:

<!-- highlight-box.component.html -->
<div class="highlight-box">
  <ng-content></ng-content>
</div>

In the parent component, use HighlightBoxComponent:

<!-- parent.component.html -->
<app-highlight-box>
  <p>This is a paragraph.</p>
  <button>Click me</button>
</app-highlight-box>

Multiple ng-content Tags

Use multiple ng-content tags to organize different content from the parent component, distinguished by the select attribute.

Modify HighlightBoxComponent to accept a title and body content:

<!-- highlight-box.component.html -->
<div class="highlight-box">
  <h2><ng-content select="h2"></ng-content></h2>
  <div><ng-content></ng-content></div>
</div>

In the parent component:

<!-- parent.component.html -->
<app-highlight-box>
  <h2>Title</h2>
  <p>This is the body content.</p>
</app-highlight-box>

Controlling Projection with *ngIf

Conditionally display projected content using *ngIf on ng-content:

<!-- highlight-box.component.html -->
<div class="highlight-box">
  <ng-content *ngIf="showContent"></ng-content>
</div>

Control showContent in the component class:

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

@Component({
  selector: 'app-highlight-box',
  templateUrl: './highlight-box.component.html',
})
export class HighlightBoxComponent {
  showContent = true;
}

Styling and Attributes for Projected Content

Projected content inherits styles and attributes from the parent component. To apply child component styles to projected content, use the ::ng-deep pseudo-class:

/* highlight-box.component.css */
.highlight-box {
  background-color: yellow;
}

/* Apply styles to projected content */
.highlight-box ::ng-deep p {
  color: red;
}

Order of Projected Content

Projected content is inserted in the order it appears in the parent component. Multiple ng-content tags are filled sequentially based on this order.

Projecting with ng-template

Use ng-template for projecting complex structures:

<!-- parent.component.html -->
<app-highlight-box>
  <ng-template #title>
    <h2>Title</h2>
  </ng-template>
  <ng-template #content>
    <p>This is the body content.</p>
  </ng-template>
</app-highlight-box>

In the child component:

<!-- highlight-box.component.html -->
<div class="highlight-box">
  <ng-content select="[title]"></ng-content>
  <ng-content select="[content]"></ng-content>
</div>

Limitations of Projection

While ng-content is powerful, it has limitations, such as not supporting event bindings or two-way data binding for projected content. Use input properties or custom events to address these limitations.

Share your love