Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Декоратори @input та @output в Angular». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`@Input`** передає дані від батьківського компонента до дочірнього. **`@Output`** з `EventEmitter` надсилає події від дочірнього назад до батьківського. ```typescript @Input() name = ''; // батько передає @Output() removed = new EventEmitter<string>(); // дитина повідомляє батька ``` ```html <app-card [name]="user.name" (removed)="remove($event)"></app-card> ``` **Ключове:** прив'язка односпрямована в обох випадках. Для двостороннього зв'язку - пара `value` і `valueChange`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`@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"`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.