Связь компонентов: input() и output()
Родитель кормит ребёнка данными через input(), а ребёнок зовёт родителя обратно через output() — это весь контракт общения компонентов.
«Данные текут вниз, как вода по дереву; события всплывают вверх, как пузырьки. Нарушите это — получите хаос».
Приложение — это дерево компонентов, и им надо разговаривать. Angular задаёт строгий направленный поток: родитель передаёт данные ребёнку через входы, а ребёнок уведомляет родителя через выходы. В современном Angular входы и выходы построены на сигналах — это новый, более типобезопасный API, заменивший старые декораторы @Input() и @Output().
Объявим карточку товара, которая принимает данные снаружи и сообщает о клике «купить»:
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-product-card',
standalone: true,
template: `
<article>
<h2>{{ title() }}</h2>
<p>{{ price() }} ₽</p>
<button (click)="buy.emit(title())">Купить</button>
</article>
`,
})
export class ProductCardComponent {
title = input.required<string>(); // вход, обязателен
price = input(0); // вход со значением по умолчанию
buy = output<string>(); // выход — событие
}
Обратите внимание: input() возвращает сигнал, поэтому в шаблоне мы читаем его как функцию — title(). А родитель использует компонент так:
template: `
<app-product-card
[title]="'Кофемолка'"
[price]="2990"
(buy)="onBuy($event)" />
`
Квадратные скобки [title] — это привязка свойства (данные вниз), круглые (buy) — привязка события (событие вверх). Переменная $event содержит то, что ребёнок передал в emit().
Как работает под капотом
Когда родитель передаёт значение в [title], Angular при каждом обнаружении изменений сравнивает старое и новое значение и обновляет сигнал входа. output() же — это лёгкий эмиттер: вызов buy.emit(x) синхронно вызывает обработчик родителя.
PARENT
| [title]="..." (вход: данные вниз)
v
CHILD (ProductCard)
| (buy)="onBuy()" (выход: событие вверх)
^
| buy.emit('Кофемолка')
PARENT.onBuy($event)
Запускаемая врезка: поток «вниз/вверх» на чистом JS
Смоделируем контракт родитель–ребёнок без Angular, чтобы прочувствовать механику. Нажмите «Попробуй сам ▶».
// child получает данные (input) и зовёт колбэк (output)
function createChild(onBuy) {
let title = '';
return {
setTitle(value) { title = value; }, // вход: данные вниз
clickBuy() { onBuy(title); }, // выход: событие вверх
};
}
const parentLog = [];
const child = createChild((name) => parentLog.push('Куплено: ' + name));
child.setTitle('Кофемолка'); // родитель кормит ребёнка
child.clickBuy(); // ребёнок уведомляет родителя
child.setTitle('Чайник');
child.clickBuy();
console.log(parentLog);
// ["Куплено: Кофемолка", "Куплено: Чайник"]
Частые ошибки
- Менять входные данные внутри ребёнка. Вход принадлежит родителю; ребёнок только читает.
- Путать скобки.
[x]— свойство (вниз),(x)— событие (вверх). Перепутать — частая причина «ничего не работает». - Забыть вызвать сигнал. В шаблоне нужно писать
title(), а неtitle— иначе выведется сама функция.
Best practices
- Используйте
input.required()для обязательных входов — компилятор заставит передать значение. - Имена выходов — глаголы в прошедшем/настоящем без префикса
on:buy,delete, а неonBuy. - Передавайте через выход минимум данных — обычно id или само событие, а не весь объект.
Итоги. input() — данные вниз, output() — события вверх. В шаблоне родителя: [свойство] и (событие). Это фундамент композиции. Дальше — глубже в шаблоны и привязки.
Закрепляем
Контракт общения компонентов — это половина успеха хорошей архитектуры. Держите данные как можно «выше» по дереву и спускайте их вниз через input(), а решения и события поднимайте наверх через output(). Такой однонаправленный поток делает приложение предсказуемым: вы всегда знаете, кто владеет данными и кто имеет право их менять. Это противоядие от хаоса, когда непонятно, какой компонент в какой момент что испортил.
Различайте, кто чем владеет. Входные данные принадлежат родителю — ребёнок их только читает и не должен мутировать. Если ребёнку нужно «изменить» вход, он на самом деле просит родителя об этом через выход, а родитель уже решает, менять ли состояние. Эта дисциплина кажется лишней церемонией в маленьком примере, но именно она спасает большие приложения от состояния, которое меняется из десяти мест одновременно и которое невозможно отладить.
| Синтаксис | Смысл |
|---|---|
| input() | Вход: данные от родителя (сигнал) |
| input.required() | Обязательный вход |
| output() | Выход: событие к родителю |
| [prop] / (event) | Привязка свойства / события в шаблоне |