Introduction
Welcome to Chapter 13! In this chapter, we’re diving deep into the art of building flexible and powerful user interfaces in Angular using advanced component techniques. We’ll explore three essential concepts: Component Composition, Content Projection, and Dynamic Component Loading. These patterns are crucial for creating reusable, maintainable, and scalable applications, especially as your projects grow in complexity.
Why do these matter? Imagine building a complex application like an e-commerce site or a dashboard. You wouldn’t want to rewrite the same “card” or “modal” UI element every time, right? Component composition allows you to break down your UI into smaller, manageable pieces. Content projection empowers you to create generic “wrapper” components that can host any content, making them incredibly versatile. And dynamic component loading opens doors to building highly adaptive UIs, where components are rendered on the fly based on user actions or data, without being hardcoded into templates.
By the end of this chapter, you’ll not only understand what these techniques are but also why they are indispensable in modern Angular development using standalone components. We’ll walk through step-by-step implementations, tackle practical challenges, and equip you with the knowledge to debug common issues, ensuring you can apply these patterns with confidence in your own production-ready applications.
Before we begin, ensure you have a basic understanding of Angular components, standalone architecture, and @Input() / @Output() decorators. If you’re new to standalone components, a quick review of how they simplify module management by allowing components, directives, and pipes to directly import their dependencies might be helpful.
Core Concepts
Let’s break down these powerful concepts one by one, understanding their purpose and how they fit into the bigger picture of robust Angular applications.
What is Component Composition?
At its heart, component composition is about building complex user interfaces by combining smaller, simpler, and more focused components. Think of it like building with LEGO bricks: you start with individual bricks (small components), and then you combine them to create larger structures (complex UI features).
Why it matters:
- Modularity: Each component focuses on a single responsibility, making your codebase easier to understand and manage.
- Reusability: Small, well-defined components can be reused across different parts of your application, reducing code duplication.
- Maintainability: Changes to one component are less likely to break others, simplifying updates and bug fixes.
- Testability: Smaller components are easier to unit test in isolation.
How it works:
In Angular, composition primarily involves creating parent-child relationships between components. A parent component’s template directly includes the selector of its child components. Data flows from parent to child using @Input() properties, and events flow from child to parent using @Output() event emitters.
Example: A User Profile
Imagine a UserProfileComponent. Instead of putting all the user details, avatar, and contact info directly into one massive component, we can compose it from smaller components:
UserProfileComponent(parent)UserAvatarComponent(child: displays the user’s profile picture)UserDetailsComponent(child: displays name, email, etc.)UserContactInfoComponent(child: displays phone, address)
This approach makes each part of the profile easier to develop, test, and update independently.
What is Content Projection (<ng-content>)?
Content projection is a powerful Angular technique that allows you to “project” content from a parent component into a designated placeholder within a child component’s template. It’s like having a special “slot” in your child component where the parent can insert whatever HTML or Angular components it wants.
Why it matters:
- Flexibility & Genericity: It enables you to create highly flexible, generic wrapper components that don’t hardcode their inner content. Think of a generic
ModalComponent,CardComponent, orLayoutComponent. The wrapper provides the structure and styling, while the parent dictates what goes inside. - Separation of Concerns: The wrapper component focuses on its layout and behavior, while the projected content focuses on its specific data and logic.
- Enhanced Reusability: A single
CardComponentcan be used to display user profiles, product details, news articles, or anything else, simply by projecting different content into it.
How it works:
You define projection slots in a child component’s template using the <ng-content> element.
- Single-slot projection: If you have just one
<ng-content>tag, all content passed by the parent is projected into that single slot. - Multi-slot projection: You can define multiple slots using the
selectattribute on<ng-content>. The parent then uses specific element selectors (like[header],[body], or[footer]) to target which content goes into which slot.
Example: A Generic Card Component
Consider a CardComponent. Its job is to provide a consistent visual container with a border, shadow, and padding. But what goes inside the card? That’s up to the parent component!
<!-- card.component.html -->
<div class="card">
<div class="card-header">
<ng-content select="[cardHeader]"></ng-content>
</div>
<div class="card-body">
<ng-content></ng-content> <!-- Default slot for main content -->
</div>
<div class="card-footer">
<ng-content select="[cardFooter]"></ng-content>
</div>
</div>
The parent component using this CardComponent would then specify its content like this:
<!-- app.component.html -->
<app-card>
<h2 cardHeader>Product Title</h2>
<p>This is the main product description.</p>
<button cardFooter>Add to Cart</button>
</app-card>
Notice how cardHeader and cardFooter are attributes on the elements that are “projected,” matching the select attribute in the ng-content tags. The <p> tag without a specific select attribute goes into the default <ng-content> slot.
What is Dynamic Component Loading (ViewContainerRef)?
Dynamic component loading is the process of creating and adding components to the DOM at runtime, rather than declaring them statically in a component’s template. This means the component’s selector isn’t explicitly written in your HTML; instead, you programmatically instantiate it using TypeScript.
Why it matters:
- Advanced UI Scenarios: Essential for building highly dynamic interfaces like:
- Plugin Architectures: Loading different UI plugins based on user configuration.
- Modals/Dialogs: Creating modal windows on demand without having them always present in the DOM.
- Notification Systems: Showing various types of alerts or toasts.
- Dynamic Forms: Building forms where fields are generated based on metadata from an API.
- A/B Testing: Dynamically switching between different component versions.
- Performance Optimization: Only loading components when they are actually needed can reduce initial bundle size and improve application startup time.
- Flexibility: The specific component to load might not be known until runtime, based on data or user interaction.
How it works:
The key player here is ViewContainerRef. This object represents a container where one or more views can be attached. A view can be a component, a structural directive’s template, or just a plain HTML element.
- Identify a Host Element: You need a place in your template where the dynamic component will be inserted. This is typically an
<ng-template>element or a regular HTML element. - Get
ViewContainerRef: You injectViewContainerRefinto your component or directive. If you want to target a specific host element, you’ll create a custom directive that getsViewContainerReffrom its host element. - Create the Component: Using
viewContainerRef.createComponent(), you can instantiate an Angular component class. This method returns aComponentRefinstance, which gives you access to the created component’s instance, host element, and lifecycle hooks. - Manage the Component: You can interact with the dynamically created component by setting its
@Input()properties, subscribing to its@Output()events, and eventually destroying it usingcomponentRef.destroy()to prevent memory leaks.
Example: A Dynamic Alert System
Imagine a scenario where you want to show different types of alerts (success, error, warning) based on an API response. You don’t want all alert components cluttering your AppComponent template, visible or hidden. Instead, you dynamically inject the correct alert component when needed.
This diagram illustrates how a user action can trigger an API call, and based on its success or error, a specific alert component is dynamically loaded, displayed, and then removed.
Step-by-Step Implementation
Let’s get our hands dirty and implement these concepts in a standalone Angular application.
Setup: Creating Our Standalone Project
First, make sure you have the Angular CLI installed. We’ll create a new project using the latest stable Angular version (as of 2026-02-11, this would be Angular v17.x.x or later, which defaults to standalone components).
Open your terminal or command prompt.
Create a new Angular project:
ng new angular-composition-app --standalone --strict --style=scss--standalone: Ensures the project is configured without NgModules.--strict: Enables strict type-checking and other best practices.--style=scss: Sets up Sass for styling (optional, but good practice).- When prompted, choose routing if you like, but for this chapter, we won’t heavily rely on it.
Navigate into your new project directory:
cd angular-composition-appStart the development server:
ng serve -oThis will compile your application and open it in your browser, usually at
http://localhost:4200.
Part 1: Component Composition (Review & Setup)
Let’s quickly create a simple parent-child component structure to refresh our understanding of basic composition.
Step 1.1: Create UserAvatarComponent
This component will display a user’s avatar image and name.
ng g c components/user-avatar --standalone --inline-template --inline-style
--standalone: Creates a standalone component.--inline-template: Puts the HTML template directly in the.tsfile.--inline-style: Puts the CSS directly in the.tsfile.
Now, open src/app/components/user-avatar/user-avatar.component.ts and modify it:
// src/app/components/user-avatar/user-avatar.component.ts
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common'; // Needed for ngIf/ngFor if used
@Component({
selector: 'app-user-avatar',
standalone: true,
imports: [CommonModule], // Add CommonModule for basic directives
template: `
<div class="avatar-container">
<img [src]="imageUrl" alt="User Avatar" class="avatar-img" />
<span class="avatar-name">{{ userName }}</span>
</div>
`,
styles: `
.avatar-container {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
border: 1px solid #eee;
border-radius: 8px;
background-color: #f9f9f9;
}
.avatar-img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #007bff;
}
.avatar-name {
font-weight: bold;
color: #333;
}
`
})
export class UserAvatarComponent {
@Input() imageUrl: string = 'https://via.placeholder.com/40'; // Default image
@Input() userName: string = 'Guest User'; // Default name
}
- Explanation:
- We import
ComponentandInputfrom@angular/core. @Input()decorators markimageUrlanduserNameas properties that can receive data from a parent component.CommonModuleis imported because even standalone components need to explicitly import modules for common directives likengIf,ngFor, etc.
- We import
Step 1.2: Use UserAvatarComponent in AppComponent
Now, let’s include our UserAvatarComponent in the main AppComponent.
Open src/app/app.component.ts and modify it:
// src/app/app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { UserAvatarComponent } from './components/user-avatar/user-avatar.component'; // Import it!
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, UserAvatarComponent], // Add UserAvatarComponent here
template: `
<main class="app-container">
<h1>Welcome to Angular Composition!</h1>
<h2>Basic Component Composition</h2>
<p>Below is an example of composing a user avatar component:</p>
<app-user-avatar
imageUrl="https://i.pravatar.cc/150?img=68"
userName="Alice Wonderland"
></app-user-avatar>
<app-user-avatar
imageUrl="https://i.pravatar.cc/150?img=32"
userName="Bob The Builder"
></app-user-avatar>
<app-user-avatar></app-user-avatar> <!-- Using default values -->
<!-- Other sections will go here -->
</main>
<router-outlet />
`,
styles: `
.app-container {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
max-width: 800px;
margin: 20px auto;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
h1 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
}
h2 {
color: #34495e;
border-bottom: 1px solid #eceff1;
padding-bottom: 10px;
margin-top: 40px;
}
p {
color: #555;
line-height: 1.6;
}
app-user-avatar {
margin-bottom: 15px;
display: block; /* Ensure each avatar takes its own line */
}
`
})
export class AppComponent {
title = 'angular-composition-app';
}
- Explanation:
- We import
UserAvatarComponentand add it to theimportsarray ofAppComponent. This is crucial for standalone components to use other standalone components. - In the template, we use the
<app-user-avatar>selector and bind data to itsimageUrlanduserNameinputs.
- We import
You should now see three user avatars displayed in your browser, demonstrating basic component composition.
Part 2: Content Projection
Now let’s build a reusable CardComponent that uses content projection to host different types of content.
Step 2.1: Create CardComponent
ng g c components/card --standalone --inline-template --inline-style
Open src/app/components/card/card.component.ts and modify it:
// src/app/components/card/card.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-card',
standalone: true,
imports: [], // No specific Angular modules needed for ng-content itself
template: `
<div class="card">
<div class="card-header">
<ng-content select="[cardHeader]"></ng-content>
</div>
<div class="card-body">
<ng-content></ng-content> <!-- Default slot for main content -->
</div>
<div class="card-footer">
<ng-content select="[cardFooter]"></ng-content>
</div>
</div>
`,
styles: `
.card {
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
overflow: hidden;
background-color: #fff;
}
.card-header {
background-color: #f8f9fa;
padding: 15px 20px;
border-bottom: 1px solid #eee;
font-size: 1.2em;
font-weight: 600;
color: #333;
}
.card-body {
padding: 20px;
color: #555;
line-height: 1.6;
}
.card-footer {
background-color: #f8f9fa;
padding: 10px 20px;
border-top: 1px solid #eee;
text-align: right;
}
`
})
export class CardComponent {}
- Explanation:
- We define three
<ng-content>slots. - One is a default slot (without
select), which will capture any content not explicitly targeted. - Two are named slots using
select="[cardHeader]"andselect="[cardFooter]"to project content based on attributes.
- We define three
Step 2.2: Use CardComponent in AppComponent
Now, let’s use our flexible CardComponent to display various types of content.
Open src/app/app.component.ts and add CardComponent to its imports array. Then, add the following to its template, below the app-user-avatar examples:
// src/app/app.component.ts (partial)
import { CardComponent } from './components/card/card.component'; // Add this import
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, UserAvatarComponent, CardComponent], // Add CardComponent here
template: `
<!-- ... existing content ... -->
<h2>Content Projection with Card Component</h2>
<p>Using a generic card component to project different content sections:</p>
<!-- Example 1: Basic content projection -->
<app-card>
<h3 cardHeader>Simple Blog Post</h3>
<p>This is the main content of a blog post within the card. It's concise and to the point.</p>
<button cardFooter>Read More</button>
</app-card>
<!-- Example 2: Projecting another component -->
<app-card>
<h3 cardHeader>User Profile Card</h3>
<app-user-avatar
imageUrl="https://i.pravatar.cc/150?img=45"
userName="Jane Doe"
></app-user-avatar>
<p>Jane is a passionate software engineer specializing in Angular development.</p>
<button cardFooter>View Profile</button>
</app-card>
<!-- Example 3: Card with only default content -->
<app-card>
<p>This card only has default body content. No specific header or footer was projected.</p>
<small>Created on Feb 11, 2026</small>
</app-card>
<!-- Other sections will go here -->
`,
// ... existing styles ...
})
export class AppComponent {
// ...
}
- Explanation:
- We import
CardComponentand add it toAppComponent’simports. - We use
<app-card>multiple times. - For the first two examples, we project content into the
cardHeader, default, andcardFooterslots using the[cardHeader]and[cardFooter]attributes. Notice how we even project anapp-user-avatarcomponent into the body of the second card! - The third example shows how content without specific attributes goes into the default
<ng-content>slot.
- We import
You should now see three distinct cards in your browser, each with different projected content, demonstrating the power of <ng-content>.
Mini-Challenge 1: Add a “Subtitle” Slot to the Card
Challenge: Modify the CardComponent to include an optional “subtitle” slot that appears right below the main cardHeader. Then, update one of your app-card usages in AppComponent to project a subtitle.
Hint: Think about how you defined the cardHeader and cardFooter slots. You’ll need another <ng-content> with a unique select attribute.
What to observe/learn: This exercise reinforces your understanding of multi-slot content projection and how to design flexible component interfaces.
Part 3: Dynamic Component Loading
Now for the exciting part: dynamically loading components at runtime. We’ll create a simple alert system.
Step 3.1: Create a DynamicHostDirective
This directive will mark the spot in our template where dynamic components will be inserted. It will inject ViewContainerRef.
ng g d directives/dynamic-host --standalone
Open src/app/directives/dynamic-host/dynamic-host.directive.ts and modify it:
// src/app/directives/dynamic-host/dynamic-host.directive.ts
import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appDynamicHost]', // Our custom attribute selector
standalone: true,
})
export class DynamicHostDirective {
constructor(public viewContainerRef: ViewContainerRef) {
console.log('DynamicHostDirective initialized');
}
}
- Explanation:
- We define a simple directive
appDynamicHost. - The constructor injects
ViewContainerRefand makes itpublic. This is the key! By making it public, we can access thisviewContainerRefinstance from the component that uses this directive.ViewContainerRefis where we’ll programmatically add our components.
- We define a simple directive
Step 3.2: Create AlertSuccessComponent and AlertErrorComponent
These will be the components we dynamically load. They’ll be simple alert messages.
ng g c components/alert-success --standalone --inline-template --inline-style
ng g c components/alert-error --standalone --inline-template --inline-style
AlertSuccessComponent:
Open src/app/components/alert-success/alert-success.component.ts and modify it:
// src/app/components/alert-success/alert-success.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-alert-success',
standalone: true,
imports: [],
template: `
<div class="alert success-alert">
<p>{{ message }}</p>
<button (click)="onClose()" class="close-btn">Dismiss</button>
</div>
`,
styles: `
.alert {
padding: 15px;
margin: 10px 0;
border-radius: 5px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9em;
}
.success-alert {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.close-btn {
background: none;
border: none;
color: #155724;
font-weight: bold;
cursor: pointer;
padding: 5px 10px;
border-radius: 3px;
}
.close-btn:hover {
background-color: rgba(21, 87, 36, 0.1);
}
`
})
export class AlertSuccessComponent {
@Input() message: string = 'Operation completed successfully!';
@Output() close = new EventEmitter<void>();
onClose() {
this.close.emit();
}
}
- Explanation:
- It takes an
messageas an@Input(). - It emits a
closeevent via@Output()when the dismiss button is clicked.
- It takes an
AlertErrorComponent:
Open src/app/components/alert-error/alert-error.component.ts and modify it:
// src/app/components/alert-error/alert-error.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-alert-error',
standalone: true,
imports: [],
template: `
<div class="alert error-alert">
<p>{{ message }}</p>
<button (click)="onClose()" class="close-btn">Dismiss</button>
</div>
`,
styles: `
.alert {
padding: 15px;
margin: 10px 0;
border-radius: 5px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9em;
}
.error-alert {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.close-btn {
background: none;
border: none;
color: #721c24;
font-weight: bold;
cursor: pointer;
padding: 5px 10px;
border-radius: 3px;
}
.close-btn:hover {
background-color: rgba(114, 28, 36, 0.1);
}
`
})
export class AlertErrorComponent {
@Input() message: string = 'An unexpected error occurred.';
@Output() close = new EventEmitter<void>();
onClose() {
this.close.emit();
}
}
- Explanation: Similar to
AlertSuccessComponentbut with different styling and default message.
Step 3.3: Implement Dynamic Loading in AppComponent
Now, we’ll put it all together in AppComponent to dynamically load these alert components.
Open src/app/app.component.ts. We need to:
- Import
DynamicHostDirective,AlertSuccessComponent, andAlertErrorComponent. - Add
DynamicHostDirectiveto theimportsarray. - Add a template reference variable (
#dynamicHost) to an<ng-template>element where we want to insert components. - Use
@ViewChild()to get a reference to ourDynamicHostDirective(and thus itsViewContainerRef). - Create methods to load the components dynamically.
// src/app/app.component.ts (full updated code)
import { Component, ViewChild, ViewContainerRef, ComponentRef, OnDestroy } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { CommonModule } from '@angular/common'; // Important for ngIf if needed
import { UserAvatarComponent } from './components/user-avatar/user-avatar.component';
import { CardComponent } from './components/card/card.component';
import { DynamicHostDirective } from './directives/dynamic-host/dynamic-host.directive'; // Import directive
import { AlertSuccessComponent } from './components/alert-success/alert-success.component'; // Import dynamic components
import { AlertErrorComponent } from './components/alert-error/alert-error.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [
CommonModule, // For ngIf
RouterOutlet,
UserAvatarComponent,
CardComponent,
DynamicHostDirective // Import the directive
],
template: `
<main class="app-container">
<h1>Welcome to Angular Composition!</h1>
<h2>Basic Component Composition</h2>
<p>Below is an example of composing a user avatar component:</p>
<app-user-avatar
imageUrl="https://i.pravatar.cc/150?img=68"
userName="Alice Wonderland"
></app-user-avatar>
<app-user-avatar
imageUrl="https://i.pravatar.cc/150?img=32"
userName="Bob The Builder"
></app-user-avatar>
<app-user-avatar></app-user-avatar> <!-- Using default values -->
<h2>Content Projection with Card Component</h2>
<p>Using a generic card component to project different content sections:</p>
<!-- Example 1: Basic content projection -->
<app-card>
<h3 cardHeader>Simple Blog Post</h3>
<span cardSubtitle>Posted by Admin on Feb 10, 2026</span> <!-- Projected subtitle -->
<p>This is the main content of a blog post within the card. It's concise and to the point.</p>
<button cardFooter>Read More</button>
</app-card>
<!-- Example 2: Projecting another component -->
<app-card>
<h3 cardHeader>User Profile Card</h3>
<app-user-avatar
imageUrl="https://i.pravatar.cc/150?img=45"
userName="Jane Doe"
></app-user-avatar>
<p>Jane is a passionate software engineer specializing in Angular development.</p>
<button cardFooter>View Profile</button>
</app-card>
<!-- Example 3: Card with only default content -->
<app-card>
<p>This card only has default body content. No specific header or footer was projected.</p>
<small>Created on Feb 11, 2026</small>
</app-card>
<h2>Dynamic Component Loading</h2>
<p>Click the buttons to dynamically load alert messages:</p>
<div class="dynamic-controls">
<button (click)="loadSuccessAlert()">Show Success Alert</button>
<button (click)="loadErrorAlert()">Show Error Alert</button>
</div>
<!-- This is our host element for dynamic components -->
<ng-template appDynamicHost></ng-template>
</main>
<router-outlet />
`,
styles: `
.app-container {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
max-width: 800px;
margin: 20px auto;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
h1 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
}
h2 {
color: #34495e;
border-bottom: 1px solid #eceff1;
padding-bottom: 10px;
margin-top: 40px;
}
p {
color: #555;
line-height: 1.6;
}
app-user-avatar {
margin-bottom: 15px;
display: block; /* Ensure each avatar takes its own line */
}
.dynamic-controls {
display: flex;
gap: 15px;
margin-top: 20px;
margin-bottom: 20px;
}
.dynamic-controls button {
padding: 10px 20px;
font-size: 1em;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.dynamic-controls button:first-child {
background-color: #28a745;
color: white;
}
.dynamic-controls button:first-child:hover {
background-color: #218838;
}
.dynamic-controls button:last-child {
background-color: #dc3545;
color: white;
}
.dynamic-controls button:last-child:hover {
background-color: #c82333;
}
`
})
export class AppComponent implements OnDestroy {
title = 'angular-composition-app';
private currentAlertComponentRef: ComponentRef<any> | null = null; // To keep track of the loaded component
// @ViewChild takes the directive type or its selector.
// { static: true } is used if you need to access the element in ngOnInit.
// { static: false } (default) is used if the element is inside an ngIf or ngFor and might not be available until after change detection.
@ViewChild(DynamicHostDirective, { static: true })
dynamicHost!: DynamicHostDirective;
loadSuccessAlert() {
this.clearAlert(); // Clear any existing alert first
const viewContainerRef = this.dynamicHost.viewContainerRef;
viewContainerRef.clear(); // Clear the view container before adding a new component
// Create the component
this.currentAlertComponentRef = viewContainerRef.createComponent(AlertSuccessComponent);
// Set inputs (if any)
this.currentAlertComponentRef.instance.message = 'Data saved successfully to the server!';
// Subscribe to outputs (if any)
this.currentAlertComponentRef.instance.close.subscribe(() => {
this.clearAlert();
});
}
loadErrorAlert() {
this.clearAlert(); // Clear any existing alert first
const viewContainerRef = this.dynamicHost.viewContainerRef;
viewContainerRef.clear(); // Clear the view container before adding a new component
// Create the component
this.currentAlertComponentRef = viewContainerRef.createComponent(AlertErrorComponent);
// Set inputs (if any)
this.currentAlertComponentRef.instance.message = 'Failed to save data. Please try again.';
// Subscribe to outputs (if any)
this.currentAlertComponentRef.instance.close.subscribe(() => {
this.clearAlert();
});
}
clearAlert() {
if (this.currentAlertComponentRef) {
this.currentAlertComponentRef.destroy(); // Destroy the component instance
this.currentAlertComponentRef = null;
}
// Alternatively, if you want to clear all views from the container
// this.dynamicHost.viewContainerRef.clear();
}
ngOnDestroy(): void {
// Ensure component is destroyed when AppComponent is destroyed
this.clearAlert();
}
}
- Explanation:
- We import all necessary components and the
DynamicHostDirective. CommonModuleis added toimportsforngIf(though not explicitly used here, it’s a good habit forAppComponentif you might use structural directives).- In the template, we place
<ng-template appDynamicHost></ng-template>. Thisng-templateis a placeholder that will not render anything itself but serves as the anchor point for ourDynamicHostDirective. @ViewChild(DynamicHostDirective, { static: true }) dynamicHost!: DynamicHostDirective;retrieves an instance of ourDynamicHostDirective.{ static: true }means it’s available duringngOnInitbecause theng-templateis not conditionally rendered.loadSuccessAlert()andloadErrorAlert()methods:this.clearAlert(): First, we remove any previously loaded alert to prevent multiple alerts from stacking up (and to clean up resources).viewContainerRef.clear(): This clears all views (components) from the container.viewContainerRef.createComponent(AlertSuccessComponent): This is the magic! It dynamically creates an instance ofAlertSuccessComponentand inserts it into the DOM at theng-template’s location. It returns aComponentRef.this.currentAlertComponentRef.instance.message = '...': We can access theinstanceproperty of theComponentRefto set@Input()properties on the dynamically created component.this.currentAlertComponentRef.instance.close.subscribe(...): We can subscribe to@Output()events emitted by the dynamic component. When the dismiss button is clicked, we callclearAlert().
clearAlert(): This method is vital for memory management.componentRef.destroy()explicitly removes the component from the DOM and cleans up its resources. If you don’t do this, you risk memory leaks, especially in applications where components are frequently added and removed dynamically.ngOnDestroy(): We callclearAlert()here to ensure any active dynamic component is cleaned up whenAppComponentitself is destroyed.
- We import all necessary components and the
You should now see two buttons. Clicking “Show Success Alert” or “Show Error Alert” will dynamically load the respective alert component. Clicking the “Dismiss” button on the alert will remove it.
Mini-Challenge 2: Pass More Data and React to Events
Challenge:
- Modify
AlertSuccessComponentto accept an additional@Input()callediconName(e.g., ‘check-circle’, ‘info-circle’). Display this icon (you can use a simple text character like ‘✓’ or ‘i’ for now, or imagine a font-awesome icon). - Update
AppComponent’sloadSuccessAlert()method to pass a value foriconName. - Modify
AlertErrorComponentto emit an additional@Output()event calledretrywhen a new “Retry” button (add this button to the template) is clicked. The event should emit the originalmessageof the error. - Update
AppComponent’sloadErrorAlert()method to subscribe to thisretryevent and log the message to the console, simulating a retry action.
Hint: For iconName, you’ll add an <i> tag or similar element in the alert’s template. For the retry event, remember to create a new EventEmitter and a method to emit it.
What to observe/learn: This challenge deepens your understanding of how to fully interact with dynamically loaded components, both by passing data to them and reacting to events from them.
Common Pitfalls & Troubleshooting
Working with component composition, content projection, and dynamic loading can sometimes lead to unexpected behavior. Here are a few common pitfalls and how to troubleshoot them:
Forgetting
importsin Standalone Components:- Pitfall: You create a standalone
ChildComponentorDynamicComponentbut forget to add it to theimportsarray of the parent component (or theAppComponentfor dynamic loading). - Symptom: Angular throws an error like
NG0304: 'app-child-component' is not a known element. - Troubleshooting: Always remember that standalone components must explicitly import other standalone components, directives, or pipes they use, as well as common modules like
CommonModuleif they use directives likengIforngFor. Double-check theimportsarray in your@Componentdecorator.
- Pitfall: You create a standalone
Memory Leaks with Dynamic Components (Not Destroying
ComponentRef):- Pitfall: You dynamically create components using
viewContainerRef.createComponent(), but you don’t callcomponentRef.destroy()when the component is no longer needed. - Symptom: Your application’s memory usage steadily climbs over time, especially if components are frequently added and removed. You might observe lingering event listeners or subscriptions.
- Troubleshooting: Always keep a reference to the
ComponentRefreturned bycreateComponent(). When the component should be removed, callcomponentRef.destroy(). If the host component (the one doing the dynamic loading) is destroyed, make sure itsngOnDestroyhook also callsdestroy()on any activeComponentRefinstances.viewContainerRef.clear()is also a good way to clear all components from a container.
- Pitfall: You dynamically create components using
Incorrect
selectAttribute for Content Projection:- Pitfall: Content isn’t being projected into the correct slot, or it’s ending up in the default slot unexpectedly. This often happens with multi-slot projection.
- Symptom: Your projected content appears in the wrong place, or not at all.
- Troubleshooting:
- Check the
selectattribute in<ng-content>: Ensure it matches exactly (e.g.,select="[cardHeader]",select=".my-class",select="h3"). - Check the parent’s HTML: Make sure the element you’re projecting has the corresponding attribute (e.g.,
<h3 cardHeader>...</h3>). - Understand default slot: Any content not matching a specific
selectattribute will go into the<ng-content>without aselectattribute. If there’s no default slot, that content won’t be projected.
- Check the
ViewContainerRefNot Found or Not Ready:- Pitfall: You try to access
this.dynamicHost.viewContainerRef(or similar) too early, for instance, directly in the constructor, or before the host element is rendered. - Symptom:
TypeError: Cannot read properties of undefined (reading 'viewContainerRef')or similar errors. - Troubleshooting: Ensure you access
ViewContainerRef(via@ViewChildor@ViewChildren) insidengOnInitor later lifecycle hooks, especially if the host element is conditionally rendered (in which case,@ViewChild({ static: false })is necessary). For our example,static: trueworks becauseng-templateis always present.
- Pitfall: You try to access
Summary
Phew! We’ve covered some foundational concepts for building robust and flexible Angular UIs. Let’s quickly recap the key takeaways:
- Component Composition: It’s the art of building complex UIs from smaller, reusable, and focused components. This promotes modularity, reusability, and easier maintenance, much like building with LEGO bricks. You use
@Input()and@Output()to facilitate communication between parent and child components. - Content Projection (
<ng-content>): This powerful feature allows a parent component to “project” content into a placeholder within a child component’s template. It’s perfect for creating generic wrapper components (like cards or modals) that provide structure while allowing the parent to define the inner content. We explored both single-slot and multi-slot projection using theselectattribute. - Dynamic Component Loading (
ViewContainerRef): This technique enables you to instantiate and render components at runtime, without hardcoding their selectors in the template. It’s invaluable for highly dynamic UIs, plugin architectures, notifications, and performance optimization. We learned how to use a custom directive withViewContainerRefandviewContainerRef.createComponent()to achieve this, remembering to manage inputs, outputs, and crucially, to destroy components to prevent memory leaks.
By mastering these patterns, you gain immense flexibility in structuring your Angular applications, leading to cleaner code, higher reusability, and a better developer experience. These are not just theoretical concepts; they are daily tools for any Angular developer working on production-grade applications.
What’s Next?
In the next chapter, we’ll continue our journey into advanced UI architecture by exploring structural directives for virtualization, understanding trackBy for performance optimization in lists, and diving into advanced reactive forms. Get ready to further optimize your component rendering and build sophisticated data entry experiences!
References
- Angular Official Docs: Component Interaction
- Angular Official Docs: Content Projection
- Angular Official Docs: Dynamic Component Loader
- Angular Official Docs: ViewContainerRef
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.