Technology  /  Angular

๐Ÿ…ฐ๏ธ Angular 15 guides ยท updated 2026

Standalone components, signals, RxJS, and dependency injection โ€” building maintainable frontends with the framework enterprises trust.

Angular Pipes: Built-in Formatting, Custom Pipes, and Purity Explained

Every app renders the same raw data many ways: paise as โ‚น amounts, ISO timestamps as โ€œ2 hours agoโ€, enums as human labels, long strings truncated with ellipses. Pipes are Angularโ€™s answer โ€” small, declarative transforms applied in the template, at the point of display:

{{ order.totalPaise / 100 | currency:'INR' }} โ†’ โ‚น4,999.00
{{ order.placedAt | date:'dd MMM, h:mm a' }} โ†’ 19 Jul, 4:30 PM

They look like a garnish; they encode a real architectural stance โ€” display formatting belongs to the view layer, not the data. This guide covers the built-ins, writing your own, the purity/memoization model that makes pipes fast, and the honest comparison with computed signals now that both exist.


The Pipe Idea, and Why It Beats Alternatives

The | syntax chains a value through named transforms, left to right, with : for arguments:

{{ user.name | uppercase }}
{{ price | currency:'INR':'symbol':'1.0-0' }} <!-- multiple args -->
{{ order.note | slice:0:80 | lowercase }} <!-- chaining -->

Consider the alternatives for the same job. Formatting in the component class (this.displayPrice = format(price)) duplicates state โ€” now the raw and formatted values must be kept in sync, the stale-derived-state disease. Formatting in the data model (storing "โ‚น4,999.00" from the API) welds presentation into data, breaking the moment a second view needs a different format or locale. Template method calls ({{ format(price) }}) work but re-execute every change-detection pass. Pipes dodge all three: raw data stays raw, formatting is declared at the point of use, and purity (below) gives memoization for free.


The Built-Ins Worth Knowing Cold

{{ paise / 100 | currency:'INR' }} <!-- โ‚น formatting, locale-aware -->
{{ 0.1846 | percent:'1.1-1' }} <!-- 18.5% -->
{{ views | number:'1.0-0' }} <!-- 1,24,567 (locale grouping!) -->
{{ ts | date:'mediumDate' }} <!-- 19 Jul 2026 -->
{{ name | titlecase }} {{ code | uppercase }}
{{ description | slice:0:120 }}
{{ debugObject | json }} <!-- dev-time object dumps -->

The digit-info mini-language ('1.0-2' = min 1 integer digit, 0โ€“2 decimals) covers most numeric-precision needs. Two things elevate this from trivia:

Locale awareness is built in. currency, number, date, and percent all format per the appโ€™s configured locale โ€” Indian digit grouping (1,24,567), month names, currency symbols. Apps that hand-roll formatting with string concatenation rediscover locale bugs one market at a time; pipes route through the i18n machinery from day one. (One sharp edge inherited from JS itself: date accepts ISO strings, but store and transport timestamps properly โ€” parsing ambiguity is upstream of any pipe.)

The async pipe โ€” historically the most important pipe in Angular โ€” subscribes to an Observable/Promise in the template and unsubscribes automatically:

@if (user$ | async; as user) { {{ user.name }} }

In signal-forward code its role shrinks (signals are read directly), but it remains the bridge wherever Observables surface, and a decade of codebases lean on it โ€” full treatment in the RxJS guide.


Writing Custom Pipes

The candidates announce themselves โ€” a formatting rule appearing in three templates. Structure:

import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'timeAgo' })
export class TimeAgoPipe implements PipeTransform {
transform(value: string | Date): string {
const then = new Date(value).getTime();
const mins = Math.round((Date.now() - then) / 60_000);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins} min ago`;
const hrs = Math.round(mins / 60);
if (hrs < 24) return `${hrs} hr ago`;
return `${Math.round(hrs / 24)} d ago`;
}
}
{{ comment.createdAt | timeAgo }} <!-- after importing TimeAgoPipe -->

A pipe is a class with one method โ€” transform(value, ...args) โ€” pure-function discipline in a decorator. Design notes from production pipes:

The timeAgo example carries one instructive flaw: time moves, but the pipe re-runs only when its input changes (โ€œpurity,โ€ next section) โ€” so โ€œ2 min agoโ€ can silently stay stale. Real implementations pair the pipe with a ticking signal input or interval-driven refresh โ€” a perfect illustration that pipes model pure functions of their inputs, and time is an input you must make explicit.


Purity: The Performance Model

By default pipes are pure: Angular re-invokes transform only when the input value (or an argument) changes by reference. Same input โ†’ cached output, skipped work โ€” thousands of bindings stay cheap because unchanged rows donโ€™t re-format.

The classic gotcha follows directly: mutate an array/object input and a pure pipe wonโ€™t notice โ€” the reference didnโ€™t change:

this.items().push(newItem); // sorted-by-pipe list doesn't update!
this.items.update((xs) => [...xs, newItem]); // new reference โ†’ pipe re-runs โœ“

If your codebase follows immutable-update discipline โ€” as signal-era Angular pushes you to โ€” purity is free correctness. The escape hatch, pure: false, re-runs the pipe every change-detection cycle: occasionally legitimate (the built-in impure async pipe manages subscriptions), usually a smell that either the data flow should be immutable or the transform belongs in a computed.

Pipes vs computed signals โ€” the modern judgment call

Both memoize derived values; the split thatโ€™s emerging as idiomatic:


Building the Projectโ€™s Formatting Vocabulary

The compounding value of pipes arrives when a team treats them as a curated set rather than ad-hoc utilities. A realistic core vocabulary for a commerce app, and the thinking behind each:

{{ order.totalPaise | inr }} // wraps currency:'INR' + the /100 convention โ€”
// the paise-vs-rupees bug becomes unwritable
{{ order.placedAt | timeAgo }} // relative time, one implementation
{{ order.status | orderStatusLabel }} // enum โ†’ human copy: 'PARTIALLY_SHIPPED' โ†’ 'Partially shipped'
{{ sku | truncateMiddle:12 }} // 'KB-MECHโ€ฆ-BLK' โ€” ends matter for SKUs
{{ user.phone | maskPhone }} // '98โ€ขโ€ขโ€ขโ€ขโ€ข210' โ€” privacy display rule, centralized

Two of these illustrate design points worth stealing. The inr pipe encodes a convention โ€” the codebase stores money in paise (as it should), and by owning the division and formatting in one place, the class of off-by-100 display bugs from the self-check simply canโ€™t be written. The orderStatusLabel pipe centralizes copy โ€” when marketing renames a status, one map changes, and (crucially for i18n later) thereโ€™s exactly one seam where translation slots in.

Team practices that keep the vocabulary healthy: pipes live in one shared/pipes folder with tests beside them (each test file is a specification of the format โ€” genuinely useful documentation); new formatting logic in a component triggers the review question โ€œexisting pipe, new pipe, or computed?โ€; and pipe names are unprefixed domain words (inr, timeAgo) since selector-collision risk is low and template readability is the whole point. A mature appโ€™s templates read like the domain โ€” {{ order.totalPaise | inr }} needs no comment โ€” and that legibility is pipes doing their quiet architectural work.

Frequently Asked Questions

How do pipes work with the new control flow and signals? Seamlessly โ€” {{ items() | slice:0:5 }} reads a signal and pipes the value; @for (x of filtered() | ...) composes too. Pipes transform values; where the value came from is irrelevant.

Can pipes take other pipesโ€™ output? Thatโ€™s chaining โ€” | slice:0:80 | uppercase โ€” evaluated left to right, each transform feeding the next, function composition in template clothing.

Why does my date pipe show the wrong day near midnight? Timezones โ€” the pipe formats in the viewerโ€™s zone by default (an explicit zone is the optional third argument). Store UTC, format locally, and test boundary times; the bug is almost never the pipe.

Is there a pipe for sorting/filtering lists? Deliberately not built in โ€” the old AngularJS filters caused per-cycle re-sorts of big lists. Sort/filter in a computed (memoized, explicit) and keep pipes for formatting. Writing your own sort pipe is possible and usually the wrong call.

How do standalone pipes get used? Like components: imports: [TimeAgoPipe] in whatever componentโ€™s template uses them. Group your appโ€™s formatting pipes in one folder; they become the projectโ€™s display vocabulary.


Locale Configuration: Making the Built-Ins Speak Your Market

Since the built-insโ€™ locale-awareness was a headline claim, the two-minute setup that activates it: Angular ships with en-US data only; other locales register explicitly:

import { registerLocaleData } from '@angular/common';
import localeEnIn from '@angular/common/locales/en-IN';
registerLocaleData(localeEnIn);
// app.config.ts providers:
{ provide: LOCALE_ID, useValue: 'en-IN' }

With en-IN active, {{ 12345678 | number }} renders 1,23,45,678 (lakh/crore grouping), date uses day-month ordering, and currency defaults sensibly โ€” every pipe in the app, one configuration. Per-call overrides remain (| currency:'USD':'symbol':'1.2-2':'en-US') for multi-currency displays. Two gotchas from the field: forgetting registerLocaleData produces a runtime error naming the missing locale (clear, at least), and mixing hard-coded formatting (toFixed, manual comma insertion) with pipe formatting produces the subtly inconsistent UI that locale users spot instantly โ€” once the pipes own formatting, let them own all of it. Full multi-language i18n (translated text, not just formats) is a separate machinery (@angular/localize or runtime libraries) โ€” but it plugs into exactly the seams this guide built: pipes and centralized label maps.

Worth one closing sentence each: pipes work anywhere template expressions do โ€” inside [input] bindings ([items]="orders() | slice:0:5"), @if conditions, and @for sources, not just interpolation; and a pipe throwing inside a template surfaces as a rendering error naming the pipe, which is why the nullish-guarding habit is really an error-budget decision โ€” a formatting failure should degrade to empty, not take the view down with it.

Self-Check

  1. {{ order.paise | currency:'INR' }} displays โ‚น499,900.00 for a โ‚น4,999 order. Whatโ€™s the bug? (Hint: itโ€™s not the pipe.)
  2. A filterActive pipe on a list stops updating when itemsโ€™ active flags are toggled via mutation. Explain via purity, and give both fixes.
  3. Why is {{ heavyFormat(row.value) }} in a 500-row table worse than {{ row.value | heavyFormat }}?
  4. You need โ€œtruncate to N chars with ellipsisโ€ in six components. Pipe or computed โ€” and why?
  5. What makes pipes so easy to unit test compared to components?

(Answers: paise passed as rupees โ€” divide by 100 first (or better: a paiseToInr pipe encoding the convention once). Pure pipes re-run on reference change; mutation keeps the reference โ€” fix with immutable updates (update + spread) or, less well, pure: false. The method re-runs for all rows every CD cycle; the pure pipe re-runs only for rows whose value reference changed. Pipe โ€” cross-component display vocabulary, parameterized by N. Theyโ€™re pure transform functions โ€” instantiate, call, assert; no rendering machinery.)

That closes the components arc. Next section โ€” the application backbone, starting with Services and Dependency Injection: where non-UI logic lives, inject(), provider scopes, and why DI is the feature enterprises quietly love most.