1. Как работает @Output() и EventEmitter
Давайте представим: у вас есть компонент кнопки <app-like-button>, который инкапсулирует внутри себя логику подсчёта лайков. Вы хотите, чтобы при каждом нажатии на кнопку родительский компонент (например, карточка поста) знал, что лайк был поставлен, и мог, например, посчитать общее число лайков или отправить информацию на сервер.
Вот тут и появляется задача: передать событие от дочернего компонента к родительскому. В Angular для этого используется связка @Output() + EventEmitter.
Если проводить аналогию с реальной жизнью:
- @Input() — это когда начальник даёт задание подчинённому (передача данных сверху вниз).
- @Output() — это когда подчинённый докладывает начальнику о проделанной работе (передача событий снизу вверх).
Основная идея
- @Output() — специальный декоратор, который помечает свойство компонента как «выходное», то есть позволяющее испускать (эмиттить) события наружу.
- EventEmitter — класс, с помощью которого внутри компонента мы можем «испускать» (emit) события и передавать вместе с ними любые данные.
Когда родительский компонент размещает дочерний компонент в шаблоне, он может подписаться на событие через синтаксис (названиеСобытия)="обработчик($event)".
Схема работы
[Родитель]
↑ (подписка на событие)
[Дочерний компонент] --emit--> [Родительский обработчик]
2. Минимальный пример: кнопка лайка
Дочерний компонент (like-button.component.ts)
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-like-button',
template: ``
})
export class LikeButtonComponent {
@Output() liked = new EventEmitter<void>();
like() {
// Сообщаем родителю, что кликнули лайк
this.liked.emit();
}
}
Пояснения:
@Output() liked = new EventEmitter<void>() — создаёт выходное событие liked, которое ничего не передаёт (тип void).
В шаблоне вызывается метод like() по клику.
Внутри метода like() мы вызываем this.liked.emit(), что означает: «Эй, родитель! Лайкнули!»
Родительский компонент (post-card.component.html)
<app-like-button (liked)="onLiked()"></app-like-button>
<p>Лайков: {{ likes }}</p>
Родительский компонент (post-card.component.ts)
import { Component } from '@angular/core';
@Component({
selector: 'app-post-card',
templateUrl: './post-card.component.html'
})
export class PostCardComponent {
likes = 0;
onLiked() {
this.likes++;
}
}
Пояснения:
В шаблоне родителя мы пишем (liked)="onLiked()" — это подписка на событие liked дочернего компонента.
Каждый раз, когда пользователь нажимает на кнопку в дочернем компоненте, вызывается метод onLiked(), который увеличивает счётчик лайков.
3. Передача данных с событием
Иногда просто факта «что-то произошло» мало — нужно передать данные. Например, компонент ввода текста сообщает родителю, что введён новый текст.
Дочерний компонент (input-box.component.ts)
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-input-box',
template: `
<input [(ngModel)]="value" (keyup.enter)="send()" placeholder="Введите текст">
<button (click)="send()">Отправить</button>
`
})
export class InputBoxComponent {
value = '';
@Output() submitted = new EventEmitter<string>();
send() {
if (this.value.trim()) {
this.submitted.emit(this.value);
this.value = '';
}
}
}
Родительский компонент (chat.component.html)
<app-input-box (submitted)="onMessage($event)"></app-input-box>
<ul>
<li *ngFor="let msg of messages">{{ msg }}</li>
</ul>
Родительский компонент (chat.component.ts)
import { Component } from '@angular/core';
@Component({
selector: 'app-chat',
templateUrl: './chat.component.html'
})
export class ChatComponent {
messages: string[] = [];
onMessage(msg: string) {
this.messages.push(msg);
}
}
Пояснения:
В дочернем компоненте событие submitted эмиттит строку — введённый текст.
В родителе мы подписываемся на событие и получаем текст через $event.
4. Типизация событий
Обратите внимание: EventEmitter<T> — это дженерик, где T — тип данных, который будет передан вместе с событием. Это может быть что угодно: строка, число, объект, массив, даже void (если ничего не нужно передавать).
Примеры:
- @Output() deleted = new EventEmitter<number>(); — передаём id удаляемого элемента.
- @Output() selected = new EventEmitter<{id: number, name: string}>(); — передаём объект.
5. Практика: удаление задачи из списка
Давайте добавим в наше учебное приложение список задач с возможностью удалять отдельную задачу через дочерний компонент.
Дочерний компонент (task-item.component.ts)
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-task-item',
template: `
<span>{{ task }}</span>
<button (click)="remove()">Удалить</button>
`
})
export class TaskItemComponent {
@Input() task: string = '';
@Input() index: number = 0;
@Output() removeTask = new EventEmitter<number>();
remove() {
this.removeTask.emit(this.index);
}
}
Родительский компонент (task-list.component.html)
<ul>
<li *ngFor="let t of tasks; let i = index">
<app-task-item
[task]="t"
[index]="i"
(removeTask)="onRemove($event)">
</app-task-item>
</li>
</ul>
Родительский компонент (task-list.component.ts)
import { Component } from '@angular/core';
@Component({
selector: 'app-task-list',
templateUrl: './task-list.component.html'
})
export class TaskListComponent {
tasks: string[] = ['Погладить кота', 'Выучить Angular', 'Сделать кофе'];
onRemove(index: number) {
this.tasks.splice(index, 1);
}
}
Пояснения:
Дочерний компонент получает номер задачи через @Input() index.
При нажатии на кнопку "Удалить" дочерний компонент эмиттит событие removeTask с индексом.
Родительский компонент подписывается на событие и удаляет задачу по индексу.
6. Полезные нюансы
Жизненный цикл: когда события работают
Важно: событие можно эмиттить только после инициализации компонента. Если попытаться вызвать emit() до того, как компонент появился на странице, Angular не сможет обработать событие. Обычно это не проблема, если вы вызываете emit() из обработчиков событий (клик, ввод, и т.д.).
Особенности реализации и best practices
- Именование событий: Обычно события называют в стиле глагола в прошедшем времени: liked, deleted, submitted, selected.
- Типизация: Используйте строгую типизацию для событий. Это поможет избежать ошибок.
- Чистота компонентов: Дочерний компонент не должен знать, что именно делает родитель с событием. Он просто сообщает: «У меня что-то произошло!»
- Можно ли использовать EventEmitter вне @Output?
В теории — да, но на практике EventEmitter предназначен только для связи между компонентами. Для глобальных событий используйте сервисы.
7. Типичные ошибки при работе с @Output() и EventEmitter
Ошибка №1: Несовпадение имён событий и обработчиков.
Если в шаблоне родителя опечататься в названии события, Angular просто проигнорирует подписку. Проверяйте синтаксис: (liked)="onLiked()" — не (like)="onLiked()".
Ошибка №2: Несовпадение типов.
Если дочерний компонент эмиттит строку, а родительский обработчик ожидает число, TypeScript не даст ошибку, но на практике будет баг. Следите за типами!
Ошибка №3: Эмиттить событие до инициализации компонента.
Если вызвать emit() слишком рано (например, в конструкторе), событие не будет поймано родителем. Используйте emit только после появления компонента (обычно в обработчиках событий).
Ошибка №4: Мутировать входные данные напрямую.
Не изменяйте данные, полученные через @Input(), внутри дочернего компонента. Вместо этого эмиттите событие и пусть родитель решает, как менять данные.
Ошибка №5: Использовать EventEmitter для глобальных событий.
EventEmitter — это только для связи родитель-дочерний. Для глобальных событий используйте сервисы и Subject из RxJS.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ