Трубки в Angular (вбудовані та користувацькі)
Pipes в Angular перетворюють дані в шаблонах: значення проходить через метод transform() і повертається відформатованим для відображення. Властивість компонента при цьому не змінюється.
Теорія
TL;DR
- Pipe схожий на кавовий фільтр: "сирі" дані заходять, відформатовані виходять
- Вбудовані pipes (
date,currency,uppercase) покривають стандартне форматування без додаткового коду - Власні pipes - це класи з методом
transform(), позначені декоратором@Pipe - Pipes об'єднуються в ланцюжок через
|:{{ value | pipe1 | pipe2 }} - За замовчуванням pipe є чистим (pure): Angular запускає його лише при зміні посилання на вхідні дані
Швидкий приклад
<!-- Вбудовані pipes -->
{{ today | date:'short' }} <!-- 1/15/26, 2:30 PM -->
{{ price | currency:'USD' }} <!-- $1,234.50 -->
{{ 'hello world' | uppercase }} <!-- HELLO WORLD -->
<!-- Ланцюжок: форматуємо валюту, потім обрізаємо рядок -->
{{ price | currency:'USD' | slice:0:7 }} <!-- $1,234. -->Pipe не торкається оригінального значення. Властивість компонента залишається незмінною, pipe впливає лише на те, що бачить шаблон.
Відображення проти логіки
Pipes форматують дані для відображення, а не для бізнес-логіки. Якщо перетворене значення потрібне в TypeScript-коді (для API, стану або обчислень), роби це в методі компонента чи сервісі. Форматування номера телефону для відображення - хороший кейс. Підрахунок річної зарплати з урахуванням податків через pipe - ні.
Вбудовані pipes
Angular постачається з pipes для найпоширеніших задач форматування:
date- форматує дати патернами:'short','fullDate','dd/MM/yyyy'currency- форматує числа як гроші з підтримкою локалі та символівnumber(DecimalPipe) - контролює кількість знаків після коми:{{ 3.14159 | number:'1.2-3' }}дає3.142uppercase,lowercase,titlecase- перетворення регіструslice- обрізає масиви або рядки:{{ 'Angular' | slice:0:3 }}даєAngjson- серіалізує об'єкти для налагодження:<pre>{{ user | json }}</pre>async- підписується на Observable або Promise і автоматично очищає підписку
Власні pipes
Щоб створити власний pipe, потрібно імплементувати PipeTransform і додати декоратор @Pipe:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate',
standalone: true
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 50, trail = '...'): string {
if (!value) return '';
if (value.length <= limit) return value;
return value.substring(0, limit) + trail;
}
}<p>{{ longText | truncate }}</p> <!-- 50 символів... -->
<p>{{ longText | truncate:20 }}</p> <!-- 20 символів... -->
<p>{{ longText | truncate:30:'→' }}</p> <!-- 30 символів→ -->name в @Pipe - це те, що пишеш у шаблоні після |. Додаткові параметри через двокрапки в шаблоні стають наступними аргументами в transform(value, arg1, arg2, ...).
Чисті та нечисті pipes
За замовчуванням кожен pipe є чистим (pure). Angular запускає його лише коли змінюється посилання на вхідні дані. Тобто {{ items | sort }} не перезапуститься, якщо додати елемент у той самий масив через push, бо посилання не змінилось.
| Чистий (за замовчуванням) | Нечистий (pure: false) | |
|---|---|---|
| Запускається коли | Змінюється посилання на вхід | Кожен цикл виявлення змін |
| Продуктивність | Ефективно | Може гальмувати |
| Кейс | Безстанове форматування | Залежить від зовнішнього стану |
Нечисті pipes потрібні, коли результат залежить від чогось поза вхідними даними, наприклад поточного часу. Але вони запускаються постійно. На списку з 500 елементів - 500 викликів transform() за цикл. Використовуй рідко, а для живих оновлень краще зроби сервіс з interval() через RxJS.
AsyncPipe
async підписується на Observable або Promise і автоматично відписується, коли компонент знищується. Без нього доводиться вручну керувати підписками, що легко забути і отримати витік пам'яті. На практиці це pipe, який в реальних Angular-проєктах зустрічається найчастіше.
@Component({
template: `
<div *ngIf='user$ | async as user'>
{{ user.name }}
</div>
`
})
export class UserComponent {
user$ = this.userService.getUser(1);
constructor(private userService: UserService) {}
}Жодного subscribe(), жодного unsubscribe(), жодного ngOnDestroy. Pipe сам усе це вирішує.
Типові помилки
Помилка 1: зробити безстановий pipe нечистим
// Неправильно - запускається на кожному циклі виявлення змін
@Pipe({ name: 'formatLabel', pure: false })
// Правильно - безстановому форматуванню нечистий pipe не потрібен
@Pipe({ name: 'formatLabel' }) // pure: true за замовчуваннямНа списку з 1000 елементів pipe із pure: false викликає transform() 1000 разів за цикл. Це вже не питання форматування, це питання продуктивності.
Помилка 2: мутація вхідних даних усередині pipe
// Неправильно - сортуємо оригінальний масив
transform(items: any[]): any[] {
return items.sort((a, b) => a.name.localeCompare(b.name));
}
// Правильно - повертаємо новий масив
transform(items: any[]): any[] {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}Чисті pipes розраховують на незмінні вхідні дані. Сортування оригінального масиву непомітно змінює дані компонента і порушує роботу виявлення змін.
Помилка 3: забути зареєструвати власний pipe
// Standalone компонент - імпортуємо pipe
@Component({
imports: [TruncatePipe],
template: `<p>{{ text | truncate }}</p>`
})
// NgModule - декларуємо
@NgModule({
declarations: [TruncatePipe]
})Якщо pipe не зареєстровано, Angular кине помилку компіляції: The pipe 'truncate' could not be found.
Помилка 4: неправильний порядок pipes у ланцюжку
// Неправильно - currency повертає рядок, а number очікує число
{{ price | currency | number:'1.2-2' }}
// Правильно - спочатку числове форматування, потім валюта
{{ price | number:'1.2-2' | currency:'USD' }}Вихід кожного pipe стає входом наступного. Відстежуй тип даних на кожному кроці ланцюжка.
Де зустрічається
- Дати в таблицях і картках:
{{ row.createdAt | date:'mediumDate' }} - Відображення цін:
{{ item.price | currency:'EUR' }} - Дані з Observable без ручних підписок:
{{ users$ | async }} - Обрізання довгих описів у UI (власний
TruncatePipe) - Форматування телефонів і артикулів в кількох шаблонах одразу (власний
PhoneFormatPipe) - Налагодження стану під час розробки:
<pre>{{ data | json }}</pre>
Правило вибору: якщо те саме форматування зустрічається в 2 і більше шаблонах, створи pipe. Якщо це одноразове форматування, вистачить методу компонента.
Питання на співбесіді
Q: Яка різниця між чистим і нечистим pipe, і коли ти б обрав нечистий?
A: Чистий pipe запускається лише при зміні посилання на вхідні дані. Нечистий - на кожному циклі виявлення змін. Нечистий потрібен тільки коли результат залежить від зовнішнього стану, що змінюється незалежно, наприклад поточного часу. Для всього безстанового форматування тримай pipe чистим.
Q: Чи можна викликати pipe в TypeScript-коді, а не лише в шаблонах?
A: Так. Можна створити екземпляр класу напряму: new DatePipe('en-US').transform(date, 'short'). Але це рідкість. Зазвичай трансформуєш дані в компоненті або сервісі, а pipe використовуєш лише для відображення.
Q: Якщо застосувати власний pipe до 1000 елементів списку, Angular запустить transform() 1000 разів?
A: При першому рендері - так. Потім чистий pipe кешує результат для кожного посилання і перезапускається лише для елементів, чиє посилання змінилось. Нечистий запускається всі 1000 разів за кожен цикл. Саме тому pure: false і великі списки погано поєднуються.
Q: Як передати кілька параметрів у pipe?
A: Розділяй двокрапками: {{ value | date:'fullDate':'UTC' }}. Вхідне значення - це перший аргумент transform(), а кожне значення після : передається як наступний аргумент за порядком.
Приклади
Базовий: вбудовані pipes для стандартного форматування
<!-- Дати -->
<p>{{ today | date:'short' }}</p> <!-- 1/15/26, 2:30 PM -->
<p>{{ today | date:'fullDate' }}</p> <!-- Thursday, January 15, 2026 -->
<p>{{ today | date:'dd/MM/yyyy' }}</p> <!-- 15/01/2026 -->
<!-- Валюта і числа -->
<p>{{ price | currency }}</p> <!-- $1,234.50 -->
<p>{{ price | currency:'EUR' }}</p> <!-- €1,234.50 -->
<p>{{ 3.14159 | number:'1.2-3' }}</p> <!-- 3.142 -->
<!-- Регістр тексту -->
<p>{{ 'hello world' | uppercase }}</p> <!-- HELLO WORLD -->
<p>{{ 'hello world' | titlecase }}</p> <!-- Hello World -->
<!-- Ланцюжок pipes -->
<p>{{ birthday | date:'fullDate' | uppercase }}</p>
<!-- THURSDAY, JANUARY 15, 2026 -->Ці pipes покривають більшість задач форматування в щоденній роботі з Angular. Без зайвої логіки в компоненті.
Середній рівень: власний pipe для форматування номера телефону
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'phoneFormat',
standalone: true
})
export class PhoneFormatPipe implements PipeTransform {
transform(value: string): string {
if (!value || value.length < 10) return value;
const cleaned = value.replace(/\D/g, '');
const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/);
return match ? `(${match[1]}) ${match[2]}-${match[3]}` : value;
}
}<p>{{ '5551234567' | phoneFormat }}</p>
<!-- (555) 123-4567 -->
<p>{{ '' | phoneFormat }}</p>
<!-- порожній рядок, без помилок -->Перевірка на порожнє значення в рядку 5 - не випадкова. Pipe має обробляти некоректні вхідні дані без збоїв, бо реальні дані часто непередбачувані. При помилці форматування повертай оригінальне значення, а не кидай виняток.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.