input), а дочерние компоненты сообщают о событиях наверх через выходы (output). Состояние хранится у того, кому данные принадлежат, — обычно у ближайшего общего родителя.Студенческий материал. Его можно использовать как самостоятельный конспект, раздаточный материал и справочник после занятия.
Angular-приложение — это дерево компонентов. Чтобы оно работало как единое целое, компоненты должны передавать друг другу данные и сообщать о действиях пользователя.
Аналогия — дерево компонентов. Представьте матрёшку или семейное дерево. Внутри большой матрёшки сидит поменьше, внутри неё — ещё меньше. Так же и компоненты: один большой (родитель) внутри себя содержит поменьше (детей), а те — своих детей. Или как в семье: бабушка → мама → ребёнок. «Родитель» — тот, кто внутри себя разместил другого; «ребёнок» — тот, кого разместили. Чтобы передать записку, идут сверху вниз; чтобы позвать на помощь — снизу вверх.
В уроке 3 мы построили TaskItem, TaskList, FilterBar, StatsBar и форму-заглушку. Каждый компонент был самостоятельным и содержал собственные данные. Но настоящему приложению нужна связь:
Без связи компоненты — изолированные острова, которые не могут собраться в целое. Связь делает дерево компонентов рабочим приложением.
В уроке 3 мы уже познакомились с input() и output(). В этом уроке систематизируем знание и доведём его до полного рабочего цикла.
| Конструкция | Урок 3 | Урок 4 |
|---|---|---|
input() | Первый взгляд | Обязательные/необязательные, дефолты, алиасы |
output() | Событие со строкой | Payload разных типов, выбор, дизайн событий |
| «Данные вниз, события вверх» | Идея | Полный цикл: toggle, create, delete, edit |
| Владелец состояния | — | Lifting state up, где хранить данные |
| track | Обязательный атрибут | Зачем и как выбрать ключ |
| Неизменяемость | упоминание | Паттерны обновления массивов и объектов |
Вход (input) — это то, что родитель передаёт дочернему компоненту. Современный способ — функция input(), возвращающая сигнал.
Аналогия — вход (input). Вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input(). А «сигнал» здесь — просто способ хранить значение так, чтобы Angular заметил, когда оно поменялось (данные идут вниз, и при изменении все подписчики сразу узнают об этом).
Где применяем (3. Входные данные: input() и @Input): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
// task-item.ts
import { Component, input } from '@angular/core';
import type { Task } from '../task';
@Component({
selector: 'app-task-item',
standalone: true,
templateUrl: './task-item.html',
})
export class TaskItem {
// вход: родитель передаёт задачу через [task]="task"
readonly task = input.required<Task>();
}
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Родитель передаёт значение через property binding:
Где применяем (3. Входные данные: input() и @Input): Так Angular перебирает задачи и рисует их на экране.
Аналогия из жизни: @for + track — как рассадка учеников по именным бейджам. Angular знает, какой элемент какому соответствует по стабильному ключу (track task.id), и обновляет только нужного «ученика», а не весь класс.
@for (task of tasks(); track task.id) {
<app-task-item [task]="task" />
}
Разбор по шагам: (1) @for перебирает элементы сигнала; (2) track говорит, по какому ключу узнавать задачу; (3) для каждой рисуется дочерний компонент.
На экране: каждая задача появляется как отдельная строка списка.
В шаблоне дочернего вход читается вызовом: task().title. В классе — this.task().
@Input():Где применяем (3. Входные данные: input() и @Input): Так создаётся и описывается отдельная деталь приложения.
Аналогия из жизни: компонент — как деталь конструктора или запчасть машины. Один файл = одна деталь со своим назначением; из деталей собирают большую машину-приложение.
import { Component, Input } from '@angular/core';
@Component({ selector: 'app-task-item', ... })
export class TaskItem {
@Input() task!: Task;
// в шаблоне: task.title (без скобок)
}
Разбор по шагам: (1) декоратор @Component задаёт имя тега и файлы; (2) внутри класса — данные и методы; (3) standalone:true позволяет импортировать компонент напрямую.
На экране: готовая деталь встаёт на своё место в приложении.
Оба подхода работают. Наш курс использует современный input(); знание @Input полезно для чтения legacy-кода.
input() позволяет управлять требованиями ко входу.
Где применяем (Обязательный вход): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
readonly task = input.required<Task>();
// Angular требует: вызвавший обязан передать [task]
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Где применяем (Необязательный вход): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
readonly note = input<string>();
// может не передаваться; note() вернёт undefined
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Где применяем (Со значением по умолчанию): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
readonly maxVisible = input(3);
// если не передано — 3
readonly placeholder = input('Введите задачу');
// по умолчанию строка
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Где применяем (Псевдоним (alias)): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
readonly filter = input<TaskFilter>('all', { alias: 'value' });
// родитель пишет [value], а в классе читается как filter()
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Псевдоним полезен для совместимости или коротких публичных имён, но чаще имя входа и есть публичное имя — алиасы используйте умеренно.
required. Если поле необязательно (заголовок, метка, опция) — обычный input() с дефолтом. Обязательный вход раньше обнаружит ошибку подключения.Где применяем (Псевдоним (alias)): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
// полный набор в TaskListHeader
readonly title = input('Мои задачи');
readonly count = input(0);
readonly showStats = input(true);
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
input() на чистом JS/HTML: родитель передаёт данные вниз, записывая атрибут/свойство дочернему узлу. Angular-версия запускается через ng serve.Выход (output) — это событие, которое дочерний компонент испускает, чтобы уведомить родителя.
Аналогия — выход (output). Выход — это когда ребёнок зовёт родителя: «Мам, посмотри!» или «Я нажал кнопку!». Ребёнок не решает за маму, что делать дальше, — он только сообщает о факте. В Angular ребёнок вызывает this.toggled.emit() (позвал), а родитель, услышав (toggled), сам решает, что с этим делать. Вход — почтовый ящик ребёнка (данные вниз), выход — крик вверх.
Где применяем (5. Выходы: output() и @Output): Так дочерний компонент сообщает родителю о действии пользователя.
Аналогия из жизни: выход — это когда ребёнок зовёт родителя: «Мам, я нажал кнопку!». Он не решает за родителя, что делать дальше, — только сообщает факт. В Angular ребёнок вызывает this.toggled.emit(), а родитель, услышав (toggled), сам решает, как изменить данные.
import { Component, output } from '@angular/core';
@Component({
selector: 'app-task-item',
standalone: true,
templateUrl: './task-item.html',
})
export class TaskItem {
// выход: дочерний сообщает родителю о действии
readonly toggled = output<void>();
toggle() {
// испускаем событие — оно всплывёт к родителю через (toggled)
this.toggled.emit();
}
}
Разбор по шагам: (1) output() объявляет выход; (2) emit() испускает событие; (3) родитель слушает через (имяВыхода)="метод($event)" и реагирует.
На экране: когда пользователь кликает, родитель реагирует (задача зачёркивается, исчезает и т.п.).
Где применяем (5. Выходы: output() и @Output): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
<input type="checkbox" (change)="toggle()" />
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Родитель слушает событие через event binding:
Где применяем (5. Выходы: output() и @Output): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<app-task-item [task]="task" (toggled)="onToggle($event)" />
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Метод emit() испускает событие. Выход можно объявить без типа — тогда он пустой, как здесь.
@Output() с EventEmitter:Где применяем (5. Выходы: output() и @Output): Так дочерний компонент сообщает родителю о действии пользователя.
Аналогия из жизни: выход — это когда ребёнок зовёт родителя: «Мам, я нажал кнопку!». Он не решает за родителя, что делать дальше, — только сообщает факт. В Angular ребёнок вызывает this.toggled.emit(), а родитель, услышав (toggled), сам решает, как изменить данные.
import { Component, Output, EventEmitter } from '@angular/core';
@Component({ selector: 'app-task-item', ... })
export class TaskItem {
@Output() toggled = new EventEmitter<void>();
// this.toggled.emit();
}
Разбор по шагам: (1) output() объявляет выход; (2) emit() испускает событие; (3) родитель слушает через (имяВыхода)="метод($event)" и реагирует.
На экране: когда пользователь кликает, родитель реагирует (задача зачёркивается, исчезает и т.п.).
Наш курс использует output(). Принцип одинаков: «испустить событие».
output() на чистом JS/HTML: дочерний узел испускает пользовательское событие, которое всплывает к родителю. Angular-версия запускается через ng serve.Связь между компонентами строго направлена. Это архитектурное правило, а не деталь синтаксиса.
[task], [filter]). Дочерний не должен самовольно менять эти данные.(toggled), (created)). Обработка и изменение данных — ответственность владельца.input(), а события — вверх через output(). Никогда наоборот: дочерний не меняет чужие данные сам.Это правило делает поток данных предсказуемым: в любой момент вы знаете, где живёт конкретное значение и кто его меняет.
Вопрос «где хранить состояние?» определяет архитектуру. Простое правило: состояние хранится там, где данные нужны нескольким компонентам, — у их общего (ближайшего) родителя. Когда данные поднимают от дочернего к родителю, это называют поднятием состояния (lifting state up).
Пример: счётчик выполненных задач нужен и StatsBar, и FilterBar, и TaskItem (чтобы зачеркнуть). Значит, список tasks живёт в TaskList, а дочерние получают то, что им нужно.
Аналогия — поднятие состояния (lifting state). Представьте общий семейный календарь на холодильнике. Если про день рождения должны знать и мама, и папа, и ребёнок — его вешают в одно общее место, а не пишут каждому на лбу. «Поднять состояние» = перенести общие данные туда, где их видят все, кому нужно, — к их ближайшему общему родителю. Если данные нужны только одному человеку, пусть лежат у него в блокноте (локально).
Где применяем (7. Кто владеет состоянием: lifting state): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
TaskList (владеет tasks)
├── TaskItem ← [task]
├── StatsBar ← [tasks]
└── FilterBar ← [filter] и (filterChange)
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Если данные нужны в нескольких независимых ветках дерева, состояние поднимается к их общему предку. Если веток много и дерево глубокое — состояние выносится в сервис (урок 6) с помощью DI.
Где применяем (7. Кто владеет состоянием: lifting state): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
// локальное состояние — остаётся в компоненте
export class Accordion {
isOpen = false; // не делится — живёт здесь
}
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Соберём первый полный цикл связи: чекбокс в TaskItem управляет состоянием через TaskList.
Где применяем (8. Полный цикл: TaskItem и переключение): Так выглядит компонент, у которого есть и вход (данные сверху), и выходы (крик наверх).
Аналогия из жизни: это как пара «почтовый ящик ребёнка (данные вниз) + зовёт родителя (событие вверх)». Родитель опускает в ящик данные (вход), а ребёнок, когда что-то случилось, зовёт родителя (выход). Ребёнок данные не правит — только сообщает о факте, а родитель решает, что делать.
// task-item.ts
import { Component, input, output } from '@angular/core';
import type { Task } from '../task';
@Component({
selector: 'app-task-item',
standalone: true,
templateUrl: './task-item.html',
})
export class TaskItem {
// вход данных сверху
readonly task = input.required<Task>();
// выход события вверх
readonly toggled = output<void>();
toggle() {
// сообщаем родителю о намерении, сами данные не меняем
this.toggled.emit();
}
}
Разбор по шагам: (1) родитель пишет [task]="..." — данные идут вниз; (2) ребёнок объявляет input.required и output; (3) при действии ребёнок вызывает emit(), и родитель в (toggled) ловит событие и меняет данные.
На экране: видите список задач, у каждой — чекбокс и кнопка удаления; всё работает через передачу данных и событий.
Где применяем (8. Полный цикл: TaskItem и переключение): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
<!-- task-item.html: шаблон только читает вход и испускает событие -->
<div [class.done]="task().completed">
<input
type="checkbox"
[checked]="task().completed"
(change)="toggle()" <!-- клик -> выход toggled -->
[attr.aria-label]="'Отметить: ' + task().title"
/>
<span>{{ task().title }}</span>
</div>
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Где применяем (8. Полный цикл: TaskItem и переключение): Так хранится и меняется общий список задач у владельца.
Аналогия из жизни: сигнал — это переменная с подписчиками, как группа в мессенджере. Меняете значение — и все, кто подписан (шаблон), сразу получают обновление и перерисовываются. Вам не нужно вручную трогать страницу.
// task-list.ts
readonly tasks = signal<Task[]>([...]);
onToggled(id: number) {
// неизменяемое обновление: создаём новый массив
this.tasks.update(current =>
current.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
)
);
}
Разбор по шагам: (1) signal хранит массив; (2) update() строит НОВЫЙ массив; (3) Angular видит новое значение и перерисовывает только зависимые места.
На экране: изменили одну задачу — обновилась только она, остальное на месте.
Где применяем (8. Полный цикл: TaskItem и переключение): Так Angular перебирает задачи и рисует их на экране.
Аналогия из жизни: @for + track — как рассадка учеников по именным бейджам. Angular знает, какой элемент какому соответствует по стабильному ключу (track task.id), и обновляет только нужного «ученика», а не весь класс.
<!-- task-list.html -->
@for (task of tasks(); track task.id) {
<li>
<app-task-item
[task]="task"
(toggled)="onToggled(task.id)"
/>
</li>
}
Разбор по шагам: (1) @for перебирает элементы сигнала; (2) track говорит, по какому ключу узнавать задачу; (3) для каждой рисуется дочерний компонент.
На экране: каждая задача появляется как отдельная строка списка.
Последовательность событий:
toggle() и испускает toggled;(toggled) и вызывает onToggled(task.id);map() создаёт новый массив, где у нужной задачи изменён completed;Создание задачи — второй полный цикл. TaskForm собирает название и передаёт его наверх, TaskList добавляет запись.
Где применяем (9. Полный цикл: TaskForm и создание): Так дочерний компонент сообщает родителю о действии пользователя.
Аналогия из жизни: выход — это когда ребёнок зовёт родителя: «Мам, я нажал кнопку!». Он не решает за родителя, что делать дальше, — только сообщает факт. В Angular ребёнок вызывает this.toggled.emit(), а родитель, услышав (toggled), сам решает, как изменить данные.
// task-form.ts
import { Component, output } from '@angular/core';
@Component({
selector: 'app-task-form',
standalone: true,
templateUrl: './task-form.html',
})
export class TaskForm {
newTitle = '';
// выход несёт строку — название новой задачи
readonly created = output<string>();
onInput(event: Event) {
this.newTitle = (event.target as HTMLInputElement).value;
}
submit() {
if (this.newTitle.trim() === '') {
return; // guard clause: пустую задачу не создаём
}
// испускаем только название; id и объект — забота родителя
this.created.emit(this.newTitle.trim());
this.newTitle = '';
}
}
Разбор по шагам: (1) output() объявляет выход; (2) emit() испускает событие; (3) родитель слушает через (имяВыхода)="метод($event)" и реагирует.
На экране: когда пользователь кликает, родитель реагирует (задача зачёркивается, исчезает и т.п.).
Где применяем (9. Полный цикл: TaskForm и создание): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
<form (submit)="submit()">
<input [value]="newTitle" (input)="onInput($event)"
placeholder="Название задачи" aria-label="Название задачи" />
<button type="submit" [disabled]="newTitle.trim() === ''">Добавить</button>
</form>
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Где применяем (9. Полный цикл: TaskForm и создание): Так хранится и меняется общий список задач у владельца.
Аналогия из жизни: сигнал — это переменная с подписчиками, как группа в мессенджере. Меняете значение — и все, кто подписан (шаблон), сразу получают обновление и перерисовываются. Вам не нужно вручную трогать страницу.
// task-list.ts
onCreated(title: string) {
const id = Math.max(0, ...this.tasks().map(t => t.id)) + 1;
const task: Task = { id, title, completed: false, priority: 'low' };
this.tasks.update(current => [task, ...current]);
}
Разбор по шагам: (1) signal хранит массив; (2) update() строит НОВЫЙ массив; (3) Angular видит новое значение и перерисовывает только зависимые места.
На экране: изменили одну задачу — обновилась только она, остальное на месте.
Где применяем (9. Полный цикл: TaskForm и создание): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<!-- task-list.html -->
<app-task-form (created)="onCreated($event)" />
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Выход output<string>() несёт строку — название новой задачи. Родитель получает её через $event.
Math.max(0, ...id-ов) + 1 даёт следующий уникальный номер. Это временное решение, пока данные не приходят с сервера (урок 7). Тогда id будет присваивать API.Удаление — тоже цикл: TaskItem испускает событие, TaskList убирает элемент из массива через filter().
Где применяем (10. Полный цикл: удаление задачи): Так дочерний компонент сообщает родителю о действии пользователя.
Аналогия из жизни: выход — это когда ребёнок зовёт родителя: «Мам, я нажал кнопку!». Он не решает за родителя, что делать дальше, — только сообщает факт. В Angular ребёнок вызывает this.toggled.emit(), а родитель, услышав (toggled), сам решает, как изменить данные.
// task-item.ts
readonly deleted = output<void>();
deleteTask() {
this.deleted.emit();
}
Разбор по шагам: (1) output() объявляет выход; (2) emit() испускает событие; (3) родитель слушает через (имяВыхода)="метод($event)" и реагирует.
На экране: когда пользователь кликает, родитель реагирует (задача зачёркивается, исчезает и т.п.).
Где применяем (10. Полный цикл: удаление задачи): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<!-- task-item.html -->
<button (click)="deleteTask()" aria-label="Удалить задачу">✕</button>
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Где применяем (10. Полный цикл: удаление задачи): Так хранится и меняется общий список задач у владельца.
Аналогия из жизни: сигнал — это переменная с подписчиками, как группа в мессенджере. Меняете значение — и все, кто подписан (шаблон), сразу получают обновление и перерисовываются. Вам не нужно вручную трогать страницу.
// task-list.ts
onDeleted(id: number) {
this.tasks.update(current => current.filter(t => t.id !== id));
}
Разбор по шагам: (1) signal хранит массив; (2) update() строит НОВЫЙ массив; (3) Angular видит новое значение и перерисовывает только зависимые места.
На экране: изменили одну задачу — обновилась только она, остальное на месте.
Где применяем (10. Полный цикл: удаление задачи): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<!-- task-list.html -->
<app-task-item
[task]="task"
(toggled)="onToggled(task.id)"
(deleted)="onDeleted(task.id)"
/>
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
filter() возвращает новый массив без удалённой задачи. Поскольку создаётся новый массив (а не мутация), сигнал корректно обнаружит изменение.
(deleted) не несёт id — родитель знает, в каком цикле находится, и передаёт task.id сам. Так события остаются простыми, а контекст добавляет родитель.В уроке 2 (раздел 11) мы разобрали деструктуризацию. В методах компонента она удобна для извлечения полей из объекта.
Где применяем (11. Деструктуризация данных в методах): Так ищут, где оборвалась связь между компонентами.
Аналогия из жизни: console.log — как записная книжка следователя: вы оставляете пометки «здесь событие дошло», чтобы понять, на каком шаге оборвалась цепочка. Потом эти пометки убирают.
// без деструктуризации
logTask(task: Task) {
console.log(task.id, task.title, task.completed);
}
// с деструктуризацией
logTask({ id, title, completed }: Task) {
console.log(id, title, completed);
}
Разбор по шагам: (1) ставим метку до испускания события; (2) ставим метку в обработчике родителя; (3) смотрим в консоль, на каком шаге нет записи.
На экране: в консоли браузера (F12) видны ваши пометки-шаги.
Паттерн из Angular: при обработке события разбираем payload на нужные поля:
Где применяем (11. Деструктуризация данных в методах): Так ищут, где оборвалась связь между компонентами.
Аналогия из жизни: console.log — как записная книжка следователя: вы оставляете пометки «здесь событие дошло», чтобы понять, на каком шаге оборвалась цепочка. Потом эти пометки убирают.
onCreated(title: string) {
// title уже извлечён — payload выхода
}
onEdit({ title }: Task) {
// берём только title из задачи
console.log('Редактируем:', title);
}
Разбор по шагам: (1) ставим метку до испускания события; (2) ставим метку в обработчике родителя; (3) смотрим в консоль, на каком шаге нет записи.
На экране: в консоли браузера (F12) видны ваши пометки-шаги.
Деструктуризация объекта в параметрах функции — частый паттерн в обработчиках. Она делает сигнатуру ясной: видно, какие поля реально используются.
task.title.Когда список рендерится через @for, Angular должен понимать, какой элемент какому соответствует. Для этого служит track.
Аналогия — track (ключ элемента). Это как бейдж с именем у ученика в классе. Если Ваня пересел на другую парту, учитель всё равно знает, что это Ваня, — по бейджу, а не по тому, какое он занял место в ряду. Без бейджа (по номеру в списке) учитель перепутает детей, когда кто-то выйдет из класса. В Angular track task.id — это бейдж: стабильный признак, по которому фреймворк узнаёт задачу, даже если она поменяла позицию в списке.
Где применяем (12. Идентификация: id задачи и track): Так Angular перебирает задачи и рисует их на экране.
Аналогия из жизни: @for + track — как рассадка учеников по именным бейджам. Angular знает, какой элемент какому соответствует по стабильному ключу (track task.id), и обновляет только нужного «ученика», а не весь класс.
@for (task of tasks(); track task.id) {
<app-task-item [task]="task" (toggled)="onToggled(task.id)" />
}
Разбор по шагам: (1) @for перебирает элементы сигнала; (2) track говорит, по какому ключу узнавать задачу; (3) для каждой рисуется дочерний компонент.
На экране: каждая задача появляется как отдельная строка списка.
Зачем это важно: если задача №2 изменила completed, Angular может обновить только элемент с task.id === 2, не перерисовывая весь список. Без стабильного ключа Angular потерял бы связь между данными и DOM.
Что можно использовать как ключ:
track task.id — стабильный идентификатор (лучший вариант);track task — сам объект (по ссылке, если id нет);track $index — позиция в массиве (только если порядок фиксирован).$index только для списков с неизменным порядком.Хорошая практика — всегда задавать id в модели Task и использовать его в track. Это же id используется в onToggled(task.id), onDeleted(task.id).
Выход может нести данные разного объёма: без типа, строку, число, объект.
Где применяем (13. События с данными: payload выходов): Так дочерний компонент сообщает родителю о действии пользователя.
Аналогия из жизни: выход — это когда ребёнок зовёт родителя: «Мам, я нажал кнопку!». Он не решает за родителя, что делать дальше, — только сообщает факт. В Angular ребёнок вызывает this.toggled.emit(), а родитель, услышав (toggled), сам решает, как изменить данные.
readonly toggled = output<void>(); // без данных
readonly created = output<string>(); // строка
readonly priorityChanged = output<'high'|'low'>(); // union literal
readonly edited = output<Task>(); // целый объект
Разбор по шагам: (1) output() объявляет выход; (2) emit() испускает событие; (3) родитель слушает через (имяВыхода)="метод($event)" и реагирует.
На экране: когда пользователь кликает, родитель реагирует (задача зачёркивается, исчезает и т.п.).
Когда нужно передать больше одного значения, удобно использовать объект (payload):
Где применяем (13. События с данными: payload выходов): Так дочерний компонент сообщает родителю о действии пользователя.
Аналогия из жизни: выход — это когда ребёнок зовёт родителя: «Мам, я нажал кнопку!». Он не решает за родителя, что делать дальше, — только сообщает факт. В Angular ребёнок вызывает this.toggled.emit(), а родитель, услышав (toggled), сам решает, как изменить данные.
readonly selection = output<{ id: number; completed: boolean }>();
notify() {
this.selection.emit({
id: this.task().id,
completed: !this.task().completed,
});
}
Разбор по шагам: (1) output() объявляет выход; (2) emit() испускает событие; (3) родитель слушает через (имяВыхода)="метод($event)" и реагирует.
На экране: когда пользователь кликает, родитель реагирует (задача зачёркивается, исчезает и т.п.).
Родитель читает поля:
Где применяем (13. События с данными: payload выходов): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<app-task-item (selection)="onSelection($event)" />
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Где применяем (13. События с данными: payload выходов): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
onSelection(p: { id: number; completed: boolean }) {
// p.id, p.completed
}
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
void и подставляйте контекст на стороне родителя (как task.id в цикле). Объект — когда родитель не может сам восстановить данные.Не каждый клик нужно выносить наверх. Разделим: какие события принадлежат дочернему компоненту, а какие — его родителю.
| Ситуация | Кому принадлежит | Почему |
|---|---|---|
| Отметить задачу | Родитель (TaskList) | Меняет общий список tasks |
| Удалить задачу | Родитель | Меняет общий список tasks |
| Создать задачу | Родитель | Добавляет в общий список |
| Выбрать фильтр | Родитель | Фильтр влияет на весь список вниз |
| Открыть внутренний дропдаун | Дочерний (TaskItem) | Локальный UI-статус |
| Подсветить hover | Дочерний | Чисто визуальное состояние |
Правило: выносим наверх события, которые меняют данные, общие для нескольких компонентов, или задают внешнее поведение. События чисто внутреннего UI оставляем внутри.
Когда мы меняем список, мы не мутируем старый массив, а создаём новый. Это называется «неизменяемое обновление». Оно нужно, чтобы Angular надёжно заметил изменение.
Где применяем (15. Неизменяемость данных при обновлении): Так хранится и меняется общий список задач у владельца.
Аналогия из жизни: сигнал — это переменная с подписчиками, как группа в мессенджере. Меняете значение — и все, кто подписан (шаблон), сразу получают обновление и перерисовываются. Вам не нужно вручную трогать страницу.
// создание: новый массив с новой задачей спереди
this.tasks.update(cur => [task, ...cur]);
// удаление: новый массив без задачи
this.tasks.update(cur => cur.filter(t => t.id !== id));
// переключение: новый массив с изменённой копией задачи
this.tasks.update(cur =>
cur.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
);
Разбор по шагам: (1) signal хранит массив; (2) update() строит НОВЫЙ массив; (3) Angular видит новое значение и перерисовывает только зависимые места.
На экране: изменили одну задачу — обновилась только она, остальное на месте.
Где применяем (15. Неизменяемость данных при обновлении): Так хранится и меняется общий список задач у владельца.
Аналогия из жизни: сигнал — это переменная с подписчиками, как группа в мессенджере. Меняете значение — и все, кто подписан (шаблон), сразу получают обновление и перерисовываются. Вам не нужно вручную трогать страницу.
// плохо — мутация исходного массива
const list = this.tasks();
list.push(task); // сигнал не заметит изменение
this.tasks.set(list); // ссылка та же — может не сработать корректно
Разбор по шагам: (1) signal хранит массив; (2) update() строит НОВЫЙ массив; (3) Angular видит новое значение и перерисовывает только зависимые места.
На экране: изменили одну задачу — обновилась только она, остальное на месте.
Лучше всегда создавать новую ссылку через update() с возвратом нового массива. Такой подход согласуется с принципами React и state management из уроков 2 и 21.
Оператор spread { ...t, completed: !t.completed } создаёт копию объекта с изменённым полем, не трогая оригинал (урок 2, раздел 12).
Иногда хочется, чтобы дочерний компонент и показывал значение, и мог его менять, а изменение автоматически возвращалось в родителя. Для полей форм это делается «двусторонней» связью. Принятое название — «banana in a box» из-за скобок [( )].
Где применяем (16. Двусторонняя связь: банан и model()): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
<input [(ngModel)]="title" />
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
[( )] — совмещённая запись property binding + event binding. В современных приложениях проще и нагляднее писать два явных биндинга:
Где применяем (16. Двусторонняя связь: банан и model()): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
<input [value]="title" (input)="onInput($event)" />
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Angular также предлагает сигнальную двустороннюю связь через model():
Где применяем (16. Двусторонняя связь: банан и model()): Так выглядит компонент, у которого есть и вход (данные сверху), и выходы (крик наверх).
Аналогия из жизни: это как пара «почтовый ящик ребёнка (данные вниз) + зовёт родителя (событие вверх)». Родитель опускает в ящик данные (вход), а ребёнок, когда что-то случилось, зовёт родителя (выход). Ребёнок данные не правит — только сообщает о факте, а родитель решает, что делать.
// дочерний компонент с модельным входом-выходом
const draft = model<string>('');
// использует как input: draft()
// и как output: draft.set(...) / draft.update(...)
Разбор по шагам: (1) родитель пишет [task]="..." — данные идут вниз; (2) ребёнок объявляет input.required и output; (3) при действии ребёнок вызывает emit(), и родитель в (toggled) ловит событие и меняет данные.
На экране: видите список задач, у каждой — чекбокс и кнопка удаления; всё работает через передачу данных и событий.
Где применяем (16. Двусторонняя связь: банан и model()): Так делают двустороннюю связь «показал и тут же поменял».
Аналогия из жизни: model() — это как двусторонний телефон: ребёнок и слушает, что говорит родитель, и может сам позвонить обратно, меняя значение. [(title)] — это «банан в коробке»: одна скобочная пара означает сразу «дал данные» + «принял изменение обратно».
// родитель
<app-title-editor [(draft)]="currentTitle" />
Разбор по шагам: (1) model() — одновременно вход и выход; (2) дочерний читает title() и меняет через title.set(); (3) изменение автоматически улетает родителю через событие titleChange.
На экране: ввод в поле сразу меняет значение и у родителя, и у ребёнка синхронно.
Связь компонентов в React устроена так же по принципу, но иначе по синтаксису. Проведём параллель, как в уроке 1 (раздел 24) и уроке 3 (раздел 18).
| Задача | Angular | React |
|---|---|---|
| Данные вниз | [task]="task" + input() | <TaskItem task={task} /> (props) |
| Событие вверх | (toggled)="..." + output() | передача callback-функции onToggled |
| Чтение в дочернем | task().title | task.title (деструктуризация props) |
| Двусторонняя | [(model)], model() | value + onChange |
| Идентификация | track | key |
React-компонент-ребёнок получает props и колбэки: function TaskItem({ task, onToggle }). Внутри он вызывает onToggle(id) — это аналог output().
Где применяем (17. Сравнение: входы/выходы Angular и props/React): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
// React (позже)
function TaskItem({ task, onToggle }) {
return <input type="checkbox" checked={task.completed}
onChange={() => onToggle(task.id)} />;
}
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Соберём полное дерево TaskFlow с учётом всех связей из урока.
Где применяем (18. Композиция TaskFlow: полное дерево): Так выглядит компонент, у которого есть и вход (данные сверху), и выходы (крик наверх).
Аналогия из жизни: это как пара «почтовый ящик ребёнка (данные вниз) + зовёт родителя (событие вверх)». Родитель опускает в ящик данные (вход), а ребёнок, когда что-то случилось, зовёт родителя (выход). Ребёнок данные не правит — только сообщает о факте, а родитель решает, что делать.
App
└── TaskList // владеет tasks и filter
├── TaskForm // output: (created)
├── FilterBar // input: [filter], output: (filterChange)
├── StatsBar // input: [tasks]
└── TaskItem × N // input: [task], outputs: (toggled)(deleted)(edited)
Разбор по шагам: (1) родитель пишет [task]="..." — данные идут вниз; (2) ребёнок объявляет input.required и output; (3) при действии ребёнок вызывает emit(), и родитель в (toggled) ловит событие и меняет данные.
На экране: видите список задач, у каждой — чекбокс и кнопка удаления; всё работает через передачу данных и событий.
Все данные спускаются от TaskList к дочерним, все события поднимаются обратно к TaskList. Так управлять приложением просто: достаточно смотреть на один компонент-владелец.
input(), а события — вверх через output(). Родитель владеет состоянием, дети лишь сообщают о намерениях. Это и есть суть архитектуры урока.ng serve.completed (готово/не готово). Меняй код и запускай снова.Начнём сборку. Создадим связь переключения: чекбокс в TaskItem меняет состояние в TaskList.
Где применяем (19. Практика 1: связываем TaskItem и TaskList): Так создаётся и описывается отдельная деталь приложения.
Аналогия из жизни: компонент — как деталь конструктора или запчасть машины. Один файл = одна деталь со своим назначением; из деталей собирают большую машину-приложение.
ng generate component task-item
ng generate component task-list
Разбор по шагам: (1) декоратор @Component задаёт имя тега и файлы; (2) внутри класса — данные и методы; (3) standalone:true позволяет импортировать компонент напрямую.
На экране: готовая деталь встаёт на своё место в приложении.
Создайте файлы: src/app/task-item/task-item.ts, src/app/task-item/task-item.html, src/app/task-list/task-list.ts, src/app/task-list/task-list.html.
Где применяем (19. Практика 1: связываем TaskItem и TaskList): Так выглядит компонент, у которого есть и вход (данные сверху), и выходы (крик наверх).
Аналогия из жизни: это как пара «почтовый ящик ребёнка (данные вниз) + зовёт родителя (событие вверх)». Родитель опускает в ящик данные (вход), а ребёнок, когда что-то случилось, зовёт родителя (выход). Ребёнок данные не правит — только сообщает о факте, а родитель решает, что делать.
// task-item.ts
import { Component, input, output } from '@angular/core';
import type { Task } from '../task';
@Component({
selector: 'app-task-item',
standalone: true,
templateUrl: './task-item.html',
styleUrls: ['./task-item.css'],
})
export class TaskItem {
readonly task = input.required<Task>(); // данные вниз
readonly toggled = output<void>(); // событие вверх
readonly deleted = output<void>();
toggle() { this.toggled.emit(); } // сообщаем о переключении
delete() { this.deleted.emit(); } // сообщаем об удалении
}
Разбор по шагам: (1) родитель пишет [task]="..." — данные идут вниз; (2) ребёнок объявляет input.required и output; (3) при действии ребёнок вызывает emit(), и родитель в (toggled) ловит событие и меняет данные.
На экране: видите список задач, у каждой — чекбокс и кнопка удаления; всё работает через передачу данных и событий.
Где применяем (19. Практика 1: связываем TaskItem и TaskList): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
<!-- task-item.html -->
<li class="task-item" [class.done]="task().completed">
<input
type="checkbox"
[checked]="task().completed"
(change)="toggle()" <!-- клик -> выход toggled -->
[attr.aria-label]="'Отметить: ' + task().title"
/>
<span>{{ task().title }}</span>
<button class="delete" (click)="delete()" aria-label="Удалить">✕</button> <!-- клик -> выход deleted -->
</li>
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Где применяем (19. Практика 1: связываем TaskItem и TaskList): Так хранится и меняется общий список задач у владельца.
Аналогия из жизни: сигнал — это переменная с подписчиками, как группа в мессенджере. Меняете значение — и все, кто подписан (шаблон), сразу получают обновление и перерисовываются. Вам не нужно вручную трогать страницу.
// task-list.ts
import { Component, signal } from '@angular/core';
import { TaskItem } from '../task-item/task-item';
import type { Task } from '../task';
@Component({
selector: 'app-task-list',
standalone: true,
imports: [TaskItem],
templateUrl: './task-list.html',
styleUrls: ['./task-list.css'],
})
export class TaskList {
readonly tasks = signal<Task[]>([
{ id: 1, title: 'Изучить компоненты', completed: true, priority: 'high' },
{ id: 2, title: 'Связать события', completed: false, priority: 'medium' },
]);
onToggled(id: number) {
// владелец меняет состояние по событию от ребёнка
this.tasks.update(cur =>
cur.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
);
}
onDeleted(id: number) {
// фильтрация создаёт новый массив (без мутации исходного)
this.tasks.update(cur => cur.filter(t => t.id !== id));
}
}
Разбор по шагам: (1) signal хранит массив; (2) update() строит НОВЫЙ массив; (3) Angular видит новое значение и перерисовывает только зависимые места.
На экране: изменили одну задачу — обновилась только она, остальное на месте.
Где применяем (19. Практика 1: связываем TaskItem и TaskList): Так Angular перебирает задачи и рисует их на экране.
Аналогия из жизни: @for + track — как рассадка учеников по именным бейджам. Angular знает, какой элемент какому соответствует по стабильному ключу (track task.id), и обновляет только нужного «ученика», а не весь класс.
<!-- task-list.html -->
<ul>
@for (task of tasks(); track task.id) {
<app-task-item
[task]="task"
(toggled)="onToggled(task.id)"
(deleted)="onDeleted(task.id)"
/>
}
</ul>
Разбор по шагам: (1) @for перебирает элементы сигнала; (2) track говорит, по какому ключу узнавать задачу; (3) для каждой рисуется дочерний компонент.
На экране: каждая задача появляется как отдельная строка списка.
Соберите TaskItem и TaskList. Проверьте: отметить задачу чекбоксом — зачёркивается; удалить кнопкой — исчезает. Обновление состояния происходит в TaskList.
Фильтр живёт в TaskList, а выбор происходит в FilterBar. Свяжем их: вниз — выбранный фильтр, вверх — событие смены фильтра.
Создайте файлы: src/app/filter-bar/filter-bar.ts, src/app/filter-bar/filter-bar.html, src/app/filter-bar/filter-bar.css.
Где применяем (20. Практика 2: фильтрация через выбор в FilterBar): Так выглядит компонент, у которого есть и вход (данные сверху), и выходы (крик наверх).
Аналогия из жизни: это как пара «почтовый ящик ребёнка (данные вниз) + зовёт родителя (событие вверх)». Родитель опускает в ящик данные (вход), а ребёнок, когда что-то случилось, зовёт родителя (выход). Ребёнок данные не правит — только сообщает о факте, а родитель решает, что делать.
// filter-bar.ts
import { Component, input, output } from '@angular/core';
export type TaskFilter = 'all' | 'active' | 'done';
@Component({
selector: 'app-filter-bar',
standalone: true,
templateUrl: './filter-bar.html',
styleUrls: ['./filter-bar.css'],
})
export class FilterBar {
readonly filter = input.required<TaskFilter>(); // текущий фильтр вниз
readonly filterChange = output<TaskFilter>(); // выбор вверх
select(value: TaskFilter) {
// испускаем выбранное значение родителю
this.filterChange.emit(value);
}
}
Разбор по шагам: (1) родитель пишет [task]="..." — данные идут вниз; (2) ребёнок объявляет input.required и output; (3) при действии ребёнок вызывает emit(), и родитель в (toggled) ловит событие и меняет данные.
На экране: видите список задач, у каждой — чекбокс и кнопка удаления; всё работает через передачу данных и событий.
Где применяем (20. Практика 2: фильтрация через выбор в FilterBar): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<div class="filters">
<button
(click)="select('all')" <!-- выбор -> выход filterChange -->
[class.active]="filter() === 'all'"
>Все</button>
<button
(click)="select('active')"
[class.active]="filter() === 'active'"
>Активные</button>
<button
(click)="select('done')"
[class.active]="filter() === 'done'"
>Выполненные</button>
</div>
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Где применяем (20. Практика 2: фильтрация через выбор в FilterBar): Так хранится и меняется общий список задач у владельца.
Аналогия из жизни: сигнал — это переменная с подписчиками, как группа в мессенджере. Меняете значение — и все, кто подписан (шаблон), сразу получают обновление и перерисовываются. Вам не нужно вручную трогать страницу.
// task-list.ts (добавляем фильтр)
readonly filter = signal<TaskFilter>('all');
onFilterChange(value: TaskFilter) {
// родитель хранит выбранный фильтр
this.filter.set(value);
}
visibleTasks() {
const f = this.filter();
if (f === 'active') return this.tasks().filter(t => !t.completed);
if (f === 'done') return this.tasks().filter(t => t.completed); // done — значение фильтра
return this.tasks();
}
Разбор по шагам: (1) signal хранит массив; (2) update() строит НОВЫЙ массив; (3) Angular видит новое значение и перерисовывает только зависимые места.
На экране: изменили одну задачу — обновилась только она, остальное на месте.
Где применяем (20. Практика 2: фильтрация через выбор в FilterBar): Так из общего списка получают только нужные задачи.
Аналогия из жизни: filter() — как ситечко для чая: пропускает только нужное, а остальное оставляет за бортом. В TaskFlow оно отсеивает задачи, которые не подходят под выбранный фильтр, и возвращает новый список.
<!-- task-list.html -->
<app-filter-bar
[filter]="filter()"
(filterChange)="onFilterChange($event)"
/>
<ul>
@for (task of visibleTasks(); track task.id) {
<app-task-item ... />
}
</ul>
Разбор по шагам: (1) filter() перебирает элементы; (2) оставляет только те, где условие true; (3) возвращает новый массив без изменённых оригиналов.
На экране: после выбора фильтра в списке остаются только подходящие задачи.
Как работает: FilterBar сообщает о выборе через (filterChange), TaskList сохраняет фильтр в сигнале, а метод visibleTasks() отдаёт отфильтрованный список (применён filter() из урока 2).
StatsBar получает данные (весь список) от родителя и сам вычисляет производные значения.
Создайте файлы: src/app/stats-bar/stats-bar.ts, src/app/stats-bar/stats-bar.html.
Где применяем (21. Практика 3: статистика из родителя): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
// stats-bar.ts
import { Component, input } from '@angular/core';
import type { Task } from '../task';
@Component({
selector: 'app-stats-bar',
standalone: true,
templateUrl: './stats-bar.html',
})
export class StatsBar {
readonly tasks = input.required<Task[]>();
total() { return this.tasks().length; }
active() { return this.tasks().filter(t => !t.completed).length; }
done() { return this.tasks().filter(t => t.completed).length; }
}
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Где применяем (21. Практика 3: статистика из родителя): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<div class="stats">
<span>Всего: {{ total() }}</span>
<span>Активных: {{ active() }}</span>
<span>Выполнено: {{ done() }}</span>
</div>
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Где применяем (21. Практика 3: статистика из родителя): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<!-- task-list.html -->
<app-stats-bar [tasks]="tasks()" />
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
StatsBar — «чистый» компонент: только данные на вход, никаких внутренних изменений. Он зависит от родителя лишь через tasks. Переиспользуется где угодно, куда передадут массив.
input()) StatsBar ничего не ломает в родителе. Его можно тестировать изолированно: подали массив — проверили вывод.TaskForm передаёт новое название наверх через output(). TaskList добавляет задачу в список.
Создайте файлы: src/app/task-form/task-form.ts, src/app/task-form/task-form.html, src/app/task-form/task-form.css.
Где применяем (22. Практика 4: добавление задачи из формы): Так дочерний компонент сообщает родителю о действии пользователя.
Аналогия из жизни: выход — это когда ребёнок зовёт родителя: «Мам, я нажал кнопку!». Он не решает за родителя, что делать дальше, — только сообщает факт. В Angular ребёнок вызывает this.toggled.emit(), а родитель, услышав (toggled), сам решает, как изменить данные.
// task-form.ts
import { Component, output } from '@angular/core';
@Component({
selector: 'app-task-form',
standalone: true,
templateUrl: './task-form.html',
styleUrls: ['./task-form.css'],
})
export class TaskForm {
newTitle = '';
readonly created = output<string>(); // выход несёт название
onInput(event: Event) {
this.newTitle = (event.target as HTMLInputElement).value;
}
submit() {
const title = this.newTitle.trim();
if (title === '') return; // guard clause
this.created.emit(title); // данные вверх родителю
this.newTitle = '';
}
}
Разбор по шагам: (1) output() объявляет выход; (2) emit() испускает событие; (3) родитель слушает через (имяВыхода)="метод($event)" и реагирует.
На экране: когда пользователь кликает, родитель реагирует (задача зачёркивается, исчезает и т.п.).
Где применяем (22. Практика 4: добавление задачи из формы): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
<form class="task-form" (submit)="submit()">
<input
[value]="newTitle"
(input)="onInput($event)" <!-- данные из поля вверх -->
placeholder="Название новой задачи"
aria-label="Название задачи"
/>
<button type="submit" [disabled]="newTitle.trim() === ''">Добавить</button>
</form>
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Где применяем (22. Практика 4: добавление задачи из формы): Так хранится и меняется общий список задач у владельца.
Аналогия из жизни: сигнал — это переменная с подписчиками, как группа в мессенджере. Меняете значение — и все, кто подписан (шаблон), сразу получают обновление и перерисовываются. Вам не нужно вручную трогать страницу.
// task-list.ts
onCreated(title: string) {
const id = Math.max(0, ...this.tasks().map(t => t.id)) + 1;
const task: Task = { id, title, completed: false, priority: 'low' };
this.tasks.update(cur => [task, ...cur]);
}
Разбор по шагам: (1) signal хранит массив; (2) update() строит НОВЫЙ массив; (3) Angular видит новое значение и перерисовывает только зависимые места.
На экране: изменили одну задачу — обновилась только она, остальное на месте.
Где применяем (22. Практика 4: добавление задачи из формы): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<!-- task-list.html -->
<app-task-form (created)="onCreated($event)" />
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Обратите внимание: TaskForm не знает, как строится объект задачи и какой id получит. Он лишь сообщает название. Создание и id — забота TaskList.
Удаление: TaskItem испускает deleted, TaskList убирает элемент через filter().
Где применяем (23. Практика 5: удаление задачи): Так дочерний компонент сообщает родителю о действии пользователя.
Аналогия из жизни: выход — это когда ребёнок зовёт родителя: «Мам, я нажал кнопку!». Он не решает за родителя, что делать дальше, — только сообщает факт. В Angular ребёнок вызывает this.toggled.emit(), а родитель, услышав (toggled), сам решает, как изменить данные.
// task-item.ts (уже есть)
readonly deleted = output<void>();
delete() { this.deleted.emit(); }
Разбор по шагам: (1) output() объявляет выход; (2) emit() испускает событие; (3) родитель слушает через (имяВыхода)="метод($event)" и реагирует.
На экране: когда пользователь кликает, родитель реагирует (задача зачёркивается, исчезает и т.п.).
Где применяем (23. Практика 5: удаление задачи): Так хранится и меняется общий список задач у владельца.
Аналогия из жизни: сигнал — это переменная с подписчиками, как группа в мессенджере. Меняете значение — и все, кто подписан (шаблон), сразу получают обновление и перерисовываются. Вам не нужно вручную трогать страницу.
// task-list.ts (уже есть)
onDeleted(id: number) {
this.tasks.update(cur => cur.filter(t => t.id !== id));
}
Разбор по шагам: (1) signal хранит массив; (2) update() строит НОВЫЙ массив; (3) Angular видит новое значение и перерисовывает только зависимые места.
На экране: изменили одну задачу — обновилась только она, остальное на месте.
Разберём, почему работает связка (deleted)="onDeleted(task.id)": событие пришло без данных (void), но родитель знает, в каком элементе цикла находится благодаря task.id. Так сочетание «цикл + событие» позволяет родителю восстановить контекст.
Также можно добавить «Отменить» для мягкого удаления — но это уже дополнительные события, принцип тот же.
Редактирование названия — следующий шаг. TaskItem сообщает о необходимости редактирования, TaskList заменяет объект.
Где применяем (24. Практика 6: редактирование (замена объекта)): Так дочерний компонент сообщает родителю о действии пользователя.
Аналогия из жизни: выход — это когда ребёнок зовёт родителя: «Мам, я нажал кнопку!». Он не решает за родителя, что делать дальше, — только сообщает факт. В Angular ребёнок вызывает this.toggled.emit(), а родитель, услышав (toggled), сам решает, как изменить данные.
// task-item.ts
readonly edited = output<string>();
edit() {
this.edited.emit(this.task().title);
}
Разбор по шагам: (1) output() объявляет выход; (2) emit() испускает событие; (3) родитель слушает через (имяВыхода)="метод($event)" и реагирует.
На экране: когда пользователь кликает, родитель реагирует (задача зачёркивается, исчезает и т.п.).
Где применяем (24. Практика 6: редактирование (замена объекта)): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<button (click)="edit()" aria-label="Редактировать">✎</button>
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Где применяем (24. Практика 6: редактирование (замена объекта)): Так хранится и меняется общий список задач у владельца.
Аналогия из жизни: сигнал — это переменная с подписчиками, как группа в мессенджере. Меняете значение — и все, кто подписан (шаблон), сразу получают обновление и перерисовываются. Вам не нужно вручную трогать страницу.
// task-list.ts
onEdited(id: number, newTitle: string) {
this.tasks.update(cur =>
cur.map(t =>
t.id === id
? { ...t, title: newTitle.trim() || t.title }
: t
)
);
}
Разбор по шагам: (1) signal хранит массив; (2) update() строит НОВЫЙ массив; (3) Angular видит новое значение и перерисовывает только зависимые места.
На экране: изменили одну задачу — обновилась только она, остальное на месте.
Где применяем (24. Практика 6: редактирование (замена объекта)): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<!-- task-list.html -->
<app-task-item
[task]="task"
(toggled)="onToggled(task.id)"
(deleted)="onDeleted(task.id)"
(edited)="onEdited(task.id, $event)"
/>
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
{ ...t, title: newTitle } создаёт новый объект с изменённым заголовком, не трогая остальные поля (spread из урока 2). newTitle.trim() || t.title — если строка пустая после обрезки, оставляем старое название (guard-приём).
t.title = newTitle прямо в массиве. Вместо этого копируем объект через spread и возвращаем новый. Это неизменяемое обновление, о котором говорилось в разделе 15.Обработчик события получает либо void (если выход без данных), либо значение, которое испустил дочерний компонент. Это значение называется $event в шаблоне.
Где применяем (24.1. $event и типизация payload событий): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<app-task-item (toggled)="onToggled($event)" />
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Что именно приходит в $event, определяется типом выхода. Разберём три случая.
| Тип выхода | Что в $event | Пример обработчика |
|---|---|---|
output<void>() | undefined | onToggled() { } — без параметра |
output<string>() | строка | onCreated(title: string) |
output<Task>() | объект | onEdited(task: Task) |
Важно различать два уровня $event:
(input) на <input>) даёт объект Event. Значение поля берётся так: (event.target as HTMLInputElement).value.(created) на app-task-form) даёт то, что дочерний положил в emit(): строку, объект или ничего.Где применяем (События DOM и события компонентов): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
// DOM-событие: $event — это Event
onInput(event: Event) {
this.newTitle = (event.target as HTMLInputElement).value;
}
// событие компонента: $event — это title
onCreated(title: string) {
this.addTask(title);
}
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
(input) на элементе, $event — DOM-объект, и в нём нет .target-less строки. Если вы слушаете событие компонента, $event — это payload, объявленный в output(). Смотрите, что именно испускает дочерний компонент.Чтобы не возиться с event.target, можно взять значение из шаблонной ссылки (раздел 28.2 урока 3):
Где применяем (Упрощение через шаблонную ссылку): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
<input #title (input)="onInput(title.value)" />
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Где применяем (Упрощение через шаблонную ссылку): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
onInput(value: string) {
this.newTitle = value;
}
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Это чище, чем читать $event.target: метод принимает уже готовую строку, а не низкоуровневый DOM-объект. Тип параметра становится точным и понятным.
output<TaskFilter>(), обработчик принимает value: TaskFilter. Тогда компилятор заранее поймает несоответствие типов.Когда мы меняем массив задач через tasks.update(...), Angular автоматически перерисовывает только те места шаблона, которые зависят от этого сигнала. Это и есть реактивность.
Аналогия — сигнал (signal). Сигнал — это переменная с «подписчиками», как группа в мессенджере. Вы меняете значение (пишете сообщение), и все, кто подписан, сразу его получают и реагируют. Шаблон «подписан» на tasks(): стоит списку обновиться — и Angular сам перерисовывает только те места, где этот список показан. Вам не нужно вручную искать на странице нужную строчку и менять её текст.
Механика упрощённо выглядит так:
tasks(), filter(). Angular запоминает эту зависимость.Где применяем (24.2. Почему сигналы: реактивность и перерисовка): Так хранится и меняется общий список задач у владельца.
Аналогия из жизни: сигнал — это переменная с подписчиками, как группа в мессенджере. Меняете значение — и все, кто подписан (шаблон), сразу получают обновление и перерисовываются. Вам не нужно вручную трогать страницу.
readonly tasks = signal<Task[]>([]);
updateIsDone(id: number) {
this.tasks.update(cur =>
cur.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
);
}
Разбор по шагам: (1) signal хранит массив; (2) update() строит НОВЫЙ массив; (3) Angular видит новое значение и перерисовывает только зависимые места.
На экране: изменили одну задачу — обновилась только она, остальное на месте.
Заметьте: метод не говорит Angular «перерисуй список». Он просто создаёт новое значение. Обновление DOM происходит автоматически, потому что шаблон зависит от сигнала.
computed() — производные значения, которые пересчитываются автоматически, когда меняется источник. Например, visibleTasks() пока мы считаем методом; в уроке 5 его можно будет сделать вычисляемым сигналом.Иногда входящее значение нужно преобразовать перед использованием. Для этого существует опция transform у input().
Где применяем (24.3. Transform inputs: нормализация входных данных): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
readonly title = input('', { transform: trimTitle });
function trimTitle(value: string) {
return value.trim();
}
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Теперь, что бы ни передал родитель через [title], дочерний всегда получит значение, обрезанное по краям. Это избавляет от повторяющихся .trim() внутри компонента.
Другой полезный приём — опция initialValueTransform, которая обрабатывает только значение по умолчанию:
Где применяем (24.3. Transform inputs: нормализация входных данных): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
readonly priority = input<'high'|'low'>('low', {
initialValueTransform: normalizePriority,
});
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Входы с transform полезны для:
trim, приведение к нижнему регистру);Где применяем (24.3. Transform inputs: нормализация входных данных): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
// вход, который принимает число как строку и превращает в number
readonly limit = input(0, {
transform: (value: string | number) =>
typeof value === 'string' ? Number(value) : value,
});
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
В разделе 16 мы увидели model() в двух строках. Теперь разберём его с полным рабочим примером — поле ввода, которое может читать и обновлять значение, а изменение автоматически возвращается родителю.
model() создаёт одновременно и вход, и выход. Дочерний читает значение как сигнал и может обновлять его через set/update. При обновлении значение «уезжает» наверх к родителю через синтетическое событие modelNameChange.
Где применяем (24.4. model() углублённо: двусторонний вход-выход): Так делают двустороннюю связь «показал и тут же поменял».
Аналогия из жизни: model() — это как двусторонний телефон: ребёнок и слушает, что говорит родитель, и может сам позвонить обратно, меняя значение. [(title)] — это «банан в коробке»: одна скобочная пара означает сразу «дал данные» + «принял изменение обратно».
// title-editor.ts
import { Component, model } from '@angular/core';
@Component({
selector: 'app-title-editor',
standalone: true,
templateUrl: './title-editor.html',
})
export class TitleEditor {
readonly title = model('');
onInput(event: Event) {
const v = (event.target as HTMLInputElement).value;
this.title.set(v); // обновляет сигнал и уведомляет родителя
}
}
Разбор по шагам: (1) model() — одновременно вход и выход; (2) дочерний читает title() и меняет через title.set(); (3) изменение автоматически улетает родителю через событие titleChange.
На экране: ввод в поле сразу меняет значение и у родителя, и у ребёнка синхронно.
Где применяем (24.4. model() углублённо: двусторонний вход-выход): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
<input [value]="title()" (input)="onInput($event)" />
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Где применяем (24.4. model() углублённо: двусторонний вход-выход): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
// title-editor.html
<input [value]="title()" (input)="onInput($event)" />
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
Родитель использует двойной биндинг [(title)]:
Где применяем (24.4. model() углублённо: двусторонний вход-выход): Так делают двустороннюю связь «показал и тут же поменял».
Аналогия из жизни: model() — это как двусторонний телефон: ребёнок и слушает, что говорит родитель, и может сам позвонить обратно, меняя значение. [(title)] — это «банан в коробке»: одна скобочная пара означает сразу «дал данные» + «принял изменение обратно».
<app-title-editor [(title)]="heading" />
Разбор по шагам: (1) model() — одновременно вход и выход; (2) дочерний читает title() и меняет через title.set(); (3) изменение автоматически улетает родителю через событие titleChange.
На экране: ввод в поле сразу меняет значение и у родителя, и у ребёнка синхронно.
Где применяем (24.4. model() углублённо: двусторонний вход-выход): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
// parent.ts
heading = 'Мои задачи';
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Теперь при вводе пользователя heading в родителе обновится автоматически. Это эквивалентно записи:
Где применяем (24.4. model() углублённо: двусторонний вход-выход): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<app-title-editor [title]="heading" (titleChange)="heading = $event" />
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
title событие называется titleChange. Двойной биндинг [(title)] — это просто сахар для «вход title + выход titleChange».input()/output() читаются лучше. Выбор зависит от характера компонента.Два компонента-«брата» (FilterBar и TaskItem) не общаются напрямую: у них нет прямого канала. Вместо этого они общаются через общего родителя — TaskList.
Цепочка выглядит так:
(filterChange) с новым значением;filter;visibleTasks() возвращает отфильтрованный список;[task].Обратите внимание: ни FilterBar, ни TaskItem не знают о существовании друг друга. Весь «мост» — в родителе. Это ключевая идея: «братья» общаются только через общего родителя.
Хорошие события — такие, по названию которых сразу понятно намерение, а по типу — какие данные они приносят. Это часть «архитектурной гигиены».
| Плохо | Хорошо | Почему |
|---|---|---|
change | filterChange | ясно, что и с чем меняется |
tap | taskSelected | имя отражает предмет |
do | toggled | прошедшее время — событие уже случилось |
Принято называть событие в прошедшем времени или как факт: toggled, created, deleted, filterChange. Это сигнал о том, что действие произошло и родитель реагирует.
Правило из раздела 13: минимальный payload. Если родитель может сам восстановить контекст (например, task.id в цикле), несите мало данных.
Где применяем (Payload: сколько данных): Так дочерний компонент сообщает родителю о действии пользователя.
Аналогия из жизни: выход — это когда ребёнок зовёт родителя: «Мам, я нажал кнопку!». Он не решает за родителя, что делать дальше, — только сообщает факт. В Angular ребёнок вызывает this.toggled.emit(), а родитель, услышав (toggled), сам решает, как изменить данные.
// выход без данных — контекст у родителя
readonly toggled = output<void>();
// выход с одной важной величиной
readonly filtered = output<TaskFilter>();
// выход с объектом — когда нужны несколько полей
readonly edited = output<{ id: number; title: string }>();
Разбор по шагам: (1) output() объявляет выход; (2) emit() испускает событие; (3) родитель слушает через (имяВыхода)="метод($event)" и реагирует.
На экране: когда пользователь кликает, родитель реагирует (задача зачёркивается, исчезает и т.п.).
Когда объект-контейнер небольшой, его удобно разложить прямо в сигнатуре метода:
Где применяем (Деструктуризация payload в обработчике): Так хранится и меняется общий список задач у владельца.
Аналогия из жизни: сигнал — это переменная с подписчиками, как группа в мессенджере. Меняете значение — и все, кто подписан (шаблон), сразу получают обновление и перерисовываются. Вам не нужно вручную трогать страницу.
onEdited({ id, title }: { id: number; title: string }) {
this.applyEdit(id, title);
}
applyEdit(id: number, title: string) {
this.tasks.update(cur =>
cur.map(t => t.id === id ? { ...t, title } : t)
);
}
Разбор по шагам: (1) signal хранит массив; (2) update() строит НОВЫЙ массив; (3) Angular видит новое значение и перерисовывает только зависимые места.
На экране: изменили одну задачу — обновилась только она, остальное на месте.
Это перекликается с заданием 6 самостоятельной работы и с деструктуризацией из урока 2 (раздел 11). Приём делает обработчик коротким и точным.
Внутри @for Angular даёт несколько специальных переменных. Они доступны только внутри тела цикла и позволяют добавить контекст в событие.
| Переменная | Значение |
|---|---|
$index | позиция элемента (с 0) |
$count | общее число элементов |
$first | true для первого элемента |
$last | true для последнего элемента |
$even / $odd | чётность позиции |
Где применяем (24.7. Аргументы @for: $index, $count, контекст в событиях): Так Angular перебирает задачи и рисует их на экране.
Аналогия из жизни: @for + track — как рассадка учеников по именным бейджам. Angular знает, какой элемент какому соответствует по стабильному ключу (track task.id), и обновляет только нужного «ученика», а не весь класс.
@for (task of visibleTasks(); track task.id; let i = $index) {
<app-task-item
[task]="task"
[attr.data-index]="i"
(moved)="onMoved($event, i)"
/>
}
Разбор по шагам: (1) @for перебирает элементы сигнала; (2) track говорит, по какому ключу узнавать задачу; (3) для каждой рисуется дочерний компонент.
На экране: каждая задача появляется как отдельная строка списка.
Здесь $event приносит функцию, а i — позицию из цикла. Родитель получает и то, и другое. Так можно передавать в событие дополнительный контекст, который сам дочерний компонент не знает.
Где применяем (24.7. Аргументы @for: $index, $count, контекст в событиях): Так ищут, где оборвалась связь между компонентами.
Аналогия из жизни: console.log — как записная книжка следователя: вы оставляете пометки «здесь событие дошло», чтобы понять, на каком шаге оборвалась цепочка. Потом эти пометки убирают.
onMoved(fn: (tasks: Task[]) => Task[], index: number) {
// используем и результат выхода, и позицию из цикла
console.log('Позиция:', index);
}
Разбор по шагам: (1) ставим метку до испускания события; (2) ставим метку в обработчике родителя; (3) смотрим в консоль, на каком шаге нет записи.
На экране: в консоли браузера (F12) видны ваши пометки-шаги.
$index ненадёжен как track при изменении порядка. Но как дополнительная информация в событии (например, номер для отображения) он допустим. Главное — не использовать его как единственный стабильный идентификатор.Где применяем (24.7. Аргументы @for: $index, $count, контекст в событиях): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
// показать номер перед задачей (только для отображения)
<span class="num">{{ $index + 1 }}</span>
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Синтаксис let i = $index даёт локальную ссылку на позицию. Это удобно, когда внутри @for требуется несколько раз сослаться на номер элемента.
События — это не только клики мышью. Хорошее приложение должно работать с клавиатуры и читаться скринридерами. Свяжем события с доступностью.
Чекбокс — это <input type="checkbox">, а не просто <div> с обработчиком клика. Кнопка — <button>, а не <span>. Так браузер сам даёт клавиатурную навигацию (Enter/Space) и скринридеру — роль элемента.
Где применяем (Семантические элементы): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<!-- правильно: настоящая кнопка -->
<button class="delete" (click)="delete()" aria-label="Удалить задачу">✕</button>
<!-- неправильно: div с кликом -->
<div class="delete" (click)="delete()">✕</div>
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Иконки без текста («✕», «✎») непонятны скринридеру. Добавляйте aria-label:
Где применяем (aria-label для иконок): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<button (click)="delete()" aria-label="Удалить задачу">✕</button>
<button (click)="edit()" aria-label="Редактировать задачу">✎</button>
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Для переключения чекбокса состояние видно и так. Для фильтров активное состояние удобно сообщить скринридеру:
Где применяем (Текущее состояние через aria): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<button
(click)="select('done')"
[class.active]="filter() === 'done'"
[attr.aria-pressed]="filter() === 'done'"
>Выполненные</button>
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
aria-pressed сообщает скринридеру, что кнопка «нажата» — это аналог визуального класса active.
Когда событие не доходит или список не обновляется, отладка сводится к трём вопросам: где данные, кто владелец, в каком направлении движется событие.
signal? Если список не меняется, возможно, обновляется не тот компонент.console.log в метод дочернего и в обработчик родителя.$event: console.log($event) прямо в связке.Где применяем (Алгоритм отладки): Так ищут, где оборвалась связь между компонентами.
Аналогия из жизни: console.log — как записная книжка следователя: вы оставляете пометки «здесь событие дошло», чтобы понять, на каком шаге оборвалась цепочка. Потом эти пометки убирают.
// временная диагностика в связке
<app-task-item
[task]="task"
(toggled)="log('toggled', $event); onToggled(task.id)"
/>
Разбор по шагам: (1) ставим метку до испускания события; (2) ставим метку в обработчике родителя; (3) смотрим в консоль, на каком шаге нет записи.
На экране: в консоли браузера (F12) видны ваши пометки-шаги.
Где применяем (Алгоритм отладки): Так ищут, где оборвалась связь между компонентами.
Аналогия из жизни: console.log — как записная книжка следователя: вы оставляете пометки «здесь событие дошло», чтобы понять, на каком шаге оборвалась цепочка. Потом эти пометки убирают.
log(tag: string, value: unknown) {
console.log(tag, value);
}
Разбор по шагам: (1) ставим метку до испускания события; (2) ставим метку в обработчике родителя; (3) смотрим в консоль, на каком шаге нет записи.
На экране: в консоли браузера (F12) видны ваши пометки-шаги.
В браузере Angular предлагает панель DevTools: можно посмотреть дерево компонентов, какие входы переданы и какие сигналы меняются. Это ускоряет поиск «кто кому что передал».
Если панели нет, визуально пометьте, что происходит: временно добавьте [class.active] или цвет на изменяемый элемент — и посмотрите, отрабатывает ли связка.
Соберём все изученные приёмы в один связный компонент TaskList, который владеет данными и соединяет дочерние компоненты.
Где применяем (24.10. Собираем всё: соединение TaskFlow): Так хранится и меняется общий список задач у владельца.
Аналогия из жизни: сигнал — это переменная с подписчиками, как группа в мессенджере. Меняете значение — и все, кто подписан (шаблон), сразу получают обновление и перерисовываются. Вам не нужно вручную трогать страницу.
// task-list.ts — полный владелец состояния
import { Component, signal } from '@angular/core';
import { TaskItem } from '../task-item/task-item';
import { TaskForm } from '../task-form/task-form';
import { FilterBar, type TaskFilter } from '../filter-bar/filter-bar';
import { StatsBar } from '../stats-bar/stats-bar';
import type { Task } from '../task';
@Component({
selector: 'app-task-list',
standalone: true,
imports: [TaskItem, TaskForm, FilterBar, StatsBar],
templateUrl: './task-list.html',
})
export class TaskList {
readonly tasks = signal<Task[]>([
{ id: 1, title: 'Изучить сигналы', completed: true, priority: 'high' },
{ id: 2, title: 'Соединить компоненты', completed: false, priority: 'medium' },
]);
readonly filter = signal<TaskFilter>('all');
visibleTasks() {
const f = this.filter();
if (f === 'active') return this.tasks().filter(t => !t.completed);
if (f === 'done') return this.tasks().filter(t => t.completed); // done — значение фильтра
return this.tasks();
}
onCreated(title: string) {
const id = Math.max(0, ...this.tasks().map(t => t.id)) + 1;
const task: Task = { id, title, completed: false, priority: 'low' };
this.tasks.update(cur => [task, ...cur]); // новый массив спереди
}
onToggled(id: number) {
// переключаем completed через неизменяемое обновление
this.tasks.update(cur =>
cur.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
);
}
onDeleted(id: number) {
this.tasks.update(cur => cur.filter(t => t.id !== id));
}
onEdited(id: number, title: string) {
this.tasks.update(cur =>
cur.map(t => (t.id === id ? { ...t, title: title.trim() || t.title } : t))
);
}
onFilterChange(value: TaskFilter) {
this.filter.set(value); // храним выбор фильтра у владельца
}
}
Разбор по шагам: (1) signal хранит массив; (2) update() строит НОВЫЙ массив; (3) Angular видит новое значение и перерисовывает только зависимые места.
На экране: изменили одну задачу — обновилась только она, остальное на месте.
Где применяем (24.10. Собираем всё: соединение TaskFlow): Так из общего списка получают только нужные задачи.
Аналогия из жизни: filter() — как ситечко для чая: пропускает только нужное, а остальное оставляет за бортом. В TaskFlow оно отсеивает задачи, которые не подходят под выбранный фильтр, и возвращает новый список.
<!-- task-list.html -->
<app-task-form (created)="onCreated($event)" />
<app-filter-bar
[filter]="filter()"
(filterChange)="onFilterChange($event)"
/>
<app-stats-bar [tasks]="tasks()" />
<ul>
@for (task of visibleTasks(); track task.id) {
<app-task-item
[task]="task"
(toggled)="onToggled(task.id)"
(deleted)="onDeleted(task.id)"
(edited)="onEdited(task.id, $event)"
/>
}
</ul>
Разбор по шагам: (1) filter() перебирает элементы; (2) оставляет только те, где условие true; (3) возвращает новый массив без изменённых оригиналов.
На экране: после выбора фильтра в списке остаются только подходящие задачи.
Это полный владелец состояния: он хранит tasks и filter, обрабатывает все события, формирует видимый список и передаёт данные вниз.
Правило «хранить у общего родителя» не безусловно. Иногда состояние вообще не нужно «таскать» через цепочку компонентов — это усложняет передачу. Обсудим два ограничения.
Если значение используют только внутри одного компонента и оно не влияет на других, поднимать его не нужно. Например, открытая ли панель фильтра, текущая вкладка внутри одного виджета, позиция скролла — это локальный UI-статус.
Где применяем (Состояние, которое никому не нужно, кроме одного компонента): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
// локальное состояние остаётся локальным
export class FilterBar {
isDropdownOpen = false; // никому снаружи не нужно
toggleDropdown() {
this.isDropdownOpen = !this.isDropdownOpen;
}
}
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Если данные нужно передать через пять уровней вложенности только ради одного листа дерева, ручная передача через входы превращается в «prop drilling»: промежуточные компоненты вынуждены принимать и прокидывать значения, которые им самим не нужны.
Где применяем (Глубокая передача через много уровней (prop drilling)): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
App
└── Workspace
└── Panel
└── Section
└── TaskList // хочет user-идентификатор
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Когда цепочка становится глубокой, состояние выносится в сервис с помощью внедрения зависимостей (DI). Тогда TaskList получает данные напрямую, без прокидывания через промежуточные компоненты.
Где применяем (Глубокая передача через много уровней (prop drilling)): Так хранится и меняется общий список задач у владельца.
Аналогия из жизни: сигнал — это переменная с подписчиками, как группа в мессенджере. Меняете значение — и все, кто подписан (шаблон), сразу получают обновление и перерисовываются. Вам не нужно вручную трогать страницу.
// сервис-призрак (детально в уроке 6)
@Injectable({ providedIn: 'root' })
export class TaskStore {
readonly tasks = signal<Task[]>([]);
}
Разбор по шагам: (1) signal хранит массив; (2) update() строит НОВЫЙ массив; (3) Angular видит новое значение и перерисовывает только зависимые места.
На экране: изменили одну задачу — обновилась только она, остальное на месте.
В разделах 3 и 5 мы упомянули старый синтаксис. Теперь соберём полный пример legacy-компонента, чтобы уверенно читать существующие проекты.
Где применяем (24.12. Чтение legacy-кода: @Input/@Output и EventEmitter): Так родитель передаёт задачу ребёнку и как ребёнок её объявляет.
Аналогия из жизни: вход — это почтовый ящик ребёнка. Родитель опускает туда данные (как письмо: «возьми хлеб»), а ребёнок только достаёт и читает их — сам ничего не кладёт и не меняет. В Angular родитель пишет [task]="...", а ребёнок читает это через input() — и получает значение как сигнал, который Angular отслеживает на изменения (данные идут вниз).
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-task-item',
standalone: true,
template: `
<input type="checkbox" [checked]="task.completed"
(change)="toggle.emit()" />
<span>{{ task.title }}</span>
`,
})
export class TaskItem {
@Input() task!: Task;
@Output() toggle = new EventEmitter<void>();
}
Разбор по шагам: (1) input() создаёт вход-сигнал; (2) required значит «родитель обязан передать»; (3) в шаблоне читаем вызовом task(), в коде — this.task().
На экране: ребёнок показывает то, что передал родитель, — например, название задачи.
| Старый код | Новый код | Разница |
|---|---|---|
@Input() task!: Task | readonly task = input.required<Task>() | декоратор vs функция; чтение через task() |
@Output() toggle = new EventEmitter() | readonly toggle = output() | экземпляр vs функция; emit() в обоих |
в шаблоне: task.title | в шаблоне: task().title | скобка вызова сигнала |
Ключевые отличия, которые важно видеть при чтении старого кода:
task в старом коде — обычное поле, читается без скобок.EventEmitter — класс, создающий поток; output() — функция, возвращающая выход.[task], (toggle) одинаковы.input()/output().Финальное упражнение связывает всё: форма вверху, фильтр, статистика и список задач ниже — и все они общаются через один TaskList.
Где применяем (24.13. Практика 7: соединить форму, фильтр и список в один экран): Так из общего списка получают только нужные задачи.
Аналогия из жизни: filter() — как ситечко для чая: пропускает только нужное, а остальное оставляет за бортом. В TaskFlow оно отсеивает задачи, которые не подходят под выбранный фильтр, и возвращает новый список.
<!-- task-list.html — полный экран -->
<section class="task-flow">
<h2>TaskFlow</h2>
<app-task-form (created)="onCreated($event)" />
<app-filter-bar
[filter]="filter()"
(filterChange)="onFilterChange($event)"
/>
<app-stats-bar [tasks]="tasks()" />
<ul class="task-list">
@for (task of visibleTasks(); track task.id) {
<app-task-item
[task]="task"
(toggled)="onToggled(task.id)"
(deleted)="onDeleted(task.id)"
(edited)="onEdited(task.id, $event)"
/>
}
</ul>
</section>
Разбор по шагам: (1) filter() перебирает элементы; (2) оставляет только те, где условие true; (3) возвращает новый массив без изменённых оригиналов.
На экране: после выбора фильтра в списке остаются только подходящие задачи.
Проверьте сценарии в браузере:
| Действие | Ожидаемый результат |
|---|---|
| Ввести название и «Добавить» | новая задача появляется сверху списка |
| Отметить чекбокс | задача зачёркивается, статистика меняется |
| Выбрать «Выполненные» | в списке остаются только выполненные |
| Нажать ✕ | задача исчезает, счётчики обновляются |
| Изменить название | заголовок заменяется новым |
Если хотя бы один сценарий не сработал, примените алгоритм из раздела 24.9: найдите, где рвётся цепочка — событие, состояние или перерисовка.
Сила входов и выходов — в том, что один компонент можно вставить в разные места и настроить через входы, не меняя его код. Проверим это на FilterBar.
Допустим, мы хотим изменить подписи кнопок через входы, чтобы переиспользовать FilterBar и для другого набора фильтров.
Где применяем (24.14. Переиспользование компонента: один компонент, разные места): Так выглядит компонент, у которого есть и вход (данные сверху), и выходы (крик наверх).
Аналогия из жизни: это как пара «почтовый ящик ребёнка (данные вниз) + зовёт родителя (событие вверх)». Родитель опускает в ящик данные (вход), а ребёнок, когда что-то случилось, зовёт родителя (выход). Ребёнок данные не правит — только сообщает о факте, а родитель решает, что делать.
// filter-bar.ts — настраиваемый через входы
readonly filter = input.required<TaskFilter>();
readonly filterChange = output<TaskFilter>();
readonly labels = input<{ all: string; active: string; done: string }>({
all: 'Все',
active: 'Активные',
done: 'Выполненные',
});
Разбор по шагам: (1) родитель пишет [task]="..." — данные идут вниз; (2) ребёнок объявляет input.required и output; (3) при действии ребёнок вызывает emit(), и родитель в (toggled) ловит событие и меняет данные.
На экране: видите список задач, у каждой — чекбокс и кнопка удаления; всё работает через передачу данных и событий.
Где применяем (24.14. Переиспользование компонента: один компонент, разные места): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<button (click)="select('all')" [class.active]="filter() === 'all'">
{{ labels().all }}
</button>
<button (click)="select('active')" [class.active]="filter() === 'active'">
{{ labels().active }}
</button>
<button (click)="select('done')" [class.active]="filter() === 'done'">
{{ labels().done }}
</button>
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Где применяем (24.14. Переиспользование компонента: один компонент, разные места): Фрагмент из рабочего цикла TaskFlow.
Аналогия из жизни: любой фрагмент кода здесь — это одна маленькая операция в общем конвейере TaskFlow. Прочитайте его как шаг в цепочке «данные вниз, события вверх».
<!-- в другом месте — другие подписи без изменения компонента -->
<app-filter-bar
[filter]="filter()"
(filterChange)="onFilterChange($event)"
[labels]="{ all: 'Все записи', active: 'Открытые', done: 'Закрытые' }"
/>
Разбор по шагам: читайте код сверху вниз — каждая строка делает один маленький шаг в общей цепочке передачи данных или события.
На экране: результат этого шага встроен в общий экран TaskFlow.
Идея в том, что интерфейс компонента (input()/output()) — это его «контракт». Пока контракт стабилен, компонент переиспользуется в разных местах, адаптируясь через входы.
active, каким сигналом хранится data) скрыта. Это упрощает тестирование и замену реализации без изменения мест использования.Поскольку компонент зависит только от входов и выходов, его легко проверить: подали вход → ожидаем выход. Никакой внешней среды не требуется.
Где применяем (Тестируемость чистого компонента): Так дочерний компонент сообщает родителю о действии пользователя.
Аналогия из жизни: выход — это когда ребёнок зовёт родителя: «Мам, я нажал кнопку!». Он не решает за родителя, что делать дальше, — только сообщает факт. В Angular ребёнок вызывает this.toggled.emit(), а родитель, услышав (toggled), сам решает, как изменить данные.
// идея теста (подробнее в уроке 12)
const filterBar = new FilterBar();
// имитируем, что родитель передал
// [filter]="'all'" → нет прямого доступа, но через host
// взаимодействие: клик по кнопке → filterChange.emit('done')
Разбор по шагам: (1) output() объявляет выход; (2) emit() испускает событие; (3) родитель слушает через (имяВыхода)="метод($event)" и реагирует.
На экране: когда пользователь кликает, родитель реагирует (задача зачёркивается, исчезает и т.п.).
Слабо связанные компоненты («данные на вход, событие на выход») — основа тестируемости и переиспользования. Эта же идея лежит в основе «чистых компонентов», которые мы рассмотрим в React-части.
Шесть заданий на отработку связи компонентов. Сначала попробуйте сами, затем сверьтесь с подсказками в разделе 26.
В FilterBar кнопка выбранного фильтра должна подсвечиваться классом active. Используйте вход [filter].
После created.emit(title) очистите поле newTitle и убедитесь, что инпут пустеет.
Добавьте в FilterBar числа у каждой кнопки: «Все (3)» и т.п. Передайте их через входы с дефолтами.
Добавьте кнопку «Отменить» в TaskList после создания задачи; при клике последняя добавленная задача удаляется.
В task-list добавьте метод sortedTasks(), который возвращает высокие приоритеты первыми (примените sort на копии).
Перепишите onEdited так, чтобы извлечь id и title из объекта через параметр-деструктуризацию в отдельном методе.
Совет: сначала сравните свою идею, затем открывайте код.
[class.active]="filter() === 'all'" (соответственно 'active', 'done').submit() после created.emit(...) добавьте this.newTitle = '';. Поле обновляется, так как [value]="newTitle".allCount = input(0), activeCount = input(0), doneCount = input(0), вывести {{ allCount() }}.undoLast(), фильтрующий его. Кнопку — в TaskList над списком.sortedTasks() { return [...this.tasks()].sort((a, b) => (a.priority === 'high' ? -1 : 0) - (b.priority === 'high' ? -1 : 0)); } — копия через spread перед sort, чтобы не мутировать сигнал.applyEdit({ id, title }: Task) внутри onEdited после получения данных, с деструктуризацией параметра.Разберём типичные проблемы при соединении компонентов.
| Симптом | Причина | Решение |
|---|---|---|
| «Can't bind to 'task'» | Дочерний не объявил вход или не импортирован. | Добавьте input() и импорт в imports. |
| «Property 'toggled' does not exist» | Родитель слушает выход, которого нет. | Объявите output() в дочернем. |
| Список не обновляется | Мутация массива без новой ссылки. | Используйте update() с новым массивом. |
| Неверный элемент изменяется | Неправильный id или track по $index при изменении порядка. | Используйте track task.id и передавайте корректный id. |
| Событие не доходит | Опечатка в имени выхода или binding на другого ребёнка. | Сверьте имена выхода и ( )-биндинга. |
| Вход «undefined» | Передаёте [task], но значение не определено. | Проверьте источник данных; добавьте @if вокруг использования. |
Все связи этого урока сводятся к небольшому числу правил.
input() и читаются вызовом value().output() и обрабатываются родителем.id и track.Последний взгляд на весь урок. Связь компонентов — это не синтаксис, а способ мышления: вы заранее решаете, кто владеет данными, кто сообщает о событиях и как значения перетекают по дереву. Когда такое мышление становится автоматическим, вы можете собрать из деталей целое приложение TaskFlow и уверенно перейти к React, где идея «данные вниз, события вверх» повторяется почти один в один.
input.required<Task>()?output() отличается от входа input() по направлению?track, и почему $index опасен при фильтрации?$event в обработчике события?$event от DOM-события и от события компонента?model() уместно, а когда лучше явные входы и выходы?| Термин | Определение |
|---|---|
| Вход (input) | Данные, передаваемые родителем дочернему компоненту. |
| Выход (output) | Событие, испускаемое дочерним компонентом для родителя. |
| Property binding [ ] | Привязка значения из класса к свойству (данные вниз). |
| Event binding ( ) | Привязка события к методу (событие вверх). |
| «Данные вниз, события вверх» | Архитектурный принцип направления связи компонентов. |
| Lifting state up | Поднятие состояния к общему родителю. |
| track | Стабильный ключ элемента в @for. |
| Неизменяемое обновление | Создание нового массива/объекта вместо мутации. |
| Payload | Данные, которые несёт событие выхода. |
| model() | Сигнальный вход-выход для двусторонней связи. |
| Владелец состояния | Компонент, в котором хранится и изменяется состояние. |
| transform input | Функция, преобразующая входное значение перед использованием. |
| $event | Значение, которое несёт событие (DOM-событие или payload выхода). |
| «Братья» (siblings) | Компоненты, имеющие общего родителя; общаются через него. |
| Contract (контракт) | Совокупность входов и выходов, видимая внешним компонентам. |
| aria-label / aria-pressed | Атрибуты доступности для описания и состояния элементов. |
| Prop drilling | Ситуация глубокой передачи данных через много уровней вложенности. |
Для самостоятельного углубления:
computed() и effect() — полноценную реактивность Angular. Это сделает связь компонентов ещё более отзывчивой: производные значения будут пересчитываться автоматически при изменении источников.input() и читаю их вызовом в шаблоне.output() и испускаю события с payload.track со стабильным ключом id.Свяжите дерево компонентов: данные вниз через входы, события вверх через выходы.
Где применяем (Задание 1. TaskItem: вход и выход): Так выглядит компонент, у которого есть и вход (данные сверху), и выходы (крик наверх).
Аналогия из жизни: это как пара «почтовый ящик ребёнка (данные вниз) + зовёт родителя (событие вверх)». Родитель опускает в ящик данные (вход), а ребёнок, когда что-то случилось, зовёт родителя (выход). Ребёнок данные не правит — только сообщает о факте, а родитель решает, что делать.
export class TaskItem {
task = input.required<Task>();
toggle = output<void>();
}
Разбор по шагам: (1) родитель пишет [task]="..." — данные идут вниз; (2) ребёнок объявляет input.required и output; (3) при действии ребёнок вызывает emit(), и родитель в (toggled) ловит событие и меняет данные.
На экране: видите список задач, у каждой — чекбокс и кнопка удаления; всё работает через передачу данных и событий.
Где применяем (Задание 2. Поднимаем событие вверх): Так хранится и меняется общий список задач у владельца.
Аналогия из жизни: сигнал — это переменная с подписчиками, как группа в мессенджере. Меняете значение — и все, кто подписан (шаблон), сразу получают обновление и перерисовываются. Вам не нужно вручную трогать страницу.
// TaskPage ловит событие и меняет состояние
onToggle(id: number) {
this.tasks.update(list =>
list.map(t => t.id === id
? { ...t, completed: !t.completed } : t));
}
Разбор по шагам: (1) signal хранит массив; (2) update() строит НОВЫЙ массив; (3) Angular видит новое значение и перерисовывает только зависимые места.
На экране: изменили одну задачу — обновилась только она, остальное на месте.
| Термин | Жизненная аналогия | Коротко |
|---|---|---|
| Дерево компонентов | Матрёшка / семейное дерево | Большая матрёшка внутри содержит поменьше; «родитель» разместил «ребёнка». |
| input (вход) | Почтовый ящик ребёнка | Родитель опускает данные вниз; ребёнок только достаёт и читает — сам не меняет. |
| output (выход) | Ребёнок зовёт родителя | Дочерний кричит вверх «я нажал!» и не решает за родителя, что делать. |
| Событие | Факт, а не приказ | Сообщение о том, что случилось; обрабатывает его тот, кто владеет данными. |
| Сигнал | Группа в мессенджере | Переменная с подписчиками: изменил значение — все подписанные места перерисовались. |
| Task / TaskDraft | Готовый билет / черновик анкеты | Task — билет с id и completed; TaskDraft — черновик без номера, ещё не принятый. |
1. Дочерний компонент сам меняет данные родителя напрямую через input. Верно или неверно?
Ответ: Неверно. input — это почтовый ящик ребёнка: данные идут только вниз, а ребёнок их лишь читает. Меняет данные тот, кто ими владеет (родитель), услышав событие вверх.
2. input() отвечает за «данные вниз», а output() — за «события вверх». Верно или неверно?
Ответ: Верно. Родитель пишет [task]="..." (почтовый ящик, данные вниз), а ребёнок зовёт (toggled) (крик вверх). Так работает однонаправленный поток.
3. Сигнал похож на группу в мессенджере: при изменении значения все подписанные места сразу узнают обновление. Верно или неверно?
Ответ: Верно. Сигнал — это переменная с подписчиками, как группа в мессенджере: поменяли значение, и шаблон (все «участники») перерисовывается сам, без ручного вмешательства.