Skip to main content

Декоратори @input та @output в Angular

@Input і @Output - декоратори Angular для зв'язку між компонентами: @Input передає дані від батька до дитини, @Output надсилає події від дитини до батька.

Теорія

TL;DR

  • @Input = батько передає дані дочірньому компоненту; дитина читає їх, але не може змінити оригінал у батька
  • @Output = дитина викидає подію вгору через EventEmitter; батько сам вирішує що з нею робити
  • Аналогія: батько передає записку дитині (@Input); дитина кричить "Готово!" у відповідь (@Output)
  • Використовуй обидва для презентаційних компонентів; для спільного або глобального стану - сервіс або NgRx

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

typescript
// child.component.ts import { Component, Input, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'app-child', template: `<button (click)="notify()">Готово</button>` }) export class ChildComponent { @Input() message = 'Привіт'; // отримує від батька @Output() done = new EventEmitter<string>(); // надсилає батьку notify() { this.done.emit('Завдання виконано!'); } }
html
<!-- parent.component.html --> <app-child [message]="parentMsg" (done)="onDone($event)"></app-child>

[message] - прив'язка властивості, дані течуть всередину. (done) - прив'язка події, метод батька спрацьовує коли дитина викликає .emit().

Головна різниця

@Input створює односпрямовану прив'язку: коли значення в батьку змінюється, Angular автоматично оновлює дитину в наступному циклі change detection. @Output працює навпаки - дитина тримає EventEmitter і викликає .emit(), щоб запустити метод батька. Дитина не знає що батько зробить з тією подією, що й забезпечує слабке зв'язування між компонентами.

Коли використовувати

  • Передати дані профілю користувача в картку відображення → @Input
  • Кнопка видалення в елементі списку має повідомити батька → @Output
  • Поле форми, що показує значення і повідомляє про зміни → обидва
  • Два компоненти-сусіди мають спільний стан або стан потрібен у кількох фічах → сервіс із BehaviorSubject або NgRx

Як Angular обробляє це всередині

Angular обробляє @Input і @Output під час компіляції шаблону, перетворюючи їх на прив'язки властивостей і слухачі подій. Коли вхідне значення змінюється, Angular викликає ngOnChanges на дочірньому компоненті. @Output обгортає EventEmitter у zone.js-патчені події, тому кожен .emit() планує новий цикл change detection, а не запускає його синхронно. Це запобігає нескінченним циклам.

Типові помилки

Забуті дужки в прив'язці виходу:

html
<!-- Неправильно: Angular сприймає це як прив'язку властивості --> <app-child done="handle()"></app-child> <!-- Правильно --> <app-child (done)="handle($event)"></app-child>

Пряма мутація масиву або об'єкта з @Input у дитині:

typescript
// Неправильно: батько не дізнається що масив змінився @Input() items: string[] = []; ngOnInit() { this.items.push('new'); } // Правильно: викидай оновлену колекцію через @Output @Output() itemsChange = new EventEmitter<string[]>(); add(item: string) { this.itemsChange.emit([...this.items, item]); }

Прив'язка односпрямована - посилання батька не оновлюється. Це одна з найчастіших пасток для розробників, що переходять з React, де пропси за конвенцією незмінні.

@Output без EventEmitter:

typescript
// Неправильно: TypeScript скомпілює, але рантайм впаде @Output() done: string; // Правильно @Output() done = new EventEmitter<string>();

Відсутність transform для булевих атрибутів:

html
<!-- Неправильно: передає рядок "true", а не булеве значення --> <app-button [disabled]="'true'"></app-button>
typescript
// Правильно: booleanAttribute конвертує рядок у справжнє boolean @Input({ transform: booleanAttribute }) disabled = false;

Де зустрічається

  • Angular Material: mat-slider використовує @Input() value і @Output() valueChange для двостороннього зв'язку
  • PrimeNG: p-table приймає @Input() options і генерує @Output() onRowSelect
  • NG Bootstrap: ngb-datepicker приймає модель через @Input і кидає @Output() navigate при зміні місяця
  • Вбудована опція required (з Angular 14.3+): @Input({ required: true }) id!: string кидає NG0303 в dev-режимі якщо батько не прив'язав вхід

Питання на співбесіді

Q: Яка різниця між @Input і прив'язкою властивості?
A: @Input позначає властивість класу як таку, що приймає значення ззовні компонента. Прив'язка [prop]="value" в шаблоні батька - це спосіб передати це значення. Одне - оголошення, інше - використання.

Q: Як ChangeDetectionStrategy.OnPush взаємодіє з @Input?
A: З OnPush Angular перевіряє компонент лише коли змінюється посилання @Input (не глибока мутація) або коли спрацьовує подія. Якщо мутувати масив з @Input без нового посилання, компонент не перерендериться.

Q: Що робить @Input({ required: true })?
A: Доступний з Angular 14.3+. Компілятор кидає NG0303 під час збірки якщо батько не прив'язав цей вхід. Корисно для пропсів, що не мають сенсу без значення, наприклад id рядка в таблиці даних.

Q: Чому EventEmitter, а не звичайний Subject для @Output?
A: Прив'язка подій у шаблоні Angular розрахована на роботу з EventEmitter. Звичайний Subject запрацює в рантаймі, але EventEmitter чітко комунікує намір і відповідає конвенціям Angular API.

Q: Коли передача даних через @Input/@Output стає проблемою і що тоді робити?
A: Щойно дані мають пройти більш ніж через два рівні вкладеності або два компоненти-сусіди потребують одного стану, все стає незручним. Стандартне рішення - спільний сервіс із BehaviorSubject. Додай ChangeDetectionStrategy.OnPush на компоненти щоб скоротити зайві перерендери. Для великого стану між фічами - NgRx або Angular Signals store.

Приклади

Базовий: лічильник із двостороннім зв'язком

typescript
// counter.component.ts @Component({ selector: 'app-counter', template: ` <p>Count: {{ count }}</p> <button (click)="increment()">+</button> ` }) export class CounterComponent { @Input() count = 0; @Output() countChange = new EventEmitter<number>(); increment() { this.countChange.emit(this.count + 1); // не мутуємо count напряму } }
html
<!-- parent.component.html --> <app-counter [count]="counter" (countChange)="counter = $event"></app-counter>

Дитина ніколи не змінює власний count. Вона викидає нове значення, батько оновлює джерело правди. Якщо назвати пару входу/виходу value і valueChange, синтаксис [(value)] для двостороннього зв'язку запрацює автоматично.

Середній: список користувачів із кількома виходами

typescript
// user-item.component.ts @Component({ selector: 'app-user-item', template: ` <div> {{ user.name }} ({{ user.active ? 'Активний' : 'Неактивний' }}) <button (click)="toggle()">Перемикач</button> <button (click)="remove()">Видалити</button> </div> ` }) export class UserItemComponent { @Input() user!: { name: string; active: boolean }; @Output() userToggled = new EventEmitter<{ name: string; active: boolean }>(); @Output() userDeleted = new EventEmitter<string>(); toggle() { this.userToggled.emit({ ...this.user, active: !this.user.active }); } remove() { this.userDeleted.emit(this.user.name); } }
html
<!-- parent.component.html --> <app-user-item *ngFor="let u of users" [user]="u" (userToggled)="updateUser($event)" (userDeleted)="deleteUser($event)" ></app-user-item>

Один компонент, два виходи. Батько тримає масив users і відповідає за його оновлення. Дитина знає тільки свій user і які кнопки натиснули. Бізнес-логіка залишається поза компонентом відображення.

Розширений: обов'язковий вхід з аліасом і transform

typescript
// data-row.component.ts @Component({ selector: 'app-data-row', template: `<p>ID: {{ id }} | Disabled: {{ disabled }}</p>` }) export class DataRowComponent { @Input({ required: true, alias: 'rowId' }) id!: string; @Input({ transform: booleanAttribute }) disabled = false; }
html
<!-- Працює --> <app-data-row [rowId]="'abc-123'" disabled></app-data-row> <!-- Кидає NG0303 в dev: Missing required input 'rowId' --> <app-data-row></app-data-row>

required: true змушує компілятор зловити відсутню прив'язку ще до рантайму. alias дозволяє шаблону використовувати rowId, тоді як властивість класу називається id. booleanAttribute вирішує класичне перетворення рядка в булеве значення: атрибут disabled без явного значення стає true, а не рядком "true".

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

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

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

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