Перейти к содержимому

10. Pipes

🔧 Angular Pipes — трансформация данных в шаблонах

Заголовок раздела «🔧 Angular Pipes — трансформация данных в шаблонах»

Pipes (пайпы) — это фильтры для данных в шаблоне. Они преобразуют значения прямо в {{ }} интерполяции и property-binding, не изменяя оригинальные данные. Думай о них как о конвейере: данные входят в одном виде, выходят в другом.


Без пайпов каждый раз нужно писать методы в компоненте или форматировать данные заранее. Пайпы делают шаблон декларативным и читаемым:

// ❌ Без pipes — загромождённый компонент
@Component({
template: `<p>{{ formatDate(user.birthday) }}</p>`
})
export class UserComponent {
formatDate(d: Date): string {
return d.toLocaleDateString('ru-RU', { year: 'numeric', month: 'long', day: 'numeric' });
}
}
// ✅ С pipes — чистый шаблон
@Component({
template: `<p>{{ user.birthday | date:'d MMMM yyyy':'':'ru' }}</p>`
})
export class UserComponent {}

<!-- Базовый синтаксис: {{ значение | имяПайпа }} -->
{{ 'angular' | uppercase }} <!-- ANGULAR -->
<!-- С аргументами: | имяПайпа:арг1:арг2 -->
{{ price | currency:'RUB':'symbol':'1.2-2' }} <!-- ₽1,000.00 -->
<!-- Цепочка пайпов: применяются слева направо -->
{{ title | trim | titlecase | slice:0:20 }}
<!-- В property binding — без {{ }} -->
<img [src]="imageUrl | safeUrl">
<div [title]="description | slice:0:50">...</div>
<!-- В *ngFor -->
@for (item of items | slice:0:5 | keyvalue; track item.key) {
{{ item.key }}: {{ item.value }}
}

@Component({
template: `
<p>{{ 'hello world' | uppercase }}</p> <!-- HELLO WORLD -->
<p>{{ 'HELLO WORLD' | lowercase }}</p> <!-- hello world -->
<p>{{ 'hello world' | titlecase }}</p> <!-- Hello World -->
<p>{{ longText | slice:0:100 }}...</p> <!-- первые 100 символов -->
<p>{{ obj | json }}</p> <!-- {"key":"value"} — для отладки! -->
`
})
export class TextPipesComponent {
longText = 'Это очень длинный текст который нужно обрезать для превью';
obj = { name: 'Angular', version: 17, stable: true };
}
@Component({
template: `
<!-- DecimalPipe: {минЦелых}.{минДробных}-{максДробных} -->
{{ 3.14159 | number:'1.2-3' }} <!-- 3.142 -->
{{ 1234567 | number:'1.0-0' }} <!-- 1,234,567 -->
{{ 0.001 | number:'1.4-6' }} <!-- 0.001000 -->
<!-- PercentPipe -->
{{ 0.75 | percent }} <!-- 75% -->
{{ 0.1234 | percent:'1.1-2' }} <!-- 12.34% -->
{{ 1.5 | percent:'1.0-0' }} <!-- 150% -->
<!-- CurrencyPipe: (значение | currency:код:символ:формат:локаль) -->
{{ 1000 | currency:'USD' }} <!-- $1,000.00 -->
{{ 1000 | currency:'EUR':'symbol' }} <!-- €1,000.00 -->
{{ 1000 | currency:'RUB':'code' }} <!-- RUB 1,000.00 -->
{{ 1000 | currency:'RUB':'symbol':'1.0-0':'ru' }} <!-- 1 000 ₽ -->
`
})
export class NumberPipesComponent {}
@Component({
template: `
<!-- Предустановленные форматы -->
{{ today | date:'short' }} <!-- 1/15/24, 3:45 PM -->
{{ today | date:'medium' }} <!-- Jan 15, 2024, 3:45:00 PM -->
{{ today | date:'long' }} <!-- January 15, 2024 at 3:45:00 PM GMT+3 -->
{{ today | date:'full' }} <!-- Monday, January 15, 2024 at 3:45:00 PM -->
<!-- Только дата / только время -->
{{ today | date:'shortDate' }} <!-- 1/15/24 -->
{{ today | date:'shortTime' }} <!-- 3:45 PM -->
{{ today | date:'mediumDate' }} <!-- Jan 15, 2024 -->
<!-- Кастомный формат -->
{{ today | date:'dd.MM.yyyy' }} <!-- 15.01.2024 -->
{{ today | date:'HH:mm:ss' }} <!-- 15:45:00 -->
{{ today | date:'d MMMM yyyy' }} <!-- 15 января 2024 (нужна локаль ru) -->
{{ today | date:'EEEE' }} <!-- Monday (день недели) -->
<!-- С timezone и локалью -->
{{ today | date:'medium':'UTC': 'ru' }}
{{ today | date:'d MMMM yyyy':'':'ru' }}
<!-- Строка ISO тоже работает! -->
{{ '2024-03-15T10:30:00' | date:'dd.MM.yyyy HH:mm' }}
`
})
export class DatePipeComponent {
today = new Date();
}

💡 Для русского форматирования нужно зарегистрировать локаль в main.ts:

import { registerLocaleData } from '@angular/common';
import localeRu from '@angular/common/locales/ru';
registerLocaleData(localeRu);

AsyncPipe — самый мощный встроенный пайп. Он автоматически подписывается на Observable/Promise и отписывается при уничтожении компонента, предотвращая утечки памяти.

@Component({
template: `
<!-- Без AsyncPipe — ручная подписка + unsubscribe -->
<p>{{ userName }}</p>
<!-- С AsyncPipe — автоматически! -->
<p>{{ userName$ | async }}</p>
<!-- AsyncPipe с *ngIf для получения значения в переменную -->
<div *ngIf="user$ | async as user">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
<!-- Новый синтаксис Angular 17 -->
@if (user$ | async; as user) {
<h2>{{ user.name }}</h2>
}
<!-- AsyncPipe со списком -->
<ul>
@for (item of items$ | async; track item.id) {
<li>{{ item.name }}</li>
}
</ul>
<!-- Комбинирование с другими пайпами -->
{{ (count$ | async) ?? 0 | number:'1.0-0' }}
`,
// Важно! С AsyncPipe можно использовать OnPush — максимальная производительность
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AsyncPipeComponent implements OnInit {
userName$!: Observable<string>;
user$!: Observable<User>;
items$!: Observable<Item[]>;
count$!: Observable<number>;
constructor(private userService: UserService) {}
ngOnInit(): void {
this.user$ = this.userService.getUser(1);
this.items$ = this.userService.getItems().pipe(
map(items => items.filter(i => i.active))
);
// При уничтожении компонента AsyncPipe сам вызовет unsubscribe()! ✨
}
}
// ❌ Без AsyncPipe — риск утечки памяти!
@Component({})
export class BadComponent implements OnInit, OnDestroy {
user: User | null = null;
private destroy$ = new Subject<void>();
ngOnInit(): void {
this.userService.getUser(1).pipe(
takeUntil(this.destroy$)
).subscribe(user => this.user = user);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
@Component({
template: `
<!-- Объект -->
<dl>
@for (entry of userInfo | keyvalue; track entry.key) {
<dt>{{ entry.key }}</dt>
<dd>{{ entry.value }}</dd>
}
</dl>
<!-- Map -->
@for (entry of configMap | keyvalue; track entry.key) {
{{ entry.key }}: {{ entry.value }}
}
<!-- Кастомная сортировка: 0 = без сортировки -->
@for (entry of obj | keyvalue:originalOrder; track entry.key) {
{{ entry.key }} = {{ entry.value }}
}
`
})
export class KeyValueComponent {
userInfo = {
name: 'Яша',
age: 10,
city: 'Москва',
language: 'TypeScript'
};
configMap = new Map([
['theme', 'dark'],
['lang', 'ru'],
['version', '17']
]);
// Сохранить оригинальный порядок ключей
originalOrder = (): number => 0;
}

Это критически важное различие для производительности!

ХарактеристикаPure PipeImpure Pipe
Вызывается когдаТолько при изменении входного значения (по ссылке)При КАЖДОМ цикле change detection
Производительность✅ Высокая⚠️ Низкая
Мутация входных данныхНе видитВидит
pure: true (default)
Примерыdate, currency, uppercaseasync
// ✅ Pure pipe (по умолчанию) — вызывается только при изменении ссылки
@Pipe({ name: 'filterItems', pure: true }) // pure: true — это дефолт
export class FilterPipe implements PipeTransform {
transform(items: Item[], search: string): Item[] {
console.log('pipe called'); // Будет вызван редко — только при новом массиве
return items.filter(i => i.name.includes(search));
}
}
// Проблема с pure pipe при мутации массива:
addItem() {
this.items.push(newItem); // ❌ Ссылка та же — pipe НЕ видит изменение!
this.items = [...this.items, newItem]; // ✅ Новая ссылка — pipe вызовется
}
// ⚠️ Impure pipe — вызывается при каждой проверке изменений
@Pipe({ name: 'filterImpure', pure: false })
export class FilterImpurePipe implements PipeTransform {
transform(items: Item[], search: string): Item[] {
console.log('pipe called'); // Вызывается постоянно — осторожно!
return items.filter(i => i.name.includes(search));
}
}

🎯 Правило: Используй pure: false только когда нужно реагировать на мутации объектов/массивов. В большинстве случаев достаточно pure pipe + иммутабельные обновления.


<!-- Несколько пайпов применяются слева направо -->
{{ product.name | uppercase | slice:0:15 }}
<!-- Дата + локаль -->
{{ createdAt | date:'d MMMM' | uppercase }} <!-- 15 ЯНВАРЯ -->
<!-- Число отформатировать как валюту и обрезать -->
{{ (price * rate) | number:'1.2-2' }}
<!-- AsyncPipe с трансформацией -->
{{ (price$ | async) | currency:'RUB':'symbol':'1.0-0':'ru' }}
<!-- Null safety: если значение null/undefined, показываем дефолт -->
{{ (user$ | async)?.name | titlecase }}
{{ nullableValue ?? 'N/A' | uppercase }} <!-- оператор ?? работает до pipe -->

PipeСинтаксисПример результата
uppercase'hello' | uppercaseHELLO
lowercase'HELLO' | lowercasehello
titlecase'hello world' | titlecaseHello World
slice'abcde' | slice:1:3bc
json{a:1} | json{"a": 1}
number3.14 | number:'1.1-2'3.14
percent0.75 | percent75%
currency100 | currency:'USD'$100.00
datedate | date:'shortDate'1/15/24
asyncobs$ | async(значение)
keyvalueobj | keyvalue[{key, value}]

// Standalone компонент (Angular 14+) — импортируем нужные pipes
@Component({
standalone: true,
imports: [
DatePipe,
CurrencyPipe,
DecimalPipe,
UpperCasePipe,
AsyncPipe,
JsonPipe,
SlicePipe,
KeyValuePipe,
PercentPipe,
TitleCasePipe,
LowerCasePipe,
],
template: `{{ today | date:'mediumDate' }}`
})
export class MyComponent {
today = new Date();
}
// Или импортируй весь CommonModule (NgModules подход)
@NgModule({
imports: [CommonModule]
})
export class MyModule {}