Курс: Создание веб-приложений с использованием Angular и React

Урок №3

Angular: компоненты и шаблоны

Теория — 90 минут
Практика — 90 минут

Сквозной проект курса: TaskFlow

Компонент и шаблон: связь данных и разметкиКласс (TypeScript)tasks: Task[]completed: booleantoggle()filter: 'all'|'active'|'done'состояние и логика[ ] вниз( ) вверхTemplate (HTML){{ task.title }}<button (click)="toggle()">@for / @if / @switch[class.done]="completed"представление состоянияDOMAngular применяетизменения к реальнымэлементам страницысам, без ручного поиска
Рисунок 1. Класс держит состояние, шаблон описывает представление, Angular синхронизирует их с DOM.
Главная идея урока. Вместо того чтобы вручную искать document.querySelector и менять элементы (как в уроке 1, раздел 41), мы описываем, что должно быть при текущем состоянии, а Angular сам применяет изменения к DOM. Шаблон — это декларативное описание интерфейса.

Студенческий материал. Его можно использовать как самостоятельный конспект, раздаточный материал и справочник после занятия.

Содержание

  1. 1. Компонент — строительный блок Angular
  2. 2. Структура standalone-компонента
  3. 3. Декоратор @Component: selector, template, styles
  4. 4. Зелёный проект: подготовка TaskFlow
  5. 5. Интерполяция {{ }}
  6. 6. Выражения в шаблоне: что можно и что нельзя
  7. 7. Property binding [ ]
  8. 8. Attribute binding и class/style binding
  9. 9. Event binding ( )
  10. 10. События: $event и передача данных
  11. 11. Направления связи: две стрелки
  12. 12. Control flow: @if, @else, @else if
  13. 13. Control flow: @for, track, $index, @empty
  14. 14. Control flow: @switch и @case
  15. 15. Вложенный control flow: фильтрация в шаблоне
  16. 16. Модель Task и её связь с шаблоном
  17. 17. Рендер списка TaskFlow
  18. 18. Сравнение с будущим React: map() и key
  19. 19. Компонентная композиция: дерево TaskFlow
  20. 20. Передача данных вниз: input()
  21. 21. Сигналы: читать состояние в шаблоне
  22. 22. Стили компонента: encapsulation
  23. 23. Обработка ошибок в шаблоне: null, undefined, optional chaining
  24. 24. Практика 1: компонент TaskItem
  25. 25. Практика 2: компонент TaskList
  26. 26. Практика 3: FilterBar со статусами
  27. 27. Практика 4: StatsBar со счётчиками
  28. 28. Практика 5: форма-заглушка TaskForm
  29. 28.1. Атрибут vs свойство DOM: когда [attr.*]
  30. 28.2. Шаблонные ссылки #ref и их назначение
  31. 28.3. Связывание TaskForm с TaskList
  32. 28.4. Практика 6: интерактивное добавление задачи
  33. 28.5. Работа с вложенными данными в шаблоне
  34. 28.6. Классы и стили: расширенные приёмы
  35. 28.7. Доступность (a11y) в шаблонах
  36. 28.8. Практика 7: завершаем мастер-шаблон
  37. 28.9. Производительность шаблонов
  38. 28.10. Таблица-шпаргалка биндингов
  39. 28.11. Практика 8: отметка задачи выполненной
  40. 29. Самостоятельная работа
  41. 30. Подсказки к самостоятельной работе
  42. 31. Частые ошибки и как их читать
  43. 32. Резюме: что мы построили
  44. 33. Контрольные вопросы
  45. 34. Мини-словарь
  46. 35. Источники и продолжение
  47. 36. Чек-лист урока
Урок 3 • Раздел 1

1. Компонент — деталь конструктора Angular

Angular строит приложение из компонентов. Компонент — это связка: данные + логика + разметка + стили.

В уроке 1 (разделы 20–21) мы впервые увидели Angular-компонент, создали первый список задач через CLI и подключили его к корню. В уроке 2 мы разобрали JavaScript и TypeScript, на которых строится класс компонента. Теперь соединим всё вместе и посмотрим на компонент детально.

Компонент объединяет три части:

Почему шаблоны, а не ручная работа с DOM? В уроке 1 мы сравнивали императивный подход (найти элемент, изменить его) и декларативный (описать, что должно быть). Angular использует декларативный: вы пишете, как выглядит интерфейс при текущем состоянии, а фреймворк применяет изменения к DOM сам.

Экран TaskFlow собирается из нескольких компонентов:


App (корень)
└── TaskList
    ├── TaskItem (повторяется для каждой задачи)
    ├── FilterBar
    ├── StatsBar
    └── TaskForm

В этом уроке мы построим эти компоненты и свяжем их данными через шаблон. Именно шаблон — наша главная тема.

Урок 3 • Раздел 2

2. Структура standalone-компонента

Современный Angular (начиная с 17 версии и в нашем проекте 22) использует standalone API. Компонент не обязан жить внутри NgModule — он сам объявляет, что ему нужно для работы, через поле imports.

💡 Аналогия из жизни. Компонент — как деталь конструктора LEGO: он сам по себе закончен и делает одну вещь. А standalone — это деталь, которую не нужно вклеивать в общую инструкцию набора: она «знает» сама, какие соседние детали ей нужны (через imports), и работает сама по себе.

Контекст. Так выглядит минимальный каркас любого современного компонента TaskFlow.

Что увидит пользователь. Пока это только код — на экране ничего не появится, пока мы не используем тег компонента в шаблоне и не запустим ng serve.


import { Component } from '@angular/core';

@Component({
  selector: 'app-task-item',
  standalone: true,
  imports: [],
  templateUrl: './task-item.html',
  styles: [`...`]
})
export class TaskItem {
  title = 'Изучить Angular';
}
ПолеЧто делает
selectorИмя тега, по которому компонент используется в разметке.
standaloneГоворит Angular: компонент самостоятелен, без NgModule.
importsСписок зависимостей шаблона: другие компоненты, директивы, pipes.
template / templateUrlРазметка прямо в коде или в отдельном файле.
styles / styleUrlsСтили прямо в коде или в отдельных файлах.
Примечание. CLI создаёт файлы автоматически и заполняет декоратор. Самому писать всю структуру вручную надо редко, но понимать её необходимо — при чтении чужого кода и при тонкой настройке.
Урок 3 • Раздел 3

3. Декоратор @Component: selector, template, styles

Декоратор @Component сообщает Angular, как обрабатывать класс: считать его компонентом, какой у него шаблон и как его стилизовать.

Selector

Selector определяет способ встраивания. Чаще всего это имя тега с префиксом app:

💡 Аналогия из жизни. Selector — как имя розетки: вы решаете, в какое «гнездо» вставить компонент. Назвали его app-task-item — значит в разметке пишете <app-task-item />, и там «включится» этот компонент.

selector: 'app-task-item'
// использование в разметке:
<app-task-item />

Возможны и другие формы selector'а:


// по атрибуту
selector: '[appTaskItem]'   // <div appTaskItem></div>

// по классу
selector: '.app-task-item'  // <div class="app-task-item"></div>
Практическое правило. Для компонентов используйте селектор-тег app-.... Селекторы-атрибуты и классы чаще нужны для директив, о которых речь пойдёт в других уроках.

Template и Styles

Небольшую разметку удобно держать строкой (шаблонная строка в обратных кавычках), большую — в отдельном файле:

💡 Аналогия из жизни. Template и styles — как начинка и обёртка конфеты: маленькую конфету удобно сделать целиком в одной руке (всё в одном файле), а для большой батончицы начинку и обёртку лучше хранить отдельно, чтобы не запутаться.

// маленький компонент — всё рядом
@Component({
  selector: 'app-greeting',
  standalone: true,
  template: `<p>Привет, {{ name }}</p>`,
})
export class Greeting {
  name = 'Anna';
}

// большой компонент — файлы раздельно
@Component({
  selector: 'app-task-list',
  standalone: true,
  templateUrl: './task-list.html',
  styleUrls: ['./task-list.css'],
})
export class TaskList { }
Урок 3 • Раздел 4

4. Зелёный проект: подготовка TaskFlow

Как читать код в этом уроке. Чтобы не запутаться в блоках, используйте значки: 📖 Пример — только для чтения, изучите и поймите идею; ✍️ Действие — это нужно создать или вписать в свой проект (рядом указан файл); ▶️ Песочница — нажмите «Запустить», чтобы сразу увидеть результат без Angular. Сам Angular-код запускается командой ng serve --open (ниже). Песочницы дают быстрый аналог на чистом JS/HTML, чтобы проверить идею за секунды, не поднимая проект.

Перед кодом убедимся, что проект на месте и сервер разработки запускается. Проект мы создали в уроке 1 (раздел 36).

📖 Пример. Команды выполняются в терминале внутри папки проекта:

# Запуск сервера разработки (откроет http://localhost:4200)
cd task-flow-angular
ng serve --open

Если проект уже создан, повторно его создавать не нужно. Проверка — команда в отдельном терминале:


ng version
Если что-то не так. 1) ng: command not found — Angular CLI не установлен или не добавлен в PATH; 2) порт 4200 занят — добавьте --port 4300; 3) ошибка в коде — читайте stack trace, внизу обычно указан конкретный файл и строка.

Начнём с корневого компонента App. Это действие: создайте два файла в папке src/app/.

✍️ Действие. Файл src/app/app.ts — класс компонента:

// app.ts — корневой компонент приложения
import { Component } from '@angular/core';
import { TaskList } from './task-list/task-list';

@Component({
  selector: 'app-root',        // каким тегом использовать (<app-root>)
  standalone: true,            // современный автономный компонент
  imports: [TaskList],          // подключаем дочерний компонент
  templateUrl: './app.html',    // внешний шаблон (разметка)
})
export class App {
  title = 'TaskFlow';           // поле, которое увидим в шаблоне
}
✍️ Действие. Файл src/app/app.html — шаблон:

<!-- app.html — разметка корневого компонента -->
<h1>{{ title }}</h1>        <!-- интерполяция: выведет "TaskFlow" -->
<app-task-list />          <!-- место для списка задач -->

Компонент TaskList мы построим в разделе 25. Пока он не создан, на время разработки можно закомментировать строку импорта или создавать компонент заранее.

Урок 3 • Раздел 5

5. Интерполяция {{ }}

Интерполяция {{ }} подставляет значение выражения из класса в текст разметки. Это первый и самый частый способ показать данные.

💡 Аналогия из жизни. Интерполяция — как вписывание имени в бланк: в бланке написано «Уважаемый {{ имя }}», и вместо скобок подставляется реальное имя. В Angular вы пишете {{ title }}, а фреймворк вписывает туда текущее значение переменной title.

Контекст. В TaskFlow так выводятся заголовки задач, счётчики и статусы. Ниже — класс с данными и шаблон, который их показывает.


// task-item.ts
export class TaskItem {
  title = 'Изучить Angular';
  count = 3;
  completed = false;
}

<h3>{{ title }}</h3>
<p>Вложено шагов: {{ count }}</p>
<p>Выполнено: {{ completed }}</p>

Внутри фигурных скобок — обычное выражение TypeScript/JavaScript. Его результат конвертируется в строку и вставляется в текст.


<p>Через год: {{ count + 1 }}</p>
<p>Заглавными: {{ title.toUpperCase() }}</p>
<p>Статус: {{ completed ? 'готово' : 'в работе' }}</p>
<p>Первая буква: {{ title[0] }}</p>
Почему удобно. Когда значение в классе меняется, Angular автоматически перерисовывает затронутую часть. Не нужно вручную обновлять текст элемента — декларативный подход из урока 1.

Интерполяция используется повсеместно: заголовки, подписи, сообщения, счётчики. Например, вывести счётчик и время обновления:


<p>Всего задач: {{ tasks.length }}</p>
<p>Осталось: {{ tasks.filter(t => !t.completed).length }}</p>
<p>Обновлено: {{ updatedAt.toLocaleTimeString() }}</p>

Здесь внутри {{ }} вызван метод filter() прямо в выражении. Это допустимо, но для сложных расчётов лучше выносить логику в метод или computed-значение — шаблон останется читаемым, а производительность выше (раздел 28.9).

▶️ Быстрый запуск. Angular-код ниже запускается через ng serve, но суть интерполяции та же, что у подстановки переменных. Нажмите «Запустить», чтобы увидеть результат на чистом JS/HTML:
Песочница: нажми «Запустить» — и увидишь, как значения из кода попадают прямо в текст страницы (интерполяция). Меняй код и запускай снова.
Песочница: интерполяция (аналог на JS)
Урок 3 • Раздел 6

6. Выражения в шаблоне: что можно и что нельзя

Шаблонное выражение — это выражение (вспомните разницу «выражение vs инструкция» из урока 2, раздел 2). Поэтому в нём есть ограничения.

МожноНельзя
Арифметика: {{ a + b }}Объявление переменных через let
Тернарный оператор: {{ ok ? 1 : 0 }}Условные блоки if без фигурных скобок
Вызов методов: {{ getTitle() }}Циклы for
Доступ к свойствам: {{ task.title }}Операторы присваивания
Оператор необязательности: task?.titlenew, global, случайные глобалы
Строки и шаблонные строкиОбъявление функций
Ограничение важно для безопасности. Angular запрещает в шаблонных выражениях глобальные переменные и произвольные инструкции. Это делает шаблон предсказуемым и защищает от случайного выполнения вредоносного кода.

Поэтому сложную логику выносите в методы класса:


// в классе
getStatus() {
  return this.completed ? 'готово' : 'в работе';
}

<!-- в шаблоне -->
<p>{{ getStatus() }}</p>
💡 Попробуйте сами. Напишите в шаблоне {{ 1 + 1 }} — увидите 2. А теперь попробуйте {{ let x = 1 }} или {{ if (ok) 1 }} — Angular выдаст ошибку, потому что в выражении нельзя объявлять переменные и писать инструкции. Так на практике почувствуете границу «можно / нельзя».
Урок 3 • Раздел 7

7. Property binding [ ]

Property binding [свойство]="выражение" передаёт значение из класса в свойство DOM-элемента или директивы. Говорят «данные вниз»: от TypeScript к представлению.

💡 Что такое DOM и зачем шаблоны. DOM (Document Object Model) — это дерево элементов страницы, которое браузер строит из HTML. Когда вы пишете document.getElementById('x').textContent = ..., вы вручную меняете узел этого дерева. Это работает, но опасно: забыли обновить — на экране неверное значение; добавили узел, но не удалили старый — остаётся мусор. Angular берёт эту рутину на себя: вы описываете в шаблоне ЖЕЛАЕМЫЙ вид через {{ }}, [ ] и ( ), а фреймворк сам синхронизирует DOM с данными. Вы думаете о данных, а не о том, как их «засунуть» в страницу.

<input [value]="title" />
<button [disabled]="completed">Начать</button>
<img [src]="imageUrl" [alt]="title" />

Когда значение в классе изменяется, Angular обновляет свойство элемента. Сравните: value="title" (без скобок) — это статический атрибут. [value]="title" (в скобках) — динамическая привязка.

Различие атрибут и свойство. Атрибут HTML — то, что написано в разметке. Свойство DOM — текущее значение элемента. Angular почти всегда работает со свойствами через [ ]. Иногда нужно именно атрибут — об этом следующий раздел.

Пример из TaskFlow — отключить кнопку, пока не введено название:


// в классе
title = '';
get canSave() {
  return this.title.trim().length > 0;
}

<input [value]="title" />
<button [disabled]="!canSave">Сохранить</button>
▶️ Быстрый запуск. В Angular [value] и [disabled] привязаны к свойствам DOM-элемента; в чистом JS то же делает чтение свойств и disabled. Введите текст — кнопка разблокируется:
Песочница: нажми «Запустить» — и увидишь, как кнопка блокируется и разблокируется в зависимости от введённого текста. Меняй код и запускай снова.
Песочница: привязка свойств (аналог на JS)
Урок 3 • Раздел 8

8. Attribute binding и class/style binding

Иногда свойство и атрибут различаются. Чтобы связаться именно с атрибутом, используем [attr.имя].


<td [attr.colspan]="colspan">Объединённая ячейка</td>
<input [attr.aria-label]="label" />

Привязка классов

Класс CSS добавляется по условию — это связь «внешний вид зависит от состояния» из урока 1 (раздел 10).


<li [class.done]="task.completed">
  {{ task.title }}
</li>
▶️ Быстрый запуск. В Angular [class.done] добавляет/убирает CSS-класс по условию; в чистом JS — classList.toggle. Нажмите кнопку — класс переключится:
Песочница: нажми «Запустить» — и увидишь, как по клику задаче добавляется или убирается CSS-класс (зачёркивание выполненной задачи). Меняй код и запускай снова.
Песочница: привязка классов (аналог на JS)

Класс done появится, только когда task.completed истинно. Так мы визуально отмечаем выполненные задачи без ручной работы с DOM.

Несколько классов через объект:


<div [class]="{
  'task': true,
  'task--done': task.completed,
  'task--high': task.priority === 'high'
}">
  {{ task.title }}
</div>

Привязка стилей


<p [style.color]="task.completed ? 'gray' : 'black'">
  {{ task.title }}
</p>

С префиксом единицы измерения:


<div [style.opacity]="task.completed ? 0.5 : 1">...</div>
<div [style.width.%]="progress">...</div>
Когда class, когда style. Для переключения заранее заданных правил используйте классы. Для точечного значения (цвет, заливка, размер из данных) — style binding. Предпочтительнее классы: логика оформления остаётся в CSS, а не в TypeScript.
Урок 3 • Раздел 9

9. Event binding ( )

Event binding (событие)="действие()" реагирует на событие DOM-элемента или компонента и вызывает метод класса. Это «событие вверх»: от пользователя к логике.

💡 Аналогия из жизни. Event binding — как звонок в дверь: гость (пользователь) нажимает кнопку, раздаётся звонок, и вы (метод класса) выходите посмотреть, что случилось. Без звонка вы бы не узнали, что кто-то пришёл, — так и Angular без (click) не узнает о клике.

Контекст. В TaskFlow через (click) отмечают задачу выполненной. Ниже — метод toggle(), который меняет состояние, и кнопка, вызывающая его.


// task-item.ts
export class TaskItem {
  completed = false;

  toggle() {
    this.completed = !this.completed;
  }
}

<button (click)="toggle()">Переключить статус</button>

Когда пользователь кликает, Angular вызывает toggle(). Состояние меняется, и (после перерисовки) обновляется интерфейс.

Другие события:


<input (input)="onInput()" />
<input (change)="onChange()" />
<form (submit)="onSubmit()">...</form>
<select (change)="onSelect()">...</select>
Важное ограничение из урока 2. В шаблоне нельзя использовать стрелочную функцию: (click)="() => toggle()" не работает, потому что Angular не отслеживает такие обработчики. Всегда вызывайте метод класса напрямую — (click)="toggle()".
▶️ Быстрый запуск. В Angular (click) вызывает метод класса; в чистом JS то же делает addEventListener. Нажмите «Запустить» и кликайте кнопку:
Песочница: нажми «Запустить» — и увидишь, как по клику кнопки меняется статус задачи (в работе → готово). Меняй код и запускай снова.
Песочница: событие (аналог на JS)
Урок 3 • Раздел 10

10. События: $event и передача данных

Внутри обработчика доступна специальная переменная $event — объект события. Её можно передать методу.


<input (input)="onInput($event)" />

// в классе
onInput(event: Event) {
  const value = (event.target as HTMLInputElement).value;
  console.log(value);
}

Через $event удобно обрабатывать поля ввода. А вот запись «двустороннего» связывания через сигналы, когда значение можно и показать, и обновить, появится в разделе 21 и позже (в уроке 5).

Для кнопок и форм часто проще не читать $event, а читать значение из компонента напрямую:


// в классе
newTitle = '';

addTask() {
  if (this.newTitle.trim() === '') {
    return; // guard clause из урока 2
  }
  console.log('Добавляем:', this.newTitle);
  this.newTitle = '';
}

<input [value]="newTitle" (input)="onInput($event)" />
<button (click)="addTask()">Добавить</button>
Guard clauses в методах. Приём «рано выйти при неподходящем состоянии» из урока 2 (раздел 6) отлично работает в методах компонента: сначала проверяем вход, затем выполняем основную работу.
Урок 3 • Раздел 11

11. Направления связи: две стрелки

Две пары скобок образуют ментальную модель связи класса и шаблона.

Два направления связи[ ] — property bindingданные из класса → DOMпример: [value] = "title"( ) — event bindingсобытие DOM → класспример: (click) = "toggle()"Вместе — интерактивность[...] показывает данные( ) обновляет состояниеновое состояние → снова [...]
Рисунок 2. Property binding несёт данные вниз, event binding поднимает события вверх. Их пара создаёт интерактивный UI.

Интерполяция {{ }} — частный случай чтения данных (вниз). Event binding ( ) — это вверх. Никакой из них по отдельности не даёт «двусторонней» связи — для неё нужны оба вместе, о чём поговорим в разделе 21.

Урок 3 • Раздел 12

12. Control flow: @if, @else, @else if

Современный Angular предоставляет встроенный синтаксис управления потоком прямо в шаблоне. @if условно выводит блок разметки.

💡 Аналогия из жизни. @if — как зонт: если на улице дождь (условие истинно), вы его достаёте и видите блок «Зонт открыт»; если солнце — блок не показывается. Шаблон сам решает, какую часть страницы нарисовать, опираясь на данные.

Контекст. В TaskFlow @if показывает список задач, когда они есть, и сообщение «Пока нет задач», когда пусто.


@if (tasks.length > 0) {
  <p>Задач: {{ tasks.length }}</p>
} @else {
  <p>Пока нет задач</p>
}

Несколько условий:


@if (status === 'loading') {
  <p>Загружаем…</p>
} @else if (status === 'error') {
  <p>Ошибка загрузки</p>
} @else {
  <p>Готово</p>
}

Условие — это любое выражение, возвращающее истину или ложь. Проверки на null, пустую строку, длину массива — всё из арсенала строгих сравнений из урока 2 (раздел 5).


@if (task && task.title) {
  <p>{{ task.title }}</p>
}
@if vs старый *ngIf. Пока вы не встретили *ngIf в проекте — используйте @if. Старые структурные директивы полезно знать для чтения legacy-кода, но новые проекты пишут на @if, @for, @switch.
▶️ Быстрый запуск. В Angular @if/@else выбирает блок по условию; в чистом JS — if/else. Нажмите кнопку, чтобы переключить наличие задач:
Песочница: нажми «Запустить» — и увидишь, как в зависимости от наличия задач показывается разный текст (как зонт в дождь). Меняй код и запускай снова.
Песочница: условный блок (аналог на JS)
Урок 3 • Раздел 13

13. Control flow: @for, track, $index, @empty

@for перебирает коллекцию и выводит содержимое блока для каждого элемента. Это основной способ построить список.

💡 Аналогия из жизни. @for — как конвейер на почте: письма (задачи) одно за другим проходят через аппарат, и для каждого печатается свой конверт (строка списка). Написали один шаблон строки — и он повторился для всех задач автоматически.

Контекст. В TaskFlow @for строит строки списка из массива tasks. Ключ track task.id помогает Angular не перепутать задачи при обновлении.


@for (task of tasks; track task.id) {
  <app-task-item [task]="task" />
}
track обязателен. В @for нужно указать track — стабильный ключ элемента (обычно id). Angular использует его, чтобы обновлять только изменённые элементы, а не перерисовать весь список. Без track Angular выдаст предупреждение о производительности.

Пустой список — отдельный случай. Блок @empty показывает разметку, когда коллекция пуста:


@for (task of tasks; track task.id) {
  <app-task-item [task]="task" />
} @empty {
  <p>Задач пока нет — добавьте первую!</p>
}
Важное различие. @empty — это «список получен и он пуст» (success + empty из урока 1, раздел 33). Оно не совпадает с «список ещё не загружен» (loading). Loading обычно показывают через @if по отдельному состоянию загрузки.

Внутри блока доступны специальные переменные цикла:


@for (task of tasks; track task.id; let i = $index; let first = $first; let last = $last) {
  <p>
    {{ first ? 'Начало: ' : '' }}
    {{ i + 1 }}. {{ task.title }}
    {{ last ? ' (конец)' : '' }}
  </p>
}
ПеременнаяЗначение
$indexИндекс текущего элемента (с 0).
$firstИстина для первого элемента.
$lastИстина для последнего элемента.
$even / $oddЧётность/нечётность индекса.
$countОбщее количество элементов в коллекции.
▶️ Быстрый запуск. В Angular @for перебирает массив и создаёт элементы; в чистом JS это map() + склейка строк. Нажмите «Запустить», чтобы увидеть список:
Песочница: нажми «Запустить» — и увидишь список задач, построенный автоматически из массива данных (как почтовый конвейер). Меняй код и запускай снова.
Песочница: цикл по списку (аналог на JS)
Урок 3 • Раздел 14

14. Control flow: @switch и @case

@switch выбирает одну ветку из нескольких по значению. Это прямое применение literal types и discriminated unions из урока 2 (разделы 31–32).

💡 Аналогия из жизни. @switch — как переключатель света в комнате с несколькими лампами: вы поворачиваете ручку в положение «высокий» — загорается одна лампа, в «низкий» — другая. Одновременно горит только та, что соответствует выбранному значению.

Контекст. В TaskFlow @switch по приоритету задачи показывает бейдж «Высокий / Средний / Низкий».


@switch (task.priority) {
  @case ('high') {
    <span class="badge badge-high">Высокий приоритет</span>
  }
  @case ('medium') {
    <span class="badge badge-medium">Средний</span>
  }
  @default {
    <span class="badge badge-low">Низкий</span>
  }
}

Блок @default выполняется, когда ни один @case не совпал. Его можно опустить, если ветка «по умолчанию» не нужна.

Когда @switch, а когда @if. @switch уместен, когда нужно выбрать одну из нескольких веток по одному значению (статус, тип, приоритет). @if/@else — когда условий несколько и они логические.

Ещё пример — типы задач из будущих версий TaskFlow:


@switch (task.kind) {
  @case ('task') { <span>Задача</span> }
  @case ('note') { <span>Заметка</span> }
  @case ('subtask') { <span>Подзадача</span> }
  @default { <span>Другое</span> }
}
▶️ Быстрый запуск. В Angular @switch/@case выбирает одну ветку по значению; в чистом JS — цепочка if/else. Нажимайте кнопку, чтобы менять приоритет:
Песочница: нажми «Запустить» — и увидишь, как по приоритету задачи меняется бейдж (как переключатель света). Меняй код и запускай снова.
Песочница: выбор по значению (аналог на JS)
Урок 3 • Раздел 15

15. Вложенный control flow: фильтрация в шаблоне

Control flow можно вкладывать: внутри @for@if, внутри условия — ещё список. Так строятся реальные экраны.


@for (group of groups; track group.name) {
  <h3>{{ group.name }} ({{ group.tasks.length }})</h3>
  @for (task of group.tasks; track task.id) {
    <app-task-item [task]="task" />
  } @empty {
    <p>Нет задач в этой группе</p>
  }
}
Осторожно с вложенностью. Глубокая вложенность ухудшает читаемость. Если шаблон становится сложным — вынесите часть в отдельный компонент (TaskGroup, TaskList), а вычисления — в методы класса. Это принцип композиции из раздела 19.

Частая задача — показать список, отфильтрованный по статусу. В шаблоне Angular нет произвольных инструкций, поэтому фильтровать лучше через метод или геттер класса:


// в классе
get activeTasks(): Task[] {
  return this.tasks.filter(t => !t.completed);
}

@for (task of activeTasks; track task.id) {
  <app-task-item [task]="task" />
}

Здесь filter() из урока 2 (раздел 9) работает в классе, а шаблон остаётся простым и понятным.

Урок 3 • Раздел 16

16. Модель Task и её связь с шаблоном

Модель Task мы создали в уроке 2 (разделы 40–42). Теперь она станет типом данных, которые шаблон отображает.


// task.ts
export interface Task {
  id: number;
  title: string;
  completed: boolean;
  priority: 'high' | 'medium' | 'low';
}

Добавим необязательные поля, которые используются в интерфейсе:


export interface Task {
  id: number;
  title: string;
  completed: boolean;
  priority: 'high' | 'medium' | 'low';
  tags?: string[];            // необязательно
  dueDate?: Date | null;      // может отсутствовать
}

Необязательные свойства (оператор ?) мы разбирали в уроке 2, раздел 33. В шаблоне с ними работает оператор необязательности ?:


<p>Срок: {{ task.dueDate?.toLocaleDateString() ?? 'не задан' }}</p>

?.toLocaleDateString() не упадёт, если даты нет, а ?? подставит запасное значение.

Зачем тип в шаблоне? TypeScript помогает, пока вы пишете класс. Но сам шаблон — это строка в HTML-файле. Для полной проверки Angular использует компилятор шаблона, который знает типы полей и методов класса. Опечатка в названии поля даст ошибку сборки, а не странный пустой экран.
Урок 3 • Раздел 17

17. Рендер списка TaskFlow

Соберём первый осмысленный экран: список задач на основе модели Task и всего контрольного потока.


// task-list.ts — файл класса компонента (создайте в src/app/task-list.ts)
import { Component } from '@angular/core';
import { TaskItem } from '../task-item/task-item'; // дочерний компонент
import type { Task } from '../task';              // тип модели Task

@Component({
  selector: 'app-task-list',   // тег для вставки: <app-task-list></app-task-list>
  standalone: true,            // современный компонент без NgModule
  imports: [TaskItem],         // что нужно шаблону для работы
  templateUrl: './task-list.html', // путь к шаблону
  styleUrls: ['./task-list.css'],  // путь к стилям
})
export class TaskList {
  tasks: Task[] = [            // данные списка (массив моделей Task)
    { id: 1, title: 'Изучить компоненты', completed: true, priority: 'high' },
    { id: 2, title: 'Связать шаблон', completed: false, priority: 'medium' },
    { id: 3, title: 'Собрать список', completed: false, priority: 'low' },
  ];

  // вычисляемое свойство: сколько задач выполнено (filter из урока 2)
  get completedCount(): number {
    return this.tasks.filter(t => t.completed).length;
  }
}

<!-- task-list.html — шаблон компонента (src/app/task-list.html) -->
<h2>Мои задачи</h2>
<!-- интерполяция: подставляем значения из класса (completedCount, tasks.length) -->
<p>Выполнено: {{ completedCount }} из {{ tasks.length }}</p>

<!-- @if/@else: показать список ИЛИ сообщение, когда пусто -->
@if (tasks.length > 0) {
  <ul>
    <!-- @for: для каждой задачи; track task.id — стабильный ключ для Angular -->
    @for (task of tasks; track task.id) {
      <!-- [task]="task": передаём объект задачи в дочерний компонент -->
      <li><app-task-item [task]="task" /></li>
    }
  </ul>
} @else {
  <p>Пока нет задач — добавьте первую.</p>
}

Обратите внимание:

Урок 3 • Раздел 18

18. Сравнение с будущим React: map() и key

Мы будем изучать React со второй половины курса. Полезно уже сейчас провести параллель, как это делалось в уроке 1 (раздел 24).

В Angular список строится через @for ... track. В React — через JavaScript-метод map() с атрибутом key:


// React (позже в курсе)
<ul>
  {tasks.map(task => (
    <TaskItem key={task.id} task={task} />
  ))}
</ul>
ЗадачаAngularReact
Перебор списка@for.map()
Ключ элементаtrackkey
Условие@if&& или тернарник
Переключатель@switchswitch или объект-маппинг
Пустой список@emptyусловие по length
Общая идея. И Angular, и React выводят списки по одним и тем же данным (массивам из урока 2). Отличается только синтаксис. Освоив массив tasks и методы map/filter/find, вы сможете отобразить его любой библиотекой.
Урок 3 • Раздел 19

19. Компонентная композиция: дерево TaskFlow

Большой экран собирается из маленьких компонентов. Это и есть композиция — принцип, который мы разбирали для React в уроке 1 (раздел 23), но он в равной мере относится к Angular.

Дерево компонентов TaskFlowApp (корень)TaskListданные: tasks[], фильтрTaskItemданные: taskTaskFormсобытие: создатьFilterBarсобытие: фильтрКак читать схемуДанные текут внизСобытия текут вверхКаждый блок — отдельныйфайл и ответственность
Рисунок 3. Компонентная композиция TaskFlow: список владеет задачей рендеринга, дочерние компоненты получают данные и сообщают о событиях.

Компонент решает одну задачу. TaskItem показывает одну задачу, FilterBar — фильтры, TaskForm — создание. Это делает код читаемым и переиспользуемым.

Почему так важно делить экран на компоненты?

Композиция — общая идея и для Angular, и для React. В Angular связь между компонентами задаётся декораторами и шаблоном, в React — props и callbacks, но принцип «один компонент — одна ответственность» одинаков.

Правило границы. Задавайте себе вопрос про каждый компонент: «какая у него ответственность?». Если ответ звучит как перечисление — компонент слишком большой и его стоит разбить.
Урок 3 • Раздел 20

20. Передача данных вниз: input()

Чтобы дочерний компонент получил данные, родитель передаёт их через property binding, а дочерний объявляет вход через функцию input().

💡 Аналогия из жизни. input() — как почтовый ящик у двери дочернего компонента: родитель опускает туда письмо (задачу) через [task]="task", а компонент-жилец достаёт его и читает. Без ящика передать письмо некуда.

Контекст. В TaskFlow родитель TaskList передаёт каждой карточке TaskItem объект задачи именно так.


// task-item.ts — дочерний компонент (src/app/task-item/task-item.ts)
import { Component, input } from '@angular/core';
import type { Task } from '../task';          // тип модели задачи

@Component({
  selector: 'app-task-item',                  // тег <app-task-item>
  standalone: true,                           // современный компонент
  templateUrl: './task-item.html',            // его шаблон
})
export class TaskItem {
  // input.required — ОБЯЗАТЕЛЬНЫЙ вход: родитель должен передать task,
  // иначе Angular выдаст ошибку. В шаблоне читаем как task().title
  readonly task = input.required<Task>();
}

Форма входа:

В шаблоне читаем через вызов: task().title. В классе — this.task().


<div>
  <input type="checkbox" [checked]="task().completed" />
  <span>{{ task().title }}</span>
</div>
Почему скобки? input() возвращает сигнал. Чтение сигнала — это вызов функции: task(). Мы подробно разберём сигналы в разделе 21 и ещё глубже — в уроке 5. Здесь важно принять: вход объявляется через input(...).

Родитель передаёт значение:


@for (task of tasks; track task.id) {
  <app-task-item [task]="task" />
}
Урок 3 • Раздел 21

21. Сигналы: читать состояние в шаблоне

В уроке 1 (раздел 40) мы встретились с signal() и update(). Сигнал — реактивное значение: когда оно меняется, Angular перерисовывает те места шаблона, которые его читают.

💡 Аналогия из жизни. Сигнал — как группа в мессенджере: вы публикуете сообщение (новое значение), и все подписчики сразу его получают. В Angular «подписчики» — это места шаблона, где вы прочитали сигнал; как только значение изменилось, они перерисовываются без вашего участия.

Контекст. В TaskFlow сигнал tasks хранит список; когда мы добавляем задачу, шаблон сам показывает её — перерисовывать вручную не надо.


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

@Component({
  selector: 'app-task-page',
  standalone: true,
  // шаблон задан прямо в классе через template (вместо отдельного файла)
  template: `
    <p>Всего задач: {{ tasks().length }}</p>
    <p>Выполнено: {{ completedCount() }}</p>
  `,
})
export class TaskPage {
  // signal — реактивное значение. tasks() возвращает ТЕКУЩИЙ массив,
  // и Angular перерисовывает шаблон, когда значение меняется
  readonly tasks = signal<Task[]>([
    { id: 1, title: 'Изучить Angular', completed: true, priority: 'high' },
  ]);

  // метод читает сигнал через tasks() и вычисляет производное значение
  completedCount() {
    return this.tasks().filter(t => t.completed).length;
  }
}

Разница между геттером и функцией для вычисляемого значения: сигналы читаются как функции. Для производных значений, которые зависят от сигналов и кэшируются, Angular предлагает computed() — его вы подробно изучите в уроке 5. На этом уроке достаточно прямого чтения сигналов в шаблоне.

Двусторонняя привязка через сигналы. Если поле — сигнал, в шаблоне можно записывать [value]="title()" и читать (input)="title.set($any($event.target).value)". Это показывается как пример; полноценные формы и модель связывания мы разберём в уроке 8.
Урок 3 • Раздел 22

22. Стили компонента: encapsulation

По умолчанию стили компонента изолированы: классы внутри одного компонента не «протекают» в другие. Это называется view encapsulation.


/* task-item.css */
.done span { text-decoration: line-through; color: gray; }
.urgent { color: #d97706; font-weight: bold; }

Angular добавляет уникальные атрибуты к элементам шаблона, чтобы .done из TaskItem не влиял на такой же класс в другом компоненте.

РежимПоведение
Emulated (по умолчанию)Стили изолированы путём добавления атрибутов.
NoneСтили применяются глобально.
ShadowDomИспользуется настоящий Shadow DOM.

@Component({
  selector: 'app-task-item',
  standalone: true,
  encapsulation: ViewEncapsulation.None, // обычно не нужно
})
export class TaskItem { }
Практический совет. Оставьте режим по умолчанию. Изоляция стилей — это залог того, что изменение стилей одного компонента не сломает другой. Глобальные стили храните в styles.css проекта.

Иногда нужно, чтобы стиль родителя влиял на дочерний элемент сквозь границу компонента. Для этого Angular даёт конструкцию ::ng-deep, но её чрезмерное использование разрушает изоляцию. Придерживайтесь правила: минимальное количество ::ng-deep, лучше — передача входных данных и стилизация внутри самого компонента.

Для корректной вёрстки с внешними утилитами (например, CSS-фреймворками) удобнее глобальные классы в styles.css — они доступны везде и не требуют ::ng-deep.

Чтобы стилизовать элемент дочернего компонента из родителя, Angular предоставляет псевдокласс :host и :host-context. Базовое правило — стилизовать собственные элементы компонента, а «сквозные» состояния (например, зачёркивание выполненных задач) держать внутри своего же компонента.

Урок 3 • Раздел 23

23. Обработка ошибок в шаблоне: null, undefined, optional chaining

Данные могут быть null или undefined — особенно когда они приходят от API. Обращение к свойству такого значения вызывает ошибку. В шаблоне защищаемся оператором необязательности ?.


@if (task) {
  <p>{{ task.title }}</p>
}

<p>{{ task?.title }}</p>
<p>{{ task?.dueDate?.toLocaleDateString() ?? 'без срока' }}</p>

?. возвращает undefined вместо ошибки, если промежуточное значение пустое. ?? даёт запасное значение, когда слева null/undefined.

Различие, важное из урока 2. Строгие сравнения === и проверка на null/undefined — ваши главные инструменты. Не полагайтесь на неявные приведения. В шаблоне лучше явно писать task?.title, чем вслепую обращаться к полю.

Частый паттерн — проверка наличия до рендера списка:


@if (tasks) {
  @if (tasks.length === 0) {
    <p>Список пуст</p>
  } @else {
    @for (task of tasks; track task.id) {
      <app-task-item [task]="task" />
    }
  }
} @else {
  <p>Данные ещё не загружены</p>
}
Урок 3 • Раздел 24

24. Практика 1: компонент TaskItem

Создадим компонент одной задачи. Он получает объект Task и показывает заголовок, чекбокс и приоритет.


ng generate component task-item

// 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',
  styleUrls: ['./task-item.css'],
})
export class TaskItem {
  readonly task = input.required<Task>();
}

<!-- task-item.html -->
<div class="task-item" [class.done]="task().completed">
  <input
    type="checkbox"
    [checked]="task().completed"
    [attr.aria-label]="'Отметить: ' + task().title"
  />
  <span>{{ task().title }}</span>
  @switch (task().priority) {
    @case ('high')   { <em>Срочно</em> }
    @case ('medium') { <em>Средне</em> }
    @default         { <em>Не срочно</em> }
  }
</div>

/* task-item.css */
.task-item { display: flex; gap: 0.5rem; align-items: center; }
.done span { text-decoration: line-through; color: gray; }
em { font-style: normal; margin-left: 0.4rem; color: #d97706; }

Задание.

Создайте TaskItem и выведите его внутри TaskList из раздела 17. Убедитесь, что выполненные задачи отображаются зачёркнутыми, а срочные — с пометкой «Срочно».

Урок 3 • Раздел 25

25. Практика 2: компонент TaskList

TaskList владеет данными и собирает экран. Он импортирует дочерние компоненты и связывает их.


ng generate component task-list

// 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' },
    { id: 3, title: 'Собрать список', completed: false, priority: 'low' },
  ]);

  completedCount() {
    return this.tasks().filter(t => t.completed).length;
  }

  activeCount() {
    return this.tasks().filter(t => !t.completed).length;
  }
}

<!-- task-list.html -->
<section>
  <h2>Мои задачи</h2>
  <p>Выполнено {{ completedCount() }} из {{ tasks().length }}</p>

  @if (tasks().length > 0) {
    <ul>
      @for (task of tasks(); track task.id) {
        <li>
          <app-task-item [task]="task" />
        </li>
      }
    </ul>
  } @else {
    <p>Пока нет задач — добавьте первую.</p>
  }
</section>

Здесь tasks — сигнал, поэтому в шаблоне читаем как tasks(). Заметьте: @for (... of tasks()) — итерация по текущему значению сигнала.

Задание.

Соберите TaskList с сигналом tasks, выведите счётчики через методы и список через @for. Подключите TaskList в корневой App.

Урок 3 • Раздел 26

26. Практика 3: FilterBar со статусами

FilterBar показывает кнопки фильтров. Выбранный фильтр хранится в типе TaskFilter — union literal types из урока 2 (разделы 31–32).


// filter-bar.ts
import { Component, signal } 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 = signal<TaskFilter>('all');
  counts = { all: 3, active: 2, done: 1 };

  setFilter(value: TaskFilter) {
    this.filter.set(value);
  }
}

<div class="filters">
  <button
    (click)="setFilter('all')"
    [class.active]="filter() === 'all'"
  >Все ({{ counts.all }})</button>
  <button
    (click)="setFilter('active')"
    [class.active]="filter() === 'active'"
  >Активные ({{ counts.active }})</button>
  <button
    (click)="setFilter('done')"
    [class.active]="filter() === 'done'"
  >Выполненные ({{ counts.done }})</button>
</div>

/* filter-bar.css */
.filters { display: flex; gap: 0.5rem; margin: 1rem 0; }
button { border: 1px solid var(--line); background: var(--card); padding: 0.4rem 0.9rem; cursor: pointer; border-radius: 8px; }
button.active { background: var(--accent); color: #fff; border-color: var(--accent); }

Здесь состояние «какой фильтр выбран» хранится в сигнале, а [class.active] подсвечивает нужную кнопку. Обратите внимание на используемый тип TaskFilter: он ограничивает допустимые значения — это польза literal types.

Задание.

Создайте FilterBar и добавьте его в TaskList над списком. Реализуйте подсветку выбранного фильтра через [class.active] и @switch по мере желания.

Урок 3 • Раздел 27

27. Практика 4: StatsBar со счётчиками

StatsBar показывает статистику: сколько всего, активных, выполненных. Это упражнение на вычисляемые значения и их вывод в шаблон.


// 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',
  styleUrls: ['./stats-bar.css'],
})
export class StatsBar {
  readonly tasks = input.required<Task[]>();

  done() {
    return this.tasks().filter(t => t.completed).length;
  }

  active() {
    return this.tasks().filter(t => !t.completed).length;
  }
}

<div class="stats">
  <span>Всего: {{ tasks().length }}</span>
  <span>Активных: {{ active() }}</span>
  <span>Выполнено: {{ done() }}</span>
</div>

/* stats-bar.css */
.stats { display: flex; gap: 1rem; color: var(--muted); margin-bottom: 0.5rem; }

StatsBar получает массив через input() и сам вычисляет производные значения методами. Такой компонент переиспользуем: он не знает, откуда пришли задачи.

Задание.

Добавьте StatsBar в TaskList и передайте ему [tasks]="tasks()". Убедитесь, что счётчики корректно пересчитываются.

Урок 3 • Раздел 28

28. Практика 5: форма-заглушка TaskForm

Полноценные формы Angular разберём в уроке 8. Здесь — заглушка, которая используется для построения структуры экрана и обработки события submit.


// task-form.ts — компонент формы (src/app/task-form/task-form.ts)
import { Component, output } from '@angular/core';  // output — исходящее событие

@Component({
  selector: 'app-task-form',
  standalone: true,
  templateUrl: './task-form.html',
  styleUrls: ['./task-form.css'],
})
export class TaskForm {
  newTitle = '';                       // текущее значение поля ввода

  // output — компонент СООБЩАЕТ родителю строку (название задачи).
  // Родитель ловит её через (created)="onCreate($event)"
  readonly created = output<string>();

  // событие ввода: берём значение из поля и кладём в newTitle
  onInput(event: Event) {
    this.newTitle = (event.target as HTMLInputElement).value;
  }

  submit() {
    if (this.newTitle.trim() === '') {
      return; // guard clause
    }
    this.created.emit(this.newTitle.trim());
    this.newTitle = '';
  }
}

<form (submit)="submit()">
  <input
    [value]="newTitle"
    (input)="onInput($event)"
    placeholder="Название новой задачи"
    aria-label="Название задачи"
  />
  <button type="submit" [disabled]="newTitle.trim() === ''">
    Добавить
  </button>
</form>
output() — современный способ объявить событие компонента (аналог @Output). Родитель слушает его через event binding и реагирует. Передачу событий между компонентами детально рассмотрим в уроке 4; здесь достаточно увидеть, что родитель может «поймать» событие (created).

Задание.

Добавьте TaskForm в TaskList. Пока метод-обработчик в TaskList может просто выводить значение в консоль — связывание с массивом разберём в уроке 4.

Урок 3 • Раздел 28.1

28.1. Атрибут vs свойство DOM: когда [attr.*]

Мы уже упомянули различие между HTML-атрибутом и свойством DOM. Разберём это подробнее, потому что от него зависят некоторые биндинги.

После загрузки страницы атрибут и свойство связаны, но при взаимодействии пользователя они могут разойтись. Angular через [ ] работает преимущественно со свойствами, а через [attr.имя] — с атрибутами.


<!-- привязка к свойству (обычный случай) -->
<input [value]="title" />

<!-- привязка к атрибуту (редкий случай) -->
<td [attr.colspan]="colspan">...</td>
<input [attr.aria-label]="labelText" />

Где нужен атрибут, а не свойство?

СлучайПример
ARIA-атрибуты[attr.aria-label], [attr.aria-expanded]
SVG-атрибуты[attr.x], [attr.y], [attr.width]
Атрибуты без свойстваcolspan, rowspan
Произвольные data-атрибуты[attr.data-id]
Практическое правило. Начинайте со свойства ([ ]). Переходите на [attr.*] только когда свойства нет или нужен именно атрибут: доступность, SVG, нестандартные атрибуты.
Урок 3 • Раздел 28.2

28.2. Шаблонные ссылки #ref и их назначение

Шаблонная ссылка #имя даёт имя элементу в шаблоне, чтобы обращаться к нему в другом месте этого же шаблона.


<input #titleInput (input)="onChange(titleInput.value)" />

Здесь #titleInput — ссылка на элемент, а titleInput.value читает его значение без обращения к $event.

Шаблонные ссылки полезны, когда нужно:


<form>
  <input #t (input)="onChange(t.value)" placeholder="Задача" />
  <button (click)="add(t.value)">Добавить</button>
</form>
Замечание. Ссылки работают только внутри шаблона, где они объявлены. Чтобы передать значение другому компоненту, нужно событие output(), которое мы изучим в уроке 4. На этом уроке достаточно понимать идею ссылки.
Урок 3 • Раздел 28.3

28.3. Связывание TaskForm с TaskList

Мы разобрали компоненты по отдельности. Теперь соединим их так, чтобы данные и события двигались в нужном направлении.

Задача: TaskForm собирает название, TaskList владеет массивом задач. Когда пользователь добавляет задачу, TaskForm сообщает об этом наверх, а TaskList добавляет запись.

Поток: TaskForm → TaskListTaskFormновый titlecreated.emit(title)guard: title.trim()(created)TaskList (родитель)onCreated(title) { this.tasks.update(...) add new Task}tasks()Шаблон@for по tasks()новый элементпоявляется сам
Рисунок 5. Form сообщает наверх через событие, List обрабатывает и обновляет сигнал, шаблон перерисовывается.

Схема принципов единая для всего курса: данные вниз, события вверх. Событие (created) идёт вверх к владельцу данных; владелец меняет состояние; шаблон показывает новое состояние.

Урок 3 • Раздел 28.4

28.4. Практика 6: интерактивное добавление задачи

Соединим TaskForm и TaskList. TaskList слушает событие created и добавляет задачу в сигнал.


// task-list.ts (обновлённый)
import { Component, signal } from '@angular/core';
import { TaskItem } from '../task-item/task-item';
import { TaskForm } from '../task-form/task-form';
import type { Task } from '../task';

@Component({
  selector: 'app-task-list',
  standalone: true,
  imports: [TaskItem, TaskForm],
  templateUrl: './task-list.html',
})
export class TaskList {
  readonly tasks = signal<Task[]>([
    { id: 1, title: 'Изучить компоненты', completed: true, priority: 'high' },
  ]);

  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]);
  }
}

<!-- task-list.html (обновлённый) -->
<section>
  <h2>Мои задачи</h2>
  <app-task-form (created)="onCreated($event)" />

  @if (tasks().length > 0) {
    <ul>
      @for (task of tasks(); track task.id) {
        <li><app-task-item [task]="task" /></li>
      }
    </ul>
  } @else {
    <p>Пока нет задач.</p>
  }
</section>

Что здесь важно:

Не изменяйте сигнал методом push напрямую. Сигнал обновляется через update()/set(), создавая новое значение. Если мутировать внутренний массив, Angular может не заметить изменение. Это следствие принципа «неизменяемого состояния» из урока 2.

Задание.

Обновите TaskList из раздела 25: подключите TaskForm через событие (created), добавьте метод onCreated и проверьте, что новая задача появляется в списке.

Урок 3 • Раздел 28.5

28.5. Работа с вложенными данными в шаблоне

Данные задач часто бывают вложенными: у задачи есть свойства, внутри массивы, внутри объектов. Разберём вывод таких структур.


export interface Task {
  id: number;
  title: string;
  completed: boolean;
  tags?: string[];
  assigned?: { name: string; role: string } | null;
}

Вывод тегов — вложенный @for:


@if (task().tags && task().tags.length) {
  <ul class="tags">
    @for (tag of task().tags; track tag) {
      <li>{{ tag }}</li>
    }
  </ul>
}

Вывод вложенного объекта с проверкой на null:


@if (task().assigned) {
  <p>Ответственный: {{ task().assigned!.name }}
     ({{ task().assigned!.role }})</p>
} @else {
  <p>Не назначено</p>
}
Оператор непустого утверждения !. task().assigned!.name говорит TypeScript «я уверен, что не null», потому что проверка @if уже исключила пустое значение. Без ! компилятор может ругаться. Используйте его только после проверки.

Если вложенность становится глубокой, лучше выносить фрагмент в отдельный компонент. Вложенный вывод читается тяжело — композиция решает эту проблему.

Урок 3 • Раздел 28.6

28.6. Классы и стили: расширенные приёмы

Мы разобрали [class.имя] и [class] с объектом. Покажем ещё несколько удобных приёмов, которые пригодятся в TaskFlow.

Строковая привязка классов


// в классе
get itemClass(): string {
  const parts = ['task-item'];
  if (this.task().completed) parts.push('done');
  if (this.task().priority === 'high') parts.push('urgent');
  return parts.join(' ');
}

<div [class]="itemClass">...</div>

Условные стили по состоянию


<span
  [style.color]="task().completed ? 'gray' : 'var(--ink)'"
  [style.font-weight]="task().priority === 'high' ? 'bold' : 'normal'"
>{{ task().title }}</span>

Стили через объект


<div [style]="{
  'border-left': task().priority === 'high' ? '4px solid #d97706' : 'none',
  'opacity': task().completed ? 0.6 : 1
}">...</div>
Совет по чистоте. Чем больше логики в шаблоне, тем сложнее его тестировать. Геттеры в классе возвращают готовые классы и стили — так шаблон остаётся «описанием того, что показать», а решения принимает TypeScript.
Урок 3 • Раздел 28.7

28.7. Доступность (a11y) в шаблонах

В уроке 1 (раздел 28) мы поговорили о доступности. В шаблонах Angular это выражается через правильные атрибуты и связывание с состоянием.

ARIA и состояние


<button
  [attr.aria-pressed]="filter() === 'all'"
  (click)="setFilter('all')"
>Все</button>

aria-pressed сообщает скринридеру, выбрана ли «нажатая» кнопка фильтра. Оно меняется вместе с состоянием.

Роль списка и элементов


<ul role="list">
  @for (task of tasks(); track task.id) {
    <li role="listitem">
      <input type="checkbox"
             [checked]="task.completed"
             [attr.aria-label]="'Задача: ' + task.title" />
    </li>
  }
</ul>

Фокус при изменении содержания

Когда список меняется (добавилась задача), важно, чтобы пользователь с клавиатуры получал обратную связь. Простая забота — сообщение после действия:


<p aria-live="polite">{{ notice }}</p>

aria-live="polite" заставляет скринридер озвучивать изменения, даже если пользователь не сфокусирован на элементе.

Минимум для TaskFlow. 1) подписывайте checkbox через aria-label; 2) отмечайте выбранный фильтр через aria-pressed; 3) добавляйте текст подсказки для состояний нагрузки и пустого списка. Это небольшие строки, а доступность растёт заметно.
Урок 3 • Раздел 28.8

28.8. Практика 7: завершаем мастер-шаблон

Соберём весь экран из практик 1–6 в единый компонент TaskList, добавив статистику и фильтры. Это упражнение — «скелет» будущего TaskFlow, который мы наполним сервисами в уроках 6–7.


// 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',
  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' },
    { id: 3, title: 'Собрать список', completed: false, priority: 'low' },
  ]);

  readonly filter = signal<TaskFilter>('all');

  visibleTasks() {
    const f = this.filter();
    switch (f) {
      case 'done': return this.tasks().filter(t => t.completed);
      case 'active': return this.tasks().filter(t => !t.completed);
      default: return this.tasks();
    }
  }

  setFilter(value: TaskFilter) {
    this.filter.set(value);
  }

  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]);
  }
}

<!-- task-list.html (финал) -->
<section class="page">
  <header>
    <h2>Мои задачи</h2>
    <app-stats-bar [tasks]="tasks()" />
  </header>

  <app-task-form (created)="onCreated($event)" />
  <app-filter-bar [filter]="filter()" (filterChange)="setFilter($event)" />

  @if (visibleTasks().length > 0) {
    <ul>
      @for (task of visibleTasks(); track task.id) {
        <li>
          <app-task-item [task]="task" />
        </li>
      }
    </ul>
  } @else {
    <p class="empty">Подходящих задач нет.</p>
  }
</section>

Обратите внимание на метод visibleTasks(): он применяет filter() из урока 2 по выбранному фильтру. Здесь мы уже передаём фильтр вниз ([filter]) и получаем изменение вверх ((filterChange)) — полный цикл связи компонентов, который подробно разберём в уроке 4.

Почему это ещё не настоящий TaskFlow? Данные пока живут в компоненте, а не в сервисе, и не синхронизируются с сервером. Это осознанный этап: сначала шаблоны и связь, затем (уроки 6–7) вынесем данные в сервис и подключим HTTP.

Задание (финальное).

Соберите мастер-шаблон из этого раздела: отдельные компоненты TaskItem, TaskForm, FilterBar, StatsBar внутри TaskList. Проверьте: добавление задачи, фильтрацию по статусам и корректную статистику.

Урок 3 • Раздел 28.9

28.9. Производительность шаблонов: track, @defer, простые выражения

Шаблон выполняется Angular многократно. Правильное написание выражений влияет на скорость. Разберём три важные практики.

track для минимума перерисовок

Без track Angular по умолчанию сравнивает элементы по ссылке; при каждом обновлении массива список может перерисовываться целиком. Со стабильным track task.id Angular переиспользует элементы и обновляет только изменённые.


@for (task of tasks(); track task.id) {
  <app-task-item [task]="task" />
}

Вызовы методов в шаблоне

Каждый вызов {{ someMethod() }} происходит при проверке изменений. Тяжёлые операции в методе замедляют рендер. Убирайте вычисления из шаблона и переиспользуйте результаты.


// плохо: метод фильтрует массив на каждый рендер
<!-- {{ filterTasks() }} -->

// лучше: сигнал или computed с кэшем (разберём в уроке 5)
readonly visible = computed(() => this.tasks().filter(t => !t.completed));

Отложенная загрузка тяжёлых частей

Блок @defer откладывает создание части шаблона до события или появления в зоне видимости. Это удобно для больших, невидимых сразу блоков.


@defer (on viewport) {
  <app-export-panel [tasks]="tasks()" />
} @placeholder {
  <p>Панель экспорта появится при прокрутке</p>
}
Правило обучения. Сначала сделайте правильно и читаемо. Оптимизируйте только там, где заметили проблему (хм, heavy re-render в DevTools). Ранняя микрооптимизация усложняет код.
Урок 3 • Раздел 28.10

28.10. Таблица-шпаргалка биндингов и control flow

Сведём все конструкции урока в одну справочную таблицу. Она пригодится как конспект.

ЗадачаСинтаксисПример
Показать значение{{ }}{{ task.title }}
Передать значение в свойство[ ][value]="title"
Передать в атрибут[attr.*][attr.aria-label]="txt"
Слушать событие( )(click)="go()"
Условие@if/@else@if (ok) {...} @else {...}
Список@for ... track@for (t of ts; track t.id)
Пустой список@empty@for (...) {} @empty {...}
Переключение@switch/@case@switch (x) { @case (1) {} }
Класс по условию[class.*][class.done]="ok"
Стиль по условию[style.*][style.color]="c"
Безопасное чтение?. , ??task?.dueDate ?? '—'
Отложенная загрузка@defer@defer (on viewport)
Совет. Скопируйте таблицу в свой конспект и дополняйте по мере изучения Angular в уроках 4–13.
Урок 3 • Раздел 28.11

28.11. Практика 8: отметка задачи выполненной

Сделаем чекбокс рабочим: отметка меняет состояние задачи. Так мы свяжем событие и обновление сигнала — сердце интерактивности.


// task-list.ts (добавляем метод)
toggle(taskId: number) {
  this.tasks.update(current =>
    current.map(t =>
      t.id === taskId ? { ...t, completed: !t.completed } : t
    )
  );
}

Метод использует три важные конструкции из урока 2:

Теперь TaskItem должен сообщить о клике наверх.


// task-item.ts (добавляем выход)
export class TaskItem {
  readonly task = input.required<Task>();
  readonly toggled = output<void>();

  toggle() {
    this.toggled.emit();
  }
}

<!-- task-item.html -->
<div class="task-item" [class.done]="task().completed">
  <input
    type="checkbox"
    [checked]="task().completed"
    (change)="toggle()"
    [attr.aria-label]="'Отметить: ' + task().title"
  />
  <span>{{ task().title }}</span>
</div>

<!-- task-list.html (слушаем событие) -->
@for (task of visibleTasks(); track task.id) {
  <li>
    <app-task-item
      [task]="task"
      (toggled)="toggle(task.id)"
    />
  </li>
}

Полный цикл:

  1. пользователь кликает чекбокс;
  2. Angular вызывает toggle() внутри TaskItem;
  3. TaskItem испускает событие toggled;
  4. TaskList ловит его и вызывает toggle(task.id);
  5. сигнал обновляется новым массивом;
  6. шаблон перерисовывает задачу зачёркнутой.
Это паттерн «данные вниз, события вверх» в действии. Дочерний компонент не меняет данные сам — он сообщает о намерении родителю, владельцу состояния. Так структура остаётся предсказуемой.

Задание.

Добавьте метод toggle в TaskList и событие toggled в TaskItem. Проверьте, что отметка выполнения обновляет и счётчики StatsBar.

Урок 3 • Раздел 29

29. Самостоятельная работа

Шесть заданий. Сначала попробуйте сами, затем сверьтесь с подсказками в разделе 30.

Задание 1 — Статус задачи

В TaskItem выведите «Выполнено» или «В работе» через @if в зависимости от task().completed.

Задание 2 — Счётчик активных

В TaskList добавьте метод activeCount(), который считает невыполненные задачи через filter(), и выведите его.

Задание 3 — Пустой список

Сделайте массив задач пустым и убедитесь, что через @empty или @else показывается сообщение «Пока нет задач».

Задание 4 — Бейдж «Срочно»

Добавьте рядом с задачей, у которой priority === 'high', пометку «Срочно» через @if и класс [class.urgent].

Задание 5 — Показ выполненных

Добавьте кнопку, которая переключает показ выполненных задач. Используйте сигнал showDone и метод для его переключения.

Задание 6 — Номера в списке

Через переменную $index выведите номер перед каждой задачей: «1. Изучить компоненты».

Урок 3 • Раздел 30

30. Подсказки к самостоятельной работе

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

Задание 1. @if (task().completed) { <span>Выполнено</span> } @else { <span>В работе</span> }
Задание 2. activeCount() { return this.tasks().filter(t => !t.completed).length; }
Задание 3. Оставьте @for (... ) { ... } @empty { ... } — блок @empty сработает при пустом массиве.
Задание 4. @if (task().priority === 'high') { <span class="urgent">Срочно</span> }. Для подсветки самого блока — [class.urgent]="task().priority === 'high'".
Задание 5. Поле showDone = signal(false) и метод toggleShowDone() { this.showDone.update(v => !v); }. В шаблоне кнопка (click)="toggleShowDone()", а фильтрация через метод, который учитывает showDone().
Задание 6. В @for (task of tasks(); track task.id; let i = $index) вывести {{ i + 1 }}. {{ task.title }}.
Урок 3 • Раздел 31

31. Частые ошибки и как их читать

Понимание текста ошибки ускоряет работу. Разберём типичные ситуации.

СимптомПричинаРешение
«Property 'completed' does not exist»В шаблоне обращаетесь к полю, которого нет в типе.Проверьте имя поля и тип.
«NG0303: Can't bind to 'task'»Дочерний компонент не объявляет вход task или не импортирован.Добавьте input() и компонент в imports.
«NG0302: Cannot find control »Используете поле формы, которого нет.Проверьте FieldName (актуально при Реактивных формах, урок 8).
Пустой экран без ошибокУсловие @if не выполняется (данные не загружены).Проверьте состояние: задачи пусты или signal пуст.
Предупреждение про trackВ @for не указан track.Добавьте track task.id.
«Can't use arrow function in template»Попытались написать стрелочную функцию в биндинге.Вынесите логику в метод класса.
Стратегия отладки из урока 1 (раздел 27). 1) воспроизведите; 2) определите уровень: компилятор, runtime, шаблон, данные; 3) посмотрите Console и Network в DevTools; 4) сформулируйте одну гипотезу и проверьте её.

Мини-пример: читаем ошибку компилятора


NG0302: Can't bind to 'completed' since it isn't a known
property of 'app-task-item'.

1. If 'app-task-item' is an Angular component and it has
   'completed' input, then verify that it is part of this
   module. 
2. ... If 'app-task-item' is a Web Component then add
   'CUSTOM_ELEMENTS_SCHEMA' ...

Что здесь произошло: мы попытались передать [completed], но TaskItem не объявляет такого входа. Решение — либо объявить вход через input(), либо исправить имя свойства. Angular подсказывает возможные причины — читайте их, а не переписывайте всё «вслепую».

Системный подход. Сообщение об ошибке обычно указывает файл и строку. Откройте точное место, посмотрите, что Angular считает источником, и проверьте одно предположение за раз.
Урок 3 • Раздел 32

32. Резюме: что мы построили

За этот урок мы прошли путь от одного компонента до маленького интерфейса TaskFlow:

Этот урок — фундамент шаблонов Angular. С ним вы сможете читать и писать компоненты, связывать данные через биндинги, строить условную разметку и списки, а также собирать экран из нескольких компонентов.

Что пройдено и куда двигатьсяУрок 3 (этот урок)• Компоненты и шаблоны• Биндинги [ ] и ( )• @if / @for / @switch• input() и output()• Сигналы: простой стартДальше (уроки 4–5)• @Input / @Output подробно• computed() и реактивность• события между компонентами• изменение состояния спискаВ перспективе• Сервисы и DI (урок 6)• HTTP + mock API (урок 7)• Формы (урок 8)• Роутер (урок 9)
Рисунок 4. Урок 3 — фундамент шаблонов. Уроки 4–5 добавляют реактивность, дальше — сервисы, HTTP, формы и роутер.
Урок 3 • Раздел 33

33. Контрольные вопросы

  1. Из каких трёх частей состоит компонент Angular?
  2. Что делает selector в @Component?
  3. Чем интерполяция {{ }} отличается от property binding [ ]?
  4. Какое направление связи реализует event binding ( )?
  5. Зачем в @for нужен track?
  6. Чем блок @empty отличается от состояния «список не загружен»?
  7. Когда лучше применять @switch, а когда @if/@else?
  8. Почему в шаблоне нельзя использовать стрелочную функцию как обработчик?
  9. Как добавить CSS-класс только при истинном условии?
  10. Для чего нужен оператор ?. и ?? в шаблоне?
  11. Как объявить обязательный вход у компонента?
  12. Что означает «стили компонента изолированы»?
Урок 3 • Раздел 34

34. Мини-словарь

ТерминОпределение
КомпонентЕдиница UI: класс + template + styles.
StandaloneКомпонент, который сам объявляет зависимости через imports.
Interpolation {{ }}Подстановка значения выражения в разметку.
Property binding [ ]Передача значения из класса в свойство DOM (данные вниз).
Event binding ( )Вызов метода при событии DOM/компонента (событие вверх).
Control flowСинтаксис @if, @for, @switch для управления разметкой.
trackСтабильный ключ элемента для эффективного обновления списка.
@emptyБлок, показываемый, когда коллекция пуста.
input()Функция объявления входного сигнала компонента.
output()Функция объявления события компонента.
СигналРеактивное значение, при изменении которого Angular перерисовывает читателей.
EncapsulationИзоляция стилей компонента.
Шаблонная ссылка #Имя элемента в шаблоне для обращения к нему в этом же шаблоне.
@deferОтложенная загрузка части шаблона по событию или видимости.
ViewEncapsulationМеханика изоляции стилей: Emulated / None / ShadowDom.
aria-pressedARIA-атрибут состояния нажатой кнопки-переключателя.
Урок 3 • Раздел 35

35. Источники и продолжение

Для самостоятельного углубления:

Следующий шаг. В уроке 4 мы научимся полностью связывать компоненты: передавать данные вниз через входы и поднимать события через выходы. Это завершит построение живого TaskFlow, где пользователь сможет добавлять и отмечать задачи.
Урок 3 • Раздел 36

36. Чек-лист урока

Готовность к уроку 4. Если вы можете выполнить все шесть заданий раздела 29 без подсказок, фундамент шаблонов сформирован. В уроке 4 мы свяжем эти компоненты в интерактивное приложение.

🛠 Практика к уроку 3

Закрепите материал: соберите экран списка задач из компонентов.

Задание 1. Компонент TaskItem

Создайте standalone-компонент, получающий задачу через вход:


@Component({
  selector: 'app-task-item',
  standalone: true,
  template: `<li [class.done]="task().completed">{{ task().title }}</li>`
})
export class TaskItem {
  task = input.required<Task>();
}

Задание 2. Список через @for и @empty


@for (task of tasks(); track task.id) {
  <app-task-item [task]="task" />
} @empty {
  <p>Задач пока нет.</p>
}
Полная версия. Подробные задания, песочницы и проверка — на отдельной странице: Практика: Урок 3 →
Куда дальше: тренируй шаблоны в практике к уроку 3, а потом — данные и события в Уроке 4.

📖 Глоссарий

ТерминЖизненная аналогияКоротко
@ComponentДеталь конструктора LEGOДекоратор, превращающий класс в готовую «запчасть»: данные + логика + разметка + стили в одном.
Шаблон (template)Экран, показывающий состояниеРазметка, связанная с данными: описывает, что видит пользователь при текущем состоянии.
Интерполяция {{ }}Вписывание имени в бланкПодставляет значение переменной из класса прямо в текст страницы.
@ifЗонтПоказывает блок разметки, только когда условие истинно (как зонт — только в дождь).
@forПочтовый конвейерПовторяет шаблон строки для каждого элемента списка (как конверты для каждого письма).
@switchПереключатель светаВыбирает одну ветку из нескольких по значению (как одна лампа из many).
Привязка классов [class.имя]Табличка на двериДобавляет или убирает CSS-класс по условию — меняет «табличку» на двери компонента.

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

1. Интерполяция {{ title }} подставляет в шаблон текущее значение переменной title из класса компонента. Верно или неверно?

2. Блок @if показывает свою разметку только тогда, когда условие в скобках истинно. Верно или неверно?

3. В цикле @for обязательно указывать track — стабильный ключ (обычно id), чтобы Angular не перепутал задачи при обновлении. Верно или неверно?

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