Skip to main content

Трубки в Angular (вбудовані та користувацькі)

Pipes в Angular перетворюють дані в шаблонах: значення проходить через метод transform() і повертається відформатованим для відображення. Властивість компонента при цьому не змінюється.

Теорія

TL;DR

  • Pipe схожий на кавовий фільтр: "сирі" дані заходять, відформатовані виходять
  • Вбудовані pipes (date, currency, uppercase) покривають стандартне форматування без додаткового коду
  • Власні pipes - це класи з методом transform(), позначені декоратором @Pipe
  • Pipes об'єднуються в ланцюжок через |: {{ value | pipe1 | pipe2 }}
  • За замовчуванням pipe є чистим (pure): Angular запускає його лише при зміні посилання на вхідні дані

Швидкий приклад

html
<!-- Вбудовані 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.142
  • uppercase, lowercase, titlecase - перетворення регістру
  • slice - обрізає масиви або рядки: {{ 'Angular' | slice:0:3 }} дає Ang
  • json - серіалізує об'єкти для налагодження: <pre>{{ user | json }}</pre>
  • async - підписується на Observable або Promise і автоматично очищає підписку

Власні pipes

Щоб створити власний pipe, потрібно імплементувати PipeTransform і додати декоратор @Pipe:

typescript
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; } }
html
<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-проєктах зустрічається найчастіше.

typescript
@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 нечистим

typescript
// Неправильно - запускається на кожному циклі виявлення змін @Pipe({ name: 'formatLabel', pure: false }) // Правильно - безстановому форматуванню нечистий pipe не потрібен @Pipe({ name: 'formatLabel' }) // pure: true за замовчуванням

На списку з 1000 елементів pipe із pure: false викликає transform() 1000 разів за цикл. Це вже не питання форматування, це питання продуктивності.

Помилка 2: мутація вхідних даних усередині pipe

typescript
// Неправильно - сортуємо оригінальний масив 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

typescript
// 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 у ланцюжку

typescript
// Неправильно - 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 для стандартного форматування

html
<!-- Дати --> <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 для форматування номера телефону

typescript
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; } }
html
<p>{{ '5551234567' | phoneFormat }}</p> <!-- (555) 123-4567 --> <p>{{ '' | phoneFormat }}</p> <!-- порожній рядок, без помилок -->

Перевірка на порожнє значення в рядку 5 - не випадкова. Pipe має обробляти некоректні вхідні дані без збоїв, бо реальні дані часто непередбачувані. При помилці форматування повертай оригінальне значення, а не кидай виняток.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?