Практическая работа к уроку 4 (занятие 2)

Данные и события между компонентами

Свяжите дерево компонентов TaskFlow: данные текут вниз через входы, события всплывают вверх через выходы.

Что вы узнаете

С чего начать: три главные аналогии

Прежде чем писать код, запомните три простых сравнения. Они объясняют весь урок.

Дерево компонентов — это семья или матрёшка

Аналогия. 💡 Представьте семью: есть «глава семьи» (AppShell), у него дети (TaskPage), у тех — свои дети (TaskList, FilterBar), и так дальше. Или матрёшку: большая кукла внутри содержит меньшую, та ещё меньшую. В Angular так же: один компонент внутри себя «прячет» другие компоненты. Это и есть дерево компонентов — главный блок и вложенные в него блоки-дети.

Вход (input) — это почтовый ящик ребёнка

Аналогия. ✍️ Родитель опускает письмо в почтовый ящик ребёнка: «Вот тебе задача — сделай её». Ребёнок (дочерний компонент) только читает письмо из ящика, но не переписывает его заново. В Angular родитель передаёт данные внутрь дочернего компонента через input().

Выход (output) — это ребёнок зовёт родителя

Аналогия. Ребёнок не может сам изменить правила семьи. Вместо этого он кричит: «Пап, я нажал кнопку!» — и родитель сам решает, что делать. В Angular дочерний компонент сообщает о событии наверх через output(), а родитель уже меняет данные.

Золотое правило. Данные текут вниз (письмо в почтовом ящике ребёнка), события всплывают вверх (крик ребёнка). На самом верху живёт «настоящая правда» о состоянии — список задач.

Задание 1. TaskItem: вход и выход

Компонент TaskItem показывает одну задачу. Он получает задачу через вход и сообщает о клике через выход.

Контекст. Это самый маленький блок в дереве TaskFlow — одна строчка списка. Он ничего не решает сам, только показывает текст и «докладывает» родителю, когда пользователь нажал галочку.

Аналогия из жизни. 💡 TaskItem — как ребёнок в семье: родитель опускает задание в почтовый ящик ребёнка (вход input), а ребёнок, выполнив действие, зовёт родителя (выход output), но сам оценки не ставит.

import { Component, input, output } from '@angular/core';

@Component({
  selector: 'app-task-item',
  standalone: true,
  template: `
    <li [class.done]="task().completed">        <!-- если задача выполнена, добавляем класс done (зачёркивание) -->
      <span>{{ task().title }}</span>            <!-- показываем текст задачи из входа -->
      <button (click)="toggle.emit()">✓</button> <!-- при клике «испускаем» событие вверх родителю -->
    </li>
  `
})
export class TaskItem {
  task = input.required<Task>();  // вход: обязательное письмо в «почтовом ящике ребёнка» (данные вниз) — какая задача
  toggle = output<void>();        // выход: сигнал родителю «меня нажали!» (без данных)
}

Пошаговый разбор.

  1. Строка task = input.required<Task>() — компонент говорит: «без задачи я не запущусь, родитель обязан передать её».
  2. В шаблоне {{ task().title }} — берём текст задачи и показываем на экране.
  3. Кнопка с (click)="toggle.emit()" — при нажатии посылает сигнал вверх. Сам компонент НЕ меняет данные.
Проверка. Клик по кнопке порождает событие, но сам компонент НЕ меняет данные — этим занимается родитель. TaskItem только «докладывает».

Что увидит студент. На экране — строчка с текстом задачи и кнопка «✓». Нажатие пока ни к чему не приводит, потому что менять состояние должен родитель (следующее задание).

Задание 2. Поднимаем событие вверх

TaskList ловит событие от TaskItem и передаёт его дальше в TaskPage, который уже меняет состояние.

Контекст. Здесь видно, как сигнал «путешествует» вверх по дереву: от маленького TaskItem к списку TaskList, а от списка — к странице TaskPage, где хранится сам список задач.

Аналогия из жизни. 💡 Это как цепочка: ребёнок (TaskItem) зовёт родителя (TaskList), а тот зовёт своего родителя (TaskPage), у которого есть журнал. Только самый верхний родитель может сделать пометку «выполнено».

// TaskList — список перебирает задачи и для каждой рисует TaskItem
@for (task of tasks(); track task.id) {
  <app-task-item
    [task]="task"                     <!-- вход: опускаем задачу в почтовый ящик ребёнка (данные вниз) -->
    (toggle)="toggle.emit(task.id)" /> <!-- выход: поймали сигнал и переслали его выше -->
}

// TaskPage — тот самый «директор», у которого живёт правда о задачах
<app-task-list
  [tasks]="tasks()"                   <!-- вход: отдаём весь список -->
  (toggle)="onToggle($event)" />      <!-- выход: ловим сигнал от списка -->

onToggle(id: number) {
  // меняем состояние: ищем задачу по id и переворачиваем флаг completed
  this.tasks.update(list =>
    list.map(t => t.id === id
      ? { ...t, completed: !t.completed }  // нашли нужную — переключаем галочку
      : t)                                  // остальные оставляем как есть
  );
}

Пошаговый разбор.

  1. TaskList рисует много TaskItem, передавая каждому свою задачу через [task].
  2. Когда пользователь жмёт «✓» в TaskItem — срабатывает (toggle), и TaskList тут же пересылает сигнал выше через свой собственный toggle.emit(task.id).
  3. TaskPage ловит сигнал в методе onToggle и меняет флаг completed у нужной задачи.
Правило. Данные текут вниз ([ ]), события всплывают вверх (( )). На верхнем уровне живёт «правда» о состоянии.

Что увидит студент. После нажатия «✓» задача либо зачёркивается (выполнена), либо снова становится активной. Всё переключение произошло «наверху», в TaskPage.

Задание 3. FilterBar и выход filterChange

Полоса фильтров сообщает родителю, какое значение выбрал пользователь.

Контекст. FilterBar — это панель кнопок «Все / Открытые / Закрытые». Сама панель не прячет и не показывает задачи. Она лишь говорит родителю: «пользователь выбрал фильтр X», а родитель уже решает, что показать.

Аналогия из жизни. 💡 FilterBar — как ребёнок, зовущий родителя: он сообщает «едем в центр» (передаёт выбор), а решает и везёт пассажиров родитель. FilterBar сам руль не крутит.

@Component({
  selector: 'app-filter-bar',
  template: `
    <button (click)="filterChange.emit('all')">Все</button>      <!-- сказать родителю: покажи всё -->
    <button (click)="filterChange.emit('active')">Открытые</button> <!-- сказать: только незакрытые -->
    <button (click)="filterChange.emit('done')">Закрытые</button>  <!-- сказать: только выполненные -->
  `
})
export class FilterBar {
  // выход: передаём вверх выбранный фильтр ('all' | 'active' | 'done')
  filterChange = output<'all' | 'active' | 'done'>();
}

Пошаговый разбор.

  1. У компонента один выход filterChange, который умеет передавать строку-фильтр.
  2. Каждая кнопка при клике «испускает» своё значение: 'all', 'active' или 'done'.
  3. Родитель (TaskPage) поймает это значение и отфильтрует список. Сам FilterBar ничего не фильтрует.

Что увидит студент. Три кнопки. Нажатие пока ничего не фильтрует — потому что «мозги» (родитель) ещё не подключены к этому сигналу. В следующей практике вы свяжете FilterBar с фильтрацией списка.

Важно про слова. Здесь значение фильтра 'done' — это просто слово «закрытые задачи». Поле самой задачи называется completed (выполнено), а НЕ done. Не путайте: done — только значение фильтра, completed — поле задачи.

Интерактивная песочница

Симуляция «всплытия» события: клик внутри вызывает колбэк родителя.

Контекст. Ниже — упрощённая модель на чистом JavaScript. Она показывает ту же идею, что и Angular: «ребёнок» (функция TaskItem) не меняет список сам, а зовёт «родителя» (функцию onToggle), который и переписывает состояние.

Аналогия из жизни. ▶️ Это как макет дома из картона: не настоящий Angular, но правило «клик → сигнал родителю → смена состояния» видно целиком и сразу.

Песочница: нажми «Запустить» — и увидишь, как клик внутри зовёт родителя, а родитель переключает галочку completed. Меняй код и запускай снова.
Песочница: события вверх, данные вниз

Что увидит студент. Список из трёх задач. Первая — зачёркнута (выполнена). Нажимая «Переключить», вы видите, как галочка появляется и исчезает: клик внутри вызвал родителя, а родитель переписал состояние и перерисовал экран.

Проверьте себя

Вопросы.
  1. Почему дочерний компонент не должен менять входные данные напрямую?
    (Подсказка: вход — это «почтовый ящик» ребёнка, а правда живёт наверху. Если каждый ребёнок начнёт править письмо, родитель потеряет контроль.)
  2. Чем output() отличается от вызова метода родителя?
    (Подсказка: output просто «кричит» событие, а кто и как на него отреагирует — решает родитель. Так компоненты не зависят друг от друга напрямую.)
  3. Где в дереве должно жить «настоящее» состояние списка?
    (Подсказка: на самом верху, в TaskPage/AppShell — как журнал у директора.)

Практика к уроку 4. Далее: Практика к уроку 5 →

Куда дальше: теория данных и событий — в Уроке 4; дерево компонентов на практике — в лабораторной №4.

📖 Глоссарий

ТерминЖизненная аналогияКоротко
Дерево компонентовМатрёшка или семьяГлавный блок прячет внутри себя вложенные блоки-дети, как большая кукла — меньшую.
КомпонентДеталь конструктораМаленькая понятная часть, из которой собирается всё приложение.
input (вход)Почтовый ящик ребёнкаРодитель опускает данные вниз, ребёнок только читает их.
output / событие (выход)Ребёнок зовёт родителяДочерний компонент сообщает наверх о факте, но данные сам не меняет.
Сигнал (signal)Группа в мессенджереПеременная с подписчиками: при изменении все сразу узнают об этом.
Фильтр (filter)Ситечко для чаяПропускает только нужные задачи, остальное отсеивает.
Task / TaskDraftГотовый билет с id и completed / черновик анкеты без номераTaskDraft — только то, что ввёл пользователь; сервис добавляет номер и статус.

🧠 Самопроверка

1. Дочерний компонент сам меняет данные родителя при событии output. Верно или неверно?

2. input — это «почтовый ящик ребёнка», куда родитель опускает данные вниз. Верно или неверно?

3. TaskDraft уже содержит поля id и completed. Верно или неверно?

Прогресс курса