Welcome back, future Angular master! In Chapter 1, we laid the groundwork for our journey into modern Angular. Now, it’s time to dive into the very heart of how we build applications today: Standalone Components, Directives, and Pipes. These are the fundamental building blocks of any Angular application, and understanding them deeply is crucial for writing efficient, maintainable, and scalable code.

This chapter will guide you through Angular’s revolutionary standalone architecture, which became the default and preferred way to build applications starting with Angular v17 and has continued to evolve and mature up to the latest stable release (Angular v20.3.10 as of 2026-02-11). We’ll explore how these standalone building blocks simplify development by removing the need for NgModules, making your code more modular and easier to reason about. Get ready to build, challenge yourself, and truly understand why these concepts are so powerful in real-world production scenarios.

By the end of this chapter, you’ll not only know how to create and use standalone components, directives, and pipes but also why this approach is a game-changer for modern Angular development. We’ll build everything incrementally, explain every piece, and ensure you gain a solid, practical understanding. Let’s get started!

Core Concepts: The Standalone Revolution

Angular applications are built from many small, focused pieces that work together. Historically, these pieces (components, directives, pipes) had to be declared within an NgModule. While NgModules provided a way to organize and scope features, they often added boilerplate and complexity, especially for smaller, reusable units.

The standalone architecture changes this by allowing components, directives, and pipes to be self-contained. This means they can manage their own dependencies without being part of an NgModule, leading to simpler, more modular code.

What Problem Does Standalone Solve?

Imagine you have a small, reusable button component. Before standalone, even this tiny component needed to be declared in an NgModule. If it used NgIf or NgFor, that NgModule also needed to import CommonModule. This created a dependency chain that could become cumbersome, especially in larger applications or when sharing components across different parts of your app or even different applications (e.g., in microfrontends).

Without standalone components, you face:

  • Boilerplate: Every component, directive, or pipe required an NgModule declaration.
  • Over-scoping: NgModules could inadvertently make too many components available, leading to larger bundle sizes if not carefully managed.
  • Cognitive Load: Understanding which NgModule imports what, and how they relate, added complexity.
  • Tree-shaking limitations: While Angular’s build process is smart, NgModules could sometimes hinder optimal tree-shaking, leading to larger bundles than necessary for specific features.

Standalone components, directives, and pipes directly address these issues by:

  • Reducing Boilerplate: No more NgModules for individual units.
  • Improving Modularity: Each unit is truly self-contained, importing only what it needs.
  • Better Tree-Shaking: The build process can more accurately identify and remove unused code.
  • Simplified Mental Model: Easier to understand dependencies at a glance.

This shift aligns Angular with modern web development patterns, where individual components are often the primary unit of organization.

Understanding Standalone Components

A component is the most fundamental building block in Angular. It controls a part of the UI. A standalone component is simply a component that doesn’t belong to an NgModule.

Key Characteristics:

  • standalone: true: This property in the @Component decorator tells Angular that this component is self-sufficient.
  • imports array: Instead of an NgModule importing its dependencies, a standalone component directly imports any other standalone components, directives, pipes, or even NgModules (like CommonModule for NgIf/NgFor) that its template needs.

Let’s visualize the simplified dependency flow with standalone components:

graph TD A[main.ts] --> B{bootstrapApplication} B --> C[AppComponent] C -->|imports: CommonModule| C C -->|imports: MyFeatureComponent| D[MyFeatureComponent] D -->|imports: CustomDirective| E[CustomDirective] D -->|imports: DatePipe| F[DatePipe]

In this diagram, main.ts directly bootstraps the AppComponent. AppComponent then directly declares its dependencies in its imports array, including other standalone components, directives, and pipes. This direct, explicit import mechanism is the core of the standalone architecture.

Understanding Standalone Directives

Directives allow you to attach behavior to elements in the DOM. Think of them as enhancers. Just like components, directives can also be standalone.

Key Characteristics:

  • standalone: true: In the @Directive decorator.
  • imports array: A standalone directive can also have an imports array if its logic relies on other standalone directives or pipes, though this is less common than in components.

For example, a HighlightDirective might change the background color of an element. If it were to use another pipe or directive internally, it would declare it in its imports.

Understanding Standalone Pipes

Pipes are used to transform data right within your templates. For example, formatting a date or converting text to uppercase. Standalone pipes follow the same pattern.

Key Characteristics:

  • standalone: true: In the @Pipe decorator.
  • imports array: A standalone pipe can import other standalone pipes if it needs to chain transformations, though this is also less common.

A CapitalizePipe might take a string and return it with the first letter capitalized.

Modern File Structure Conventions

With standalone components, the concept of a “module” folder often becomes less relevant. A common convention now is to group related files (component, template, styles, tests, and potentially a local service or interface) within a single folder for that component.

For example:

src/
├── app/
│   ├── app.component.ts
│   ├── app.component.html
│   ├── app.component.css
│   ├── app.config.ts  <-- Application-wide providers
│   └── components/
│       ├── user-card/
│       │   ├── user-card.component.ts
│       │   ├── user-card.component.html
│       │   ├── user-card.component.css
│       │   └── user-card.component.spec.ts
│       └── product-list/
│           ├── product-list.component.ts
│           ├── product-list.component.html
│           ├── product-list.component.css
│           └── product-list.component.spec.ts
└── main.ts

This structure emphasizes component locality and reduces cognitive overhead when searching for related files.

Step-by-Step Implementation: Building a Standalone App

Let’s build a simple Angular application using only standalone components, directives, and pipes. We’ll create a UserCardComponent that displays user information, uses a custom directive to highlight active users, and a custom pipe to format a status.

Prerequisites: You should have Node.js (v18.x or higher) and the Angular CLI (v17.x or higher, which corresponds to Angular v20.3.10 for our 2026-02-11 context) installed. If not, please refer to Chapter 1 for setup instructions.

Step 1: Create a New Angular Standalone Project

First, let’s create a brand-new Angular project. The Angular CLI, as of v17+, defaults to standalone.

Open your terminal or command prompt and run:

ng new my-standalone-app --no-standalone --skip-git --skip-tests --style=css

Wait, why --no-standalone? This is a trick to show you the initial project structure without standalone as the default for the main AppComponent, which helps illustrate the migration. Actually, as of Angular v17 (and certainly by v20.3.10), ng new defaults to standalone. So, let’s correct that and embrace the modern default!

Let’s create it the modern way:

ng new my-standalone-app --skip-git --skip-tests --style=css
cd my-standalone-app

This will create a new project. Angular CLI will ask you about routing. For this simple example, choose No.

After the project is created, open src/app/app.component.ts. You’ll notice it already has standalone: true in its @Component decorator! This is the modern default.

// src/app/app.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; // Often imported by default for standalone components
import { RouterOutlet } from '@angular/router'; // If you had routing

@Component({
  standalone: true, // <-- This is the magic!
  imports: [CommonModule, RouterOutlet], // <-- Dependencies are listed here
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  title = 'my-standalone-app';
}

Notice the imports array. Even CommonModule, which provides structural directives like NgIf and NgFor, is imported directly here. This means if AppComponent’s template uses *ngIf or *ngFor, CommonModule must be present in its imports.

Let’s clean up app.component.html to be empty for now:

<!-- src/app/app.component.html -->
<!-- We'll add our content here soon -->

Step 2: Create a Standalone Component (UserCardComponent)

We’ll create a component to display user details.

Run the CLI command:

ng generate component components/user-card --standalone

This command generates:

  • src/app/components/user-card/user-card.component.ts
  • src/app/components/user-card/user-card.component.html
  • src/app/components/user-card/user-card.component.css

Open src/app/components/user-card/user-card.component.ts. You’ll see:

// src/app/components/user-card/user-card.component.ts
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common'; // CLI often adds this by default

@Component({
  selector: 'app-user-card',
  standalone: true, // It's standalone!
  imports: [CommonModule], // Ready for NgIf/NgFor if needed
  templateUrl: './user-card.component.html',
  styleUrl: './user-card.component.css',
})
export class UserCardComponent {
  // We'll add inputs for user data
  @Input() name: string = '';
  @Input() email: string = '';
  @Input() isActive: boolean = false;
  @Input() status: string = 'pending'; // For our custom pipe later
}

Now, let’s update user-card.component.html to display these inputs:

<!-- src/app/components/user-card/user-card.component.html -->
<div class="user-card">
  <h3>{{ name }}</h3>
  <p>Email: {{ email }}</p>
  <p>Status: {{ status }}</p>
  <p *ngIf="isActive">Active User</p>
  <p *ngIf="!isActive">Inactive User</p>
</div>

And add some basic styles to user-card.component.css:

/* src/app/components/user-card/user-card.component.css */
.user-card {
  border: 1px solid #ccc;
  padding: 15px;
  margin-bottom: 10px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.user-card h3 {
  color: #333;
  margin-top: 0;
}
.user-card p {
  color: #555;
  margin-bottom: 5px;
}

Step 3: Use the Standalone Component in AppComponent

Now, let’s use our UserCardComponent in the main AppComponent.

Open src/app/app.component.ts and import UserCardComponent:

// src/app/app.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { UserCardComponent } from './components/user-card/user-card.component'; // <-- Import our standalone component

@Component({
  standalone: true,
  imports: [CommonModule, RouterOutlet, UserCardComponent], // <-- Add it to imports!
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  title = 'my-standalone-app';

  // Sample user data
  users = [
    { name: 'Alice Smith', email: '[email protected]', isActive: true, status: 'active' },
    { name: 'Bob Johnson', email: '[email protected]', isActive: false, status: 'on leave' },
    { name: 'Charlie Brown', email: '[email protected]', isActive: true, status: 'working' }
  ];
}

Now, update src/app/app.component.html to render the UserCardComponent for each user:

<!-- src/app/app.component.html -->
<div class="container">
  <h1>{{ title }}</h1>

  <h2>Our Users</h2>
  <div *ngFor="let user of users">
    <app-user-card
      [name]="user.name"
      [email]="user.email"
      [isActive]="user.isActive"
      [status]="user.status"
    ></app-user-card>
  </div>
</div>

Add some global styles to src/app/app.component.css:

/* src/app/app.component.css */
.container {
  max-width: 800px;
  margin: 20px auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}
h1, h2 {
  color: #2c3e50;
}

Run your application to see the progress:

ng serve

Navigate to http://localhost:4200. You should see the user cards rendered!

Step 4: Create a Standalone Directive (ActiveUserHighlightDirective)

Let’s create a directive to visually distinguish active users.

ng generate directive directives/active-user-highlight --standalone

This generates src/app/directives/active-user-highlight/active-user-highlight.directive.ts.

Open the file and modify it:

// src/app/directives/active-user-highlight/active-user-highlight.directive.ts
import { Directive, ElementRef, Input, Renderer2, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; // Directives can also import modules if needed

@Directive({
  selector: '[appActiveUserHighlight]', // How we'll use it in HTML: <div appActiveUserHighlight>
  standalone: true, // It's standalone!
  imports: [CommonModule] // Not strictly needed for this simple example, but good to know it's possible.
})
export class ActiveUserHighlightDirective implements OnInit {
  @Input() appActiveUserHighlight: boolean = false; // Input to control the highlight

  constructor(private el: ElementRef, private renderer: Renderer2) {}

  ngOnInit(): void {
    // Apply highlight based on the input property
    if (this.appActiveUserHighlight) {
      this.renderer.setStyle(this.el.nativeElement, 'background-color', '#e6ffe6'); // Light green
      this.renderer.setStyle(this.el.nativeElement, 'border-left', '5px solid #28a745'); // Green border
    } else {
      this.renderer.setStyle(this.el.nativeElement, 'opacity', '0.7'); // Slightly dim inactive users
    }
  }
}

Explanation:

  • selector: '[appActiveUserHighlight]': This means the directive will be applied as an attribute on an HTML element.
  • @Input() appActiveUserHighlight: boolean = false;: We’re creating an input property that shares the same name as the selector. This allows us to pass a boolean value directly to the directive when it’s used.
  • ElementRef: A wrapper around the host element.
  • Renderer2: A service that allows us to manipulate the DOM safely, especially when working with server-side rendering or web workers.
  • ngOnInit: This lifecycle hook is where we apply the styling once the input property is available.

Step 5: Apply the Standalone Directive to UserCardComponent

Now, let’s make our UserCardComponent aware of the new directive.

Open src/app/components/user-card/user-card.component.ts and import ActiveUserHighlightDirective:

// src/app/components/user-card/user-card.component.ts
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActiveUserHighlightDirective } from '../../directives/active-user-highlight/active-user-highlight.directive'; // <-- Import our standalone directive

@Component({
  selector: 'app-user-card',
  standalone: true,
  imports: [CommonModule, ActiveUserHighlightDirective], // <-- Add it to imports!
  templateUrl: './user-card.component.html',
  styleUrl: './user-card.component.css',
})
export class UserCardComponent {
  @Input() name: string = '';
  @Input() email: string = '';
  @Input() isActive: boolean = false;
  @Input() status: string = 'pending';
}

Now, apply the directive in src/app/components/user-card/user-card.component.html to the main div:

<!-- src/app/components/user-card/user-card.component.html -->
<div class="user-card" [appActiveUserHighlight]="isActive">
  <h3>{{ name }}</h3>
  <p>Email: {{ email }}</p>
  <p>Status: {{ status }}</p>
  <p *ngIf="isActive">Active User</p>
  <p *ngIf="!isActive">Inactive User</p>
</div>

Refresh your browser (ng serve should auto-reload). You should now see Alice and Charlie’s cards highlighted, and Bob’s card slightly dimmed.

Step 6: Create a Standalone Pipe (StatusFormatPipe)

Finally, let’s create a pipe to format the user’s status for better readability.

ng generate pipe pipes/status-format --standalone

This generates src/app/pipes/status-format/status-format.pipe.ts.

Open the file and modify it:

// src/app/pipes/status-format/status-format.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'statusFormat', // How we'll use it in HTML: {{ value | statusFormat }}
  standalone: true, // It's standalone!
})
export class StatusFormatPipe implements PipeTransform {
  transform(value: string): string {
    if (!value) {
      return '';
    }
    // Simple transformation: capitalize first letter and replace hyphens with spaces
    return value.charAt(0).toUpperCase() + value.slice(1).replace(/-/g, ' ');
  }
}

Explanation:

  • name: 'statusFormat': This is the name you’ll use in your templates.
  • transform(value: string): string: This is the core method of any pipe. It takes an input value and returns the transformed value.

Step 7: Apply the Standalone Pipe to UserCardComponent

Let’s integrate our new pipe into the UserCardComponent.

Open src/app/components/user-card/user-card.component.ts and import StatusFormatPipe:

// src/app/components/user-card/user-card.component.ts
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActiveUserHighlightDirective } from '../../directives/active-user-highlight/active-user-highlight.directive';
import { StatusFormatPipe } from '../../pipes/status-format/status-format.pipe'; // <-- Import our standalone pipe

@Component({
  selector: 'app-user-card',
  standalone: true,
  imports: [CommonModule, ActiveUserHighlightDirective, StatusFormatPipe], // <-- Add it to imports!
  templateUrl: './user-card.component.html',
  styleUrl: './user-card.component.css',
})
export class UserCardComponent {
  @Input() name: string = '';
  @Input() email: string = '';
  @Input() isActive: boolean = false;
  @Input() status: string = 'pending';
}

Now, apply the pipe in src/app/components/user-card/user-card.component.html to the status display:

<!-- src/app/components/user-card/user-card.component.html -->
<div class="user-card" [appActiveUserHighlight]="isActive">
  <h3>{{ name }}</h3>
  <p>Email: {{ email }}</p>
  <p>Status: {{ status | statusFormat }}</p> <!-- <-- Apply the pipe here -->
  <p *ngIf="isActive">Active User</p>
  <p *ngIf="!isActive">Inactive User</p>
</div>

Refresh your browser. You should now see the “status” text formatted nicely, e.g., “on leave” becomes “On leave”, “working” becomes “Working”.

Congratulations! You’ve successfully built a small Angular application using only standalone components, directives, and pipes. Notice how we never touched an NgModule for these building blocks. Each piece explicitly declares what it needs, making dependencies clear and managing them simpler.

Mini-Challenge: Extend the User Card

Now it’s your turn to apply what you’ve learned.

Challenge:

  1. Add a new input to UserCardComponent: lastLoginDate: Date.
  2. Display this date in the UserCardComponent’s template.
  3. Apply Angular’s built-in DatePipe to format lastLoginDate as a short date (e.g., “Jan 1, 2026”). Remember, DatePipe is part of CommonModule.
  4. Update AppComponent’s users array to include a lastLoginDate for each user.

Hint:

  • Remember to import CommonModule into UserCardComponent’s imports array if it’s not already there, as DatePipe is provided by it.
  • The DatePipe syntax is {{ value | date:'shortDate' }}.

What to Observe/Learn:

  • How to pass different types of data (like Date objects) to components.
  • How to use built-in pipes in standalone components by importing their providing module (CommonModule).
  • The flexibility and power of Angular’s templating.

Take your time, experiment, and don’t be afraid to make mistakes. That’s how we learn best!

Click for Solution Hint (if stuck)

1. In `user-card.component.ts`, add `@Input() lastLoginDate: Date | undefined;`. 2. In `user-card.component.html`, add `

Last Login: {{ lastLoginDate | date:'shortDate' }}

`. 3. Ensure `CommonModule` is in `user-card.component.ts`'s `imports` array. 4. In `app.component.ts`, update `users` array with `lastLoginDate: new Date('2026-01-01')` (or similar valid dates).

Common Pitfalls & Troubleshooting

  1. “XYZ is not a known element/property/pipe” error:

    • Pitfall: This is the most common error when moving to standalone. It means you’ve used a component, directive, or pipe in a template, but the host component hasn’t imported it. In NgModule-based apps, you’d add it to imports of the parent module. In standalone, you add it to the imports array of the component that uses it.
    • Troubleshooting: Check the imports array of the component where the error occurs. Did you forget to import CommonModule for *ngIf or DatePipe? Did you forget to import your UserCardComponent or ActiveUserHighlightDirective?
    • Example: If UserCardComponent uses *ngIf and you remove CommonModule from its imports, you’ll get this error.
  2. Forgetting standalone: true:

    • Pitfall: If you generate a component, directive, or pipe without --standalone (or if your CLI version is older and doesn’t default to it), it won’t be standalone. If you then try to import it directly into another standalone component’s imports array, you’ll get a cryptic error about it not being a standalone entity.
    • Troubleshooting: Always verify standalone: true is present in the @Component, @Directive, or @Pipe decorator. If not, add it manually, or regenerate with --standalone.
  3. Circular Dependencies:

    • Pitfall: While less common with simple standalone units, it’s possible for Component A to import Component B, and Component B to also import Component A. This creates a circular dependency that can break the build or lead to unexpected behavior.
    • Troubleshooting: Review your imports arrays. If you suspect a circular dependency, try to refactor. Can the shared logic be extracted into a service? Can one component be a “dumb” component that only receives inputs, and the other a “smart” component that manages state?

Summary

Phew! You’ve just taken a monumental leap in understanding modern Angular. Here’s a quick recap of what we covered:

  • Standalone Architecture: Angular’s modern approach that allows components, directives, and pipes to be self-contained, eliminating the need for NgModules for individual units.
  • Problem Solved: Standalone reduces boilerplate, improves modularity, enhances tree-shaking, and simplifies the mental model of Angular applications.
  • Standalone Components: UI building blocks marked with standalone: true, managing their dependencies via an imports array.
  • Standalone Directives: Behavior enhancers for DOM elements, also marked standalone: true and capable of managing their own imports.
  • Standalone Pipes: Data transformers for templates, marked standalone: true with an imports array for chaining other pipes if needed.
  • Modern File Structure: Encourages grouping related files within a component’s folder for better locality.
  • Hands-on Experience: You successfully built a small application demonstrating the creation and usage of all three standalone types.
  • Troubleshooting: You learned to identify and resolve common issues like missing imports or incorrect standalone configuration.

The standalone architecture is a cornerstone of modern Angular development (v17+ onwards, including our target v20.3.10). By embracing it, you’re building applications that are leaner, more performant, and easier to maintain.

In the next chapter, we’ll build on this foundation by exploring Angular Services and Dependency Injection in the standalone world. You’ll learn how to share logic and data across your components efficiently and robustly. Get ready for more practical challenges and deeper insights into building professional Angular applications!

References


This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.