Декоратори @input та @output в Angular
@Input і @Output - декоратори Angular для зв'язку між компонентами: @Input передає дані від батька до дитини, @Output надсилає події від дитини до батька.
Теорія
TL;DR
@Input= батько передає дані дочірньому компоненту; дитина читає їх, але не може змінити оригінал у батька@Output= дитина викидає подію вгору черезEventEmitter; батько сам вирішує що з нею робити- Аналогія: батько передає записку дитині (
@Input); дитина кричить "Готово!" у відповідь (@Output) - Використовуй обидва для презентаційних компонентів; для спільного або глобального стану - сервіс або NgRx
Швидкий приклад
// 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('Завдання виконано!');
}
}<!-- 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, а не запускає його синхронно. Це запобігає нескінченним циклам.
Типові помилки
Забуті дужки в прив'язці виходу:
<!-- Неправильно: Angular сприймає це як прив'язку властивості -->
<app-child done="handle()"></app-child>
<!-- Правильно -->
<app-child (done)="handle($event)"></app-child>Пряма мутація масиву або об'єкта з @Input у дитині:
// Неправильно: батько не дізнається що масив змінився
@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 скомпілює, але рантайм впаде
@Output() done: string;
// Правильно
@Output() done = new EventEmitter<string>();Відсутність transform для булевих атрибутів:
<!-- Неправильно: передає рядок "true", а не булеве значення -->
<app-button [disabled]="'true'"></app-button>// Правильно: 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.
Приклади
Базовий: лічильник із двостороннім зв'язком
// 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 напряму
}
}<!-- parent.component.html -->
<app-counter [count]="counter" (countChange)="counter = $event"></app-counter>Дитина ніколи не змінює власний count. Вона викидає нове значення, батько оновлює джерело правди. Якщо назвати пару входу/виходу value і valueChange, синтаксис [(value)] для двостороннього зв'язку запрацює автоматично.
Середній: список користувачів із кількома виходами
// 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);
}
}<!-- 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
// 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;
}<!-- Працює -->
<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".
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.