Связь компонентов: 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)Привязка свойства / события в шаблоне
Проверьте себя
1. Какой синтаксис означает привязку события (данные вверх) в шаблоне Angular?
A[event]
B(event)
C{{ event }}
D*event
2. Почему в шаблоне сигнальный вход читается как title(), а не title?
AЭто опечатка
Binput() возвращает сигнал, а сигнал читают вызовом функции
CТак быстрее
DЧтобы избежать импорта