| |

Angular 60 🅰️ Standalone Directives and Pipes

The previous chapter covered standalone components and the bootstrap without NgModules. This chapter covers the other two building blocks that the standalone model made first-class: the standalone directive and the standalone pipe. A directive adds behavior to an element, and a pipe transforms a value in the template. Both followed the same evolution as the component — from the NgModule-based declaration to the self-contained standalone: true, and since Angular 19, the default. The result is a model where every building block declares its own dependencies, and the NgModule becomes an optional organizational layer rather than a required one. This chapter covers the standalone directive, the standalone pipe, the imports array in the component, the host directives, the functional pipes with pure and impure, the @Directive‘s standalone flag, the standalone pipe’s dependency injection, the patterns for composition, and the migration from the NgModule-based directives and pipes. It builds on Angular 59’s component material and completes the standalone story.

Key point: A standalone directive is a directive with the standalone: true flag (or the default since Angular 19). It declares its own dependencies in the imports array of the @Directive decorator, and it can be imported directly by any standalone component or NgModule. A standalone pipe is a pipe with the same flag, and it declares its own dependencies in the imports array of the @Pipe decorator. The component that uses a standalone directive or pipe adds it to its own imports array. The host directives — the hostDirectives field on the @Component or the @Directive — compose the directives without the template’s attributes. The pipe’s pure option determines whether the pipe recomputes on every change detection or only when the input’s reference changes. The standalone model makes the dependencies explicit and the tree-shaking finer.


The standalone directive

A standalone directive is a directive that does not belong to an NgModule. It declares its own dependencies, and it is imported directly by the components that use it.

import { Directive, ElementRef, HostListener, inject } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true,
})
export class HighlightDirective {
  private readonly el = inject(ElementRef);

  @HostListener('mouseenter')
  onMouseEnter(): void {
    this.el.nativeElement.style.backgroundColor = 'yellow';
  }

  @HostListener('mouseleave')
  onMouseLeave(): void {
    this.el.nativeElement.style.backgroundColor = '';
  }
}

The standalone: true is the flag, and the directive is self-contained. The component that uses it adds it to its imports array, and the directive is available in the template.

Why the standalone directive matters. The standalone directive is the self-contained, and the self-contained is the explicit. The NgModule’s declarations is the legacy, and the legacy is the module’s. The two are the equivalent, and the standalone is the modern.

Why the standalone directive can have the dependencies. The standalone directive’s imports array can include the other directives and the pipes, and the other is the available. The pattern is the same as the component’s, and the same is the composition.

@Directive({
  selector: '[appTooltip]',
  standalone: true,
  imports: [HighlightDirective],
})
export class TooltipDirective {
  // The HighlightDirective is available in this directive's host.
}

The TooltipDirective imports the HighlightDirective, and the HighlightDirective is the available. The two are the pair, and the pair is the composition.

Why the standalone directive’s selector matters. The selector is the attribute, and the attribute is the template’s. The [appHighlight] is the attribute’s selector, and the <div appHighlight> is the usage. The two are the pair, and the pair is the directive’s.

Why the standalone directive’s injection matters. The directive’s injection is the same as the component’s, and the same is the inject(). The inject(ElementRef) is the element’s, and the element’s is the directive’s. The two are the pair, and the pair is the DI.

Why the standalone directive can be the host’s. The directive can be the host’s, and the host’s is the component’s. The hostDirectives field is the composition, and the composition is the pattern.

@Component({
  selector: 'app-button',
  standalone: true,
  hostDirectives: [HighlightDirective],
  template: `<ng-content />`,
})
export class ButtonComponent {}

The hostDirectives: [HighlightDirective] applies the HighlightDirective to the ButtonComponent‘s host element, and the directive is the host’s. The two are the pair, and the pair is the composition.

Why the hostDirectives matters. The hostDirectives is the composition, and the composition is the pattern. The directive is applied to the host, and the host is the component’s. The two are the pair, and the pair is the reuse.

Why the standalone directive’s lifecycle matters. The directive’s lifecycle is the same as the component’s, and the same is the ngOnInit and the ngOnDestroy. The DestroyRef is the modern, and the modern is the inject(). The two are the pair, and the pair is the cleanup.


The standalone pipe

A standalone pipe is a pipe that does not belong to an NgModule. It declares its own dependencies, and it is imported directly by the components that use it.

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'truncate',
  standalone: true,
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, maxLength: number = 20): string {
    return value.length > maxLength ? value.slice(0, maxLength) + '...' : value;
  }
}

The standalone: true is the flag, and the pipe is self-contained. The component that uses it adds it to its imports array, and the pipe is available in the template.

Why the standalone pipe matters. The standalone pipe is the self-contained, and the self-contained is the explicit. The NgModule’s declarations is the legacy, and the legacy is the module’s. The two are the equivalent, and the standalone is the modern.

Why the standalone pipe’s name matters. The name is the template’s identifier, and the identifier is the pipe’s. The {{ value | truncate: 10 }} is the usage, and the truncate is the name. The two are the pair, and the pair is the pipe’s.

Why the standalone pipe can have the dependencies. The standalone pipe’s imports array can include the other pipes, and the other is the available. The pattern is the same as the component’s, and the same is the composition.

@Pipe({
  name: 'formatDate',
  standalone: true,
  imports: [DatePipe],
})
export class FormatDatePipe implements PipeTransform {
  private readonly datePipe = inject(DatePipe);

  transform(value: Date, format: string = 'short'): string {
    return this.datePipe.transform(value, format) ?? '';
  }
}

The FormatDatePipe imports the DatePipe, and the DatePipe is the available. The two are the pair, and the pair is the composition.

Why the standalone pipe’s DI matters. The pipe’s DI is the same as the component’s, and the same is the inject(). The inject(DatePipe) is the pipe’s, and the pipe’s is the DI. The two are the pair, and the pair is the modern.

Why the standalone pipe’s pure matters. The pure is the default, and the default is the recompute on the input’s reference change. The pure: false is the recompute on every change detection, and the every is the cost.

@Pipe({
  name: 'liveData',
  standalone: true,
  pure: false,  // the recompute on every change detection
})
export class LiveDataPipe implements PipeTransform {
  transform(value: unknown): unknown {
    // The impure pipe runs on every change detection cycle.
    return value;
  }
}

The pure: false is the impure, and the impure is the recompute on the every. The two are the pair, and the pair is the performance.

Why the pure matters. The pure is the default, and the default is the performance. The impure is the every, and the every is the cost. The two are the pair, and the pair is the choice.

Why the standalone pipe’s lifecycle matters. The pipe’s lifecycle is the same as the component’s, and the same is the ngOnDestroy. The DestroyRef is the modern, and the modern is the inject(). The two are the pair, and the pair is the cleanup.


The component’s imports array

The standalone component’s imports array declares the dependencies, and the dependencies are the standalone components, the directives, and the pipes.

@Component({
  selector: 'app-user-card',
  standalone: true,
  imports: [
    CommonModule,
    HighlightDirective,
    TooltipDirective,
    TruncatePipe,
    FormatDatePipe,
  ],
  template: `
    <div appHighlight appTooltip="Click me">
      <p>{{ user.name | truncate: 10 }}</p>
      <p>{{ user.createdAt | formatDate }}</p>
    </div>
  `,
})
export class UserCardComponent {
  user = { name: 'John Doe', createdAt: new Date() };
}

The imports array is the list, and the list is the dependencies. The HighlightDirective, the TooltipDirective, the TruncatePipe, and the FormatDatePipe are the components, and the components are the available.

Why the component’s imports array matters. The imports array is the component’s dependencies, and the dependencies are the explicit. The NgModule’s declarations is the legacy, and the legacy is the module’s. The two are the equivalent, and the imports array is the modern.

Why the CommonModule is often in the imports. The CommonModule provides the NgIf, the NgFor, and the NgClass, and the three are the common. The CommonModule is the import, and the import is the convenience.

Why the CommonModule can be the avoided. The CommonModule can be the avoided, and the avoided is the tree-shaking. The @if and the @for are the built-in, and the built-in is the modern. The two are the pair, and the pair is the modern.

// The modern (the built-in control flow):
@if (user) { <p>{{ user.name }}</p> }
@for (item of items; track item.id) { <div>{{ item.name }}</div> }

// The classic (the CommonModule):
<p *ngIf="user">{{ user.name }}</p>
<div *ngFor="let item of items">{{ item.name }}</div>

The @if and the @for are the built-in, and the built-in is the modern. The *ngIf and the *ngFor are the classic, and the classic is the legacy. The two are the choice, and the choice is the version.

Why the standalone’s imports should be the minimal. The imports should be the minimal, and the minimal is the bundle’s. The unused is the bundle’s cost, and the cost is the optimization. The two are the pair, and the pair is the practice.

Why the standalone’s imports should be the documented. The imports should be the documented, and the documented is the why. The comment is the why, and the why is the clarity. The two are the pair, and the pair is the discipline.


The hostDirectives

The hostDirectives field on the @Component or the @Directive applies the directives to the host element without the template’s attributes.

@Directive({
  selector: '[appHighlight]',
  standalone: true,
})
export class HighlightDirective {
  @HostListener('mouseenter')
  onMouseEnter(): void {
    // the highlight
  }
}

@Directive({
  selector: '[appTooltip]',
  standalone: true,
  hostDirectives: [HighlightDirective],
})
export class TooltipDirective {
  // The HighlightDirective is applied to the host.
}

@Component({
  selector: 'app-button',
  standalone: true,
  hostDirectives: [
    {
      directive: HighlightDirective,
      inputs: ['color'],
      outputs: ['highlighted'],
    },
  ],
  template: `<ng-content />`,
})
export class ButtonComponent {}

The hostDirectives: [HighlightDirective] applies the directive to the host, and the inputs and the outputs are the exposed. The two are the pair, and the pair is the composition.

Why the hostDirectives matters. The hostDirectives is the composition, and the composition is the pattern. The directive is applied to the host, and the host is the component’s. The two are the pair, and the pair is the reuse.

Why the hostDirectives‘ inputs and outputs matter. The inputs and the outputs are the exposed, and the exposed is the API. The inputs: ['color'] is the input’s, and the outputs: ['highlighted'] is the output’s. The two are the pair, and the pair is the API.

Why the hostDirectives‘ inputs and outputs can be the aliased. The inputs and the outputs can be the aliased, and the aliased is the renamed.

hostDirectives: [
  {
    directive: HighlightDirective,
    inputs: ['color: highlightColor'],
    outputs: ['highlighted: onHighlight'],
  },
],

The color: highlightColor is the alias, and the alias is the renamed. The highlightColor is the public’s, and the color is the directive’s. The two are the pair, and the pair is the encapsulation.

Why the hostDirectives‘ aliases matter. The aliases are the encapsulation, and the encapsulation is the API. The directive’s inputs are the private, and the component’s are the public. The two are the pair, and the pair is the design.

Why the hostDirectives can be the nested. The hostDirectives can be the nested, and the nested is the composition.

@Directive({
  selector: '[appBase]',
  standalone: true,
  hostDirectives: [HighlightDirective, TooltipDirective],
})
export class BaseDirective {}

The BaseDirective‘s hostDirectives includes the two, and the two are the composition. The two are the pair, and the pair is the nested.

Why the hostDirectives matters for the reuse. The hostDirectives is the reuse, and the reuse is the composition. The directive is the reusable, and the reusable is the pattern. The two are the pair, and the pair is the design.


The functional pipes

The pipe can be the pure function, and the PipeTransform is the interface. The functional pipe is the function, and the function is the concise.

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'truncate', standalone: true })
export class TruncatePipe implements PipeTransform {
  transform(value: string, maxLength: number = 20): string {
    return value.length > maxLength ? value.slice(0, maxLength) + '...' : value;
  }
}

The transform is the method, and the method is the function. The value and the maxLength are the parameters, and the parameters are the template’s.

Why the pipe’s transform matters. The transform is the method, and the method is the pipe’s. The first parameter is the value, and the rest are the arguments. The two are the pair, and the pair is the API.

Why the pipe’s transform should be the pure. The transform should be the pure, and the pure is the predictable. The side effects are the effect’s, and the effect is the separate. The two are the pair, and the pair is the discipline.

Why the pipe’s transform should be the fast. The transform should be the fast, and the fast is the performance. The pipe runs on the every change detection, and the every is the cost. The two are the pair, and the pair is the performance.

Why the pipe’s transform can use the DI. The transform can use the DI, and the DI is the inject(). The pipe’s constructor is the DI’s, and the DI is the service’s. The two are the pair, and the pair is the pattern.

@Pipe({ name: 'currency', standalone: true })
export class CurrencyPipe implements PipeTransform {
  private readonly locale = inject(LOCALE_ID);

  transform(value: number): string {
    return new Intl.NumberFormat(this.locale, {
      style: 'currency',
      currency: 'USD',
    }).format(value);
  }
}

The CurrencyPipe uses the inject(LOCALE_ID), and the LOCALE_ID is the DI’s. The pipe’s constructor is the DI’s, and the DI is the service’s. The two are the pair, and the pair is the pattern.

Why the pipe’s DI matters. The pipe’s DI is the service’s, and the service’s is the DI. The inject() is the modern, and the modern is the concise. The two are the pair, and the pair is the pattern.

Why the pipe’s pure matters for the performance. The pure is the default, and the default is the performance. The impure is the every, and the every is the cost. The two are the pair, and the pair is the choice.


The standalone’s migration

The migration from the NgModule-based directives and pipes is the same as the component’s, and the same is the ng generate @angular/core:standalone.

The directive’s migration. The migration adds the standalone: true to the directive and moves the dependencies to the imports array.

ng generate @angular/core:standalone
# Select "Convert all components, directives and pipes to standalone"

The migration converts the directives, and the conversion is the automatic. The imports array is the list, and the list is the dependencies.

Why the directive’s migration matters. The directive’s migration is the automatic, and the automatic is the safe. The imports array is the explicit, and the explicit is the standalone’s. The two are the pair, and the pair is the migration.

The pipe’s migration. The migration adds the standalone: true to the pipe and moves the dependencies to the imports array. The pattern is the same as the directive’s, and the same is the automatic.

Why the pipe’s migration matters. The pipe’s migration is the automatic, and the automatic is the safe. The imports array is the explicit, and the explicit is the standalone’s. The two are the pair, and the pair is the migration.

Why the migration’s TODO comments matter. The migration’s TODO comments are the manual’s, and the manual is the edge’s. The schematic’s automatic is the most, and the most is the majority. The two are the pair, and the pair is the migration.

Why the migration should be the incremental. The migration should be the incremental, and the incremental is the safe. The three steps are the sequence, and the sequence is the order. The two are the pair, and the pair is the migration.

Why the migration’s test matters. The migration’s test is the verification, and the verification is the safety. The test’s is the focused, and the focused is the fast. The two are the pair, and the pair is the test.

Why the migration’s review matters. The migration’s review is the manual, and the manual is the check. The schematic’s automatic is the most, and the most is the majority. The two are the pair, and the pair is the review.


Complete Example Session

// ============================================
// PART 1: THE STANDALONE DIRECTIVE
// ============================================

import { Directive, ElementRef, HostListener, inject } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true,
})
export class HighlightDirective {
  private readonly el = inject(ElementRef);

  @HostListener('mouseenter')
  onMouseEnter(): void {
    this.el.nativeElement.style.backgroundColor = 'yellow';
  }

  @HostListener('mouseleave')
  onMouseLeave(): void {
    this.el.nativeElement.style.backgroundColor = '';
  }
}

// ============================================
// PART 2: THE STANDALONE DIRECTIVE WITH THE DEPENDENCIES
// ============================================

@Directive({
  selector: '[appTooltip]',
  standalone: true,
  imports: [HighlightDirective],
})
export class TooltipDirective {
  @HostListener('mouseenter')
  onMouseEnter(): void {
    // The tooltip's show
  }
}

// ============================================
// PART 3: THE STANDALONE PIPE
// ============================================

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'truncate',
  standalone: true,
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, maxLength: number = 20): string {
    return value.length > maxLength ? value.slice(0, maxLength) + '...' : value;
  }
}

// ============================================
// PART 4: THE STANDALONE PIPE WITH THE DI
// ============================================

@Pipe({
  name: 'formatDate',
  standalone: true,
  imports: [DatePipe],
})
export class FormatDatePipe implements PipeTransform {
  private readonly datePipe = inject(DatePipe);

  transform(value: Date, format: string = 'short'): string {
    return this.datePipe.transform(value, format) ?? '';
  }
}

// ============================================
// PART 5: THE COMPONENT'S IMPORTS
// ============================================

@Component({
  selector: 'app-user-card',
  standalone: true,
  imports: [
    CommonModule,
    HighlightDirective,
    TooltipDirective,
    TruncatePipe,
    FormatDatePipe,
  ],
  template: `
    <div appHighlight appTooltip="Click me">
      <p>{{ user.name | truncate: 10 }}</p>
      <p>{{ user.createdAt | formatDate }}</p>
    </div>
  `,
})
export class UserCardComponent {
  user = { name: 'John Doe', createdAt: new Date() };
}

// ============================================
// PART 6: THE HOST DIRECTIVES
// ============================================

@Component({
  selector: 'app-button',
  standalone: true,
  hostDirectives: [
    {
      directive: HighlightDirective,
      inputs: ['color: highlightColor'],
      outputs: ['highlighted: onHighlight'],
    },
  ],
  template: `<ng-content />`,
})
export class ButtonComponent {}

// ============================================
// PART 7: THE DIRECTIVE'S HOST DIRECTIVES
// ============================================

@Directive({
  selector: '[appBase]',
  standalone: true,
  hostDirectives: [HighlightDirective, TooltipDirective],
})
export class BaseDirective {}

// ============================================
// PART 8: THE PURE AND THE IMPURE
// ============================================

@Pipe({
  name: 'pure',
  standalone: true,
  pure: true,  // the default
})
export class PurePipe implements PipeTransform {
  transform(value: unknown): unknown {
    return value;
  }
}

@Pipe({
  name: 'impure',
  standalone: true,
  pure: false,  // the impure
})
export class ImpurePipe implements PipeTransform {
  transform(value: unknown): unknown {
    return value;
  }
}

// ============================================
// PART 9: THE MIGRATION
// ============================================

// ng generate @angular/core:standalone
// Select "Convert all components, directives and pipes to standalone"

// ============================================
// PART 10: WHAT NOT TO DO
// ============================================

// Don't forget the standalone: true (or the default)
@Directive({ selector: '[appHighlight]' })  // the NgModule's  // ⚠️

// Don't forget the imports array
@Component({ standalone: true })  // the missing                // ⚠️

// Don't use the NgModule's declarations
declarations: [HighlightDirective]  // the standalone's error   // ⚠️

// Don't use the impure pipe without the reason
pure: false  // the performance's cost                        // ⚠️

// Don't forget the hostDirectives' inputs and outputs
hostDirectives: [HighlightDirective]  // the not exposed        // ⚠️

// Don't forget the base href
// The index.html's base href.                                // ⚠️

The ten parts cover the standalone directive, the standalone directive with the dependencies, the standalone pipe, the standalone pipe with the DI, the component’s imports, the hostDirectives, the directive’s hostDirectives, the pure and the impure, the migration, and the anti-patterns.


Quick Reference

The Standalone’s Flag

The versionThe default
Angular 14-18The standalone: true
Angular 19+The default

The Directive’s Decorator

The fieldThe purpose
The selectorThe attribute’s
The standaloneThe flag
The importsThe dependencies
The hostDirectivesThe composition

The Pipe’s Decorator

The fieldThe purpose
The nameThe template’s identifier
The standaloneThe flag
The importsThe dependencies
The pureThe recompute’s mode

The hostDirectives

The fieldThe purpose
The directiveThe directive’s class
The inputsThe exposed inputs
The outputsThe exposed outputs

The Pure and the Impure

The valueThe behavior
pure: trueThe recompute on the reference change
pure: falseThe recompute on every change detection

The Migration

The stepThe mode
The standalone’s conversionThe “Convert all components, directives and pipes to standalone”
The NgModule’s removalThe “Remove unnecessary NgModule classes”
The bootstrap’s conversionThe “Bootstrap the project using standalone APIs”

Best Practices

✅ Do This:

// Use the standalone: true (or the default)
@Directive({ selector: '[appHighlight]', standalone: true })   // ✅

// Use the standalone: true for the pipe
@Pipe({ name: 'truncate', standalone: true })                  // ✅

// Use the component's imports array
@Component({ imports: [HighlightDirective, TruncatePipe] })    // ✅

// Use the hostDirectives for the composition
hostDirectives: [HighlightDirective]                           // ✅

// Use the pure pipe by default
@Pipe({ name: 'truncate', pure: true })                        // ✅

// Use the DI in the pipe
@Pipe({ name: 'currency', standalone: true })
export class CurrencyPipe { private locale = inject(LOCALE_ID); } // ✅

// Use the migration's three steps
ng generate @angular/core:standalone                            // ✅

// Use the modern control flow
@if (user) { <p>{{ user.name }}</p> }                          // ✅

❌ Don’t Do This:

// Don't use the NgModule's declarations
declarations: [HighlightDirective]                             // ⚠️

// Don't forget the imports array
@Component({ standalone: true })  // the missing                // ⚠️

// Don't use the impure pipe without the reason
@Pipe({ name: 'truncate', pure: false })  // the cost          // ⚠️

// Don't forget the hostDirectives' inputs and outputs
hostDirectives: [HighlightDirective]  // the not exposed       // ⚠️

// Don't forget the standalone's flag
@Directive({ selector: '[appHighlight]' })  // the NgModule's  // ⚠️

// Don't forget the pipe's name
@Pipe({ standalone: true })  // the missing                     // ⚠️

Common Pitfalls

PitfallProblemSolution
The NgModule’s declarationsThe errorThe imports
The missing importsThe unknown elementThe add
The impure pipeThe performanceThe pure: true
The hostDirectives‘ missing inputsThe not exposedThe inputs
The missing standaloneThe NgModule’sThe flag
The missing pipe’s nameThe template’sThe name
The migration’s manual fixesThe TODOThe review
The base href’s missingThe route’s failThe <base href="/">

Real-World Examples

1. The standalone directive

@Directive({ selector: '[appHighlight]', standalone: true })
export class HighlightDirective {}

2. The standalone pipe

@Pipe({ name: 'truncate', standalone: true })
export class TruncatePipe implements PipeTransform {}

3. The component’s imports

@Component({ imports: [HighlightDirective, TruncatePipe] })

4. The hostDirectives

@Component({ hostDirectives: [HighlightDirective] })

5. The hostDirectives‘ inputs and outputs

hostDirectives: [
  { directive: HighlightDirective, inputs: ['color'], outputs: ['highlighted'] },
]

6. The pipe’s DI

@Pipe({ name: 'currency', standalone: true })
export class CurrencyPipe {
  private locale = inject(LOCALE_ID);
}

7. The pure pipe

@Pipe({ name: 'truncate', pure: true })

8. The impure pipe

@Pipe({ name: 'live', pure: false })

9. The migration

ng generate @angular/core:standalone

10. The modern control flow

@if (user) { <p>{{ user.name }}</p> }
@for (item of items; track item.id) { <div>{{ item.name }}</div> }

Visual: The Standalone’s Evolution

┌──────────────────────────────────────────────────────────┐
│  THE CLASSIC (the NgModule)                              │
│    The AppModule                                         │
│      declarations: [HighlightDirective, TruncatePipe]    │
│      exports: [HighlightDirective, TruncatePipe]         │
│                                                          │
│    The component's template uses the directive and the   │
│    pipe.                                                 │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  THE MODERN (the standalone)                             │
│    The HighlightDirective                                │
│      standalone: true                                    │
│                                                          │
│    The TruncatePipe                                      │
│      standalone: true                                    │
│                                                          │
│    The component's imports: [HighlightDirective, TruncatePipe]│
│                                                          │
│  The NgModule is not needed.                             │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The hostDirectives

┌──────────────────────────────────────────────────────────┐
│  @Component({                                            │
│    selector: 'app-button',                               │
│    standalone: true,                                     │
│    hostDirectives: [                                     │
│      {                                                   │
│        directive: HighlightDirective,                    │
│        inputs: ['color: highlightColor'],                │
│        outputs: ['highlighted: onHighlight'],            │
│      },                                                  │
│    ],                                                    │
│    template: `<ng-content />`,                           │
│  })                                                      │
│  export class ButtonComponent {}                         │
│                                                          │
│  THE HOST ELEMENT                                        │
│    <app-button highlightColor="yellow" onHighlight="..."> │
│                                                          │
│  The hostDirectives applies the directive to the host,   │
│  and the inputs and the outputs are the exposed.         │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Pure and the Impure

┌──────────────────────────────────────────────────────────┐
│  THE PURE (the default)                                  │
│    @Pipe({ name: 'truncate', pure: true })               │
│    The transform runs when the input's reference changes.│
│    The performance is the good.                          │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  THE IMPURE                                              │
│    @Pipe({ name: 'live', pure: false })                  │
│    The transform runs on every change detection cycle.   │
│    The performance is the cost.                          │
│    The use: the async's data, the external's state.      │
│                                                          │
│  The pure is the default, and the default is the         │
│  performance.                                            │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Component’s Imports

┌──────────────────────────────────────────────────────────┐
│  @Component({                                            │
│    selector: 'app-user-card',                            │
│    standalone: true,                                     │
│    imports: [                                            │
│      CommonModule,                                       │
│      HighlightDirective,                                 │
│      TooltipDirective,                                   │
│      TruncatePipe,                                       │
│      FormatDatePipe,                                     │
│    ],                                                    │
│    template: `...`,                                      │
│  })                                                      │
│  export class UserCardComponent {}                       │
│                                                          │
│  The imports array is the list, and the list is the      │
│  component's dependencies.                               │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Migration

┌──────────────────────────────────────────────────────────┐
│  THE STEP 1                                              │
│    The "Convert all components, directives and pipes to standalone"│
│    The standalone: false → the imports array.            │
│                                                          │
│  THE STEP 2                                              │
│    The "Remove unnecessary NgModule classes"             │
│    The empty modules are removed.                        │
│                                                          │
│  THE STEP 3                                              │
│    The "Bootstrap the project using standalone APIs"     │
│    The bootstrapModule → the bootstrapApplication.       │
│                                                          │
│  The three steps are the incremental, and the            │
│  incremental is the safe.                                │
│                                                          │
└──────────────────────────────────────────────────────────┘

Summary

ItemValue
The standalone directiveThe @Directive({ standalone: true })
The standalone pipeThe @Pipe({ standalone: true })
The component’s importsThe dependencies’ list
The hostDirectivesThe composition
The hostDirectives‘ inputsThe exposed
The hostDirectives‘ outputsThe exposed
The pure pipeThe default
The impure pipeThe pure: false
The migrationThe three steps
The modern control flowThe @if and the @for

Key takeaways:

  • A standalone directive declares its own dependencies in the imports array of the @Directive decorator
  • A standalone pipe declares its own dependencies in the imports array of the @Pipe decorator
  • The component’s imports array lists the standalone directives and pipes that its template uses
  • The hostDirectives field composes the directives on the host element, and the inputs and the outputs expose the API
  • The pipe’s pure option determines whether the pipe recomputes on the reference change (pure: true) or on every change detection (pure: false)
  • The pure pipe is the default — the impure is the specific, and the specific is the cost
  • The pipe’s DI works with the inject() — the pipe can inject the services, and the service is the pipe’s
  • The migration from the NgModule-based directives and pipes is the same as the component’s — the ng generate @angular/core:standalone runs the three steps
  • The modern control flow (@if and @for) reduces the CommonModule‘s need — the built-in is the modern, and the modern is the concise
  • The standalone model makes the dependencies explicit — the explicit is the tree-shaking’s, and the tree-shaking is the optimization

Remember: The standalone directives and the standalone pipes are the modern Angular, and the standalone: true is the default since Angular 19. The imports array is the component’s dependencies, and the hostDirectives is the composition. The pipe’s pure option is the performance’s choice, and the pipe’s DI is the inject(). The migration is the incremental, and the incremental is the safe. The NgModule is the legacy, and the legacy is the migration’s.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!