Свяжите дерево компонентов TaskFlow: данные текут вниз через входы, события всплывают вверх через выходы.
input() и input.required()output()Прежде чем писать код, запомните три простых сравнения. Они объясняют весь урок.
Аналогия. 💡 Представьте семью: есть «глава семьи» (AppShell), у него дети (TaskPage), у тех — свои дети (TaskList, FilterBar), и так дальше. Или матрёшку: большая кукла внутри содержит меньшую, та ещё меньшую. В Angular так же: один компонент внутри себя «прячет» другие компоненты. Это и есть дерево компонентов — главный блок и вложенные в него блоки-дети.
Аналогия. ✍️ Родитель опускает письмо в почтовый ящик ребёнка: «Вот тебе задача — сделай её». Ребёнок (дочерний компонент) только читает письмо из ящика, но не переписывает его заново. В Angular родитель передаёт данные внутрь дочернего компонента через input().
Аналогия. Ребёнок не может сам изменить правила семьи. Вместо этого он кричит: «Пап, я нажал кнопку!» — и родитель сам решает, что делать. В Angular дочерний компонент сообщает о событии наверх через output(), а родитель уже меняет данные.
Компонент 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>(); // выход: сигнал родителю «меня нажали!» (без данных)
}
Пошаговый разбор.
task = input.required<Task>() — компонент говорит: «без задачи я не запущусь, родитель обязан передать её».{{ task().title }} — берём текст задачи и показываем на экране.(click)="toggle.emit()" — при нажатии посылает сигнал вверх. Сам компонент НЕ меняет данные.Что увидит студент. На экране — строчка с текстом задачи и кнопка «✓». Нажатие пока ни к чему не приводит, потому что менять состояние должен родитель (следующее задание).
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) // остальные оставляем как есть
);
}
Пошаговый разбор.
[task].(toggle), и TaskList тут же пересылает сигнал выше через свой собственный toggle.emit(task.id).onToggle и меняет флаг completed у нужной задачи.[ ]), события всплывают вверх (( )). На верхнем уровне живёт «правда» о состоянии.Что увидит студент. После нажатия «✓» задача либо зачёркивается (выполнена), либо снова становится активной. Всё переключение произошло «наверху», в TaskPage.
Полоса фильтров сообщает родителю, какое значение выбрал пользователь.
Контекст. 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'>();
}
Пошаговый разбор.
filterChange, который умеет передавать строку-фильтр.'all', 'active' или 'done'.Что увидит студент. Три кнопки. Нажатие пока ничего не фильтрует — потому что «мозги» (родитель) ещё не подключены к этому сигналу. В следующей практике вы свяжете FilterBar с фильтрацией списка.
'done' — это просто слово «закрытые задачи». Поле самой задачи называется completed (выполнено), а НЕ done. Не путайте: done — только значение фильтра, completed — поле задачи.Симуляция «всплытия» события: клик внутри вызывает колбэк родителя.
Контекст. Ниже — упрощённая модель на чистом JavaScript. Она показывает ту же идею, что и Angular: «ребёнок» (функция TaskItem) не меняет список сам, а зовёт «родителя» (функцию onToggle), который и переписывает состояние.
Аналогия из жизни. ▶️ Это как макет дома из картона: не настоящий Angular, но правило «клик → сигнал родителю → смена состояния» видно целиком и сразу.
completed. Меняй код и запускай снова.Что увидит студент. Список из трёх задач. Первая — зачёркнута (выполнена). Нажимая «Переключить», вы видите, как галочка появляется и исчезает: клик внутри вызвал родителя, а родитель переписал состояние и перерисовал экран.
output() отличается от вызова метода родителя? Практика к уроку 4. Далее: Практика к уроку 5 →
| Термин | Жизненная аналогия | Коротко |
|---|---|---|
Дерево компонентов | Матрёшка или семья | Главный блок прячет внутри себя вложенные блоки-дети, как большая кукла — меньшую. |
Компонент | Деталь конструктора | Маленькая понятная часть, из которой собирается всё приложение. |
input (вход) | Почтовый ящик ребёнка | Родитель опускает данные вниз, ребёнок только читает их. |
output / событие (выход) | Ребёнок зовёт родителя | Дочерний компонент сообщает наверх о факте, но данные сам не меняет. |
Сигнал (signal) | Группа в мессенджере | Переменная с подписчиками: при изменении все сразу узнают об этом. |
Фильтр (filter) | Ситечко для чая | Пропускает только нужные задачи, остальное отсеивает. |
Task / TaskDraft | Готовый билет с id и completed / черновик анкеты без номера | TaskDraft — только то, что ввёл пользователь; сервис добавляет номер и статус. |
1. Дочерний компонент сам меняет данные родителя при событии output. Верно или неверно?
Ответ: Неверно. Выход — это когда ребёнок зовёт родителя: он только сообщает факт «я нажал!», а решает и меняет данные родитель. Сам ребёнок руль не крутит.
2. input — это «почтовый ящик ребёнка», куда родитель опускает данные вниз. Верно или неверно?
Ответ: Верно. Родитель кладёт письмо с задачей в почтовый ящик ребёнка, а тот лишь достаёт и читает. Ребёнок письмо не переписывает.
3. TaskDraft уже содержит поля id и completed. Верно или неверно?
Ответ: Неверно. TaskDraft — это черновик анкеты без номера: в нём только то, что ввёл пользователь. Номер и статус completed добавляет сервис, превращая черновик в готовый билет Task.