Pipes — Transforming Data in Templates

Pipes transform data in templates — formatting dates, currencies, numbers, and strings without cluttering component logic with display-format code. Angular’s built-in pipes cover the most common cases. The async pipe is the most powerful — it subscribes to an Observable or Promise and automatically updates the template when a new value arrives, and automatically unsubscribes when the component is destroyed. Custom pipes extend the pipe system for application-specific transformations like “time ago” or “bytes to human-readable size”.

Built-in Pipes and Custom Pipe

// ── Template using built-in pipes ─────────────────────────────────────────
@Component({
  standalone:  true,
  imports: [DatePipe, CurrencyPipe, DecimalPipe, UpperCasePipe,
            AsyncPipe, JsonPipe, KeyValuePipe, SlicePipe],
  template: `
    <!-- Date pipe ───────────────────────────────────────────────────────── -->
    <p>Published: {{ post.publishedAt | date:'mediumDate' }}</p>
    <p>At: {{ post.publishedAt | date:'h:mm a' }}</p>
    <p>Full: {{ post.publishedAt | date:'MMMM d, y, h:mm a' }}</p>

    <!-- Number / currency pipe ──────────────────────────────────────────── -->
    <p>Views: {{ post.viewCount | number:'1.0-0' }}</p>
    <p>Price: {{ product.price | currency:'USD':'symbol':'1.2-2' }}</p>

    <!-- String pipes ────────────────────────────────────────────────────── -->
    <h1>{{ post.title | uppercase }}</h1>
    <p>{{ post.tags | slice:0:3 | json }}</p>

    <!-- async pipe — subscribes to Observable, unsubscribes on destroy ──── -->
    @if (posts$ | async; as posts) {
      @for (post of posts; track post.id) {
        <app-post-card [post]="post" />
      }
    }

    <!-- Chaining pipes ──────────────────────────────────────────────────── -->
    <p>{{ post.publishedAt | date:'shortDate' | uppercase }}</p>

    <!-- keyvalue pipe — iterate over object properties ─────────────────── -->
    @for (entry of post.metadata | keyvalue; track entry.key) {
      <p>{{ entry.key }}: {{ entry.value }}</p>
    }

    <!-- json pipe — for debugging ───────────────────────────────────────── -->
    <pre>{{ post | json }}</pre>

    <!-- Custom pipe ─────────────────────────────────────────────────────── -->
    <p>{{ post.publishedAt | timeAgo }}</p>  <!-- "3 hours ago" ─────────── -->
  `,
})
export class PostListComponent {
  posts$   = this.postsService.getPublished(1, 10);
  post     = { ... } as PostDto;
}

// ── Custom TimeAgo pipe ────────────────────────────────────────────────────
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name:       'timeAgo',
  standalone:  true,
  pure:        true,   // only re-runs when input reference changes (default)
})
export class TimeAgoPipe implements PipeTransform {
  transform(value: string | null | undefined): string {
    if (!value) return '';

    const now      = Date.now();
    const then     = new Date(value).getTime();
    const diffSecs = Math.floor((now - then) / 1000);

    if (diffSecs < 60)  return `${diffSecs}s ago`;
    if (diffSecs < 3600) return `${Math.floor(diffSecs / 60)}m ago`;
    if (diffSecs < 86400) return `${Math.floor(diffSecs / 3600)}h ago`;
    return `${Math.floor(diffSecs / 86400)}d ago`;
  }
}
Note: The async pipe is the recommended way to display Observable data in templates. It subscribes to the Observable when the component is rendered, automatically unsubscribes when the component is destroyed (preventing memory leaks), and triggers change detection when new values arrive. Without the async pipe, you must manually subscribe in ngOnInit, store the result in a property, and unsubscribe in ngOnDestroy. The async pipe makes this a one-liner in the template and eliminates the subscription management boilerplate.
Tip: Use the as syntax with the async pipe to avoid multiple subscriptions: @if (posts$ | async; as posts) subscribes once and makes the value available as posts within the @if block. Without as, using {{ posts$ | async }} multiple times in a template creates multiple subscriptions — each triggering a separate HTTP request. The as pattern is the correct approach for using async pipe values in multiple places.
Warning: Impure pipes (pure: false) re-run on every change detection cycle, regardless of whether the input changed. For a component with dozens of rendered posts, an impure pipe runs dozens of times per change detection sweep — potentially hundreds of times per second. Only use impure pipes when you genuinely need the pipe to reflect changes that are not detectable by reference equality (like transforming the contents of a mutable array). Pure pipes (the default) only run when the input reference changes, which is far more efficient.

Common Mistakes

Mistake 1 — Not importing DatePipe in standalone component (pipe not found)

❌ Wrong — using | date without importing DatePipe; template error at runtime.

✅ Correct — add DatePipe to the component’s imports: [] array (or import CommonModule which includes it).

Mistake 2 — Using async pipe multiple times on the same Observable (multiple HTTP calls)

❌ Wrong — {{ posts$ | async }} and (posts$ | async)?.length in the same template; two HTTP calls.

✅ Correct — use @if (posts$ | async; as posts) once and reference posts throughout.

🧠 Test Yourself

A component navigates away while an Observable is still pending. The template uses posts$ | async. What happens to the subscription when the component is destroyed?