Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Pipes  ›  Capstone Project

Custom Pipes and Pure Change Detection

Custom Pipes 15 minA custom pipe is a class with the @Pipe decorator and a transform method, and because pipes are pure by default Angular re-runs one only when its input value or reference changes.

Custom Pipes and Pure Change Detection

Greenhouse keeps writing "Water every 1 days". You want "1 day" and "3 days", everywhere, without an @if in every template. When no built-in pipe does what you need, you write one. It takes a few lines, plus one idea about when Angular runs it.

The rule

A pipe is a pure function in a class; same input, no rerun.

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

@Pipe({ name: 'countLabel' })
export class CountLabelPipe implements PipeTransform {
  transform(count: number, word: string): string {
    return `${count} ${word}${count === 1 ? '' : 's'}`;
  }
}
  • @Pipe({ name }) gives the name used in templates, in camelCase.
  • transform receives the value on the left of | first, then each argument after a colon.
  • Like components, pipes are standalone: import the class where you use it.
<p>Water every {{ plant().everyDays | countLabel:'day' }}</p>

Pure by default. Angular treats a pipe as a pure function: the same inputs always give the same output. So it caches the result and re-runs transform only when an input changes, meaning a new primitive value or a new object reference. That makes pipes cheap, even in long lists.

The catch comes with objects and arrays. Suppose a pure thirstyOnly pipe filters the plant array:

this.plants().push(newPlant);                       // same array: pipe does NOT re-run
this.plants.update(list => [...list, newPlant]);    // new array: pipe re-runs

Mutating in place keeps the same reference, so Angular sees "same input" and shows the old result. Replace instead of mutate, and everything updates.

You can mark a pipe pure: false to run it on every change detection pass, but it then runs very often. For filtering and sorting, a computed signal in the class is usually the better tool:

thirsty = computed(() => this.plants().filter(p => p.daysSince > p.everyDays));

The shortcut

Write a pipe in three moves: name it, write transform(value, ...args), import it. Then ask "will my input ever change without becoming a new object?" If yes, switch to immutable updates or a computed signal rather than an impure pipe.

Building countLabelStep 1 of 4
  1. Name it

    @Pipe({ name: 'countLabel' }) so templates write | countLabel.

Drills

Answer fast — trust the rule.

quick drill

In {{ 4 | countLabel:'pot' }}, what does transform receive?

quick drill

By default, when does Angular re-run a pure pipe?

quick drill

A pure filter pipe does not update after this.plants().push(p). Why?

quick drill

What is usually the best fix for that filtered list?

Remember

  • A custom pipe: @Pipe({ name }), a transform(value, ...args) method, and an import where used.
  • Pure by default: it re-runs only for a new value or reference.
  • Trap: mutating arrays in place; replace them, or filter with computed instead.

Practice — level up

0/1 solved

Write your answer, then compare it with a rubric and a model answer.

ExplainBuild countLabel, then break it

Write a countLabel pipe for Greenhouse so that {{ 3 | countLabel:'day' }} shows '3 days' and {{ 1 | countLabel:'day' }} shows '1 day'. Then explain why a pure thirstyOnly pipe over the plants array might not update after plants().push(newPlant), and give a fix.