document.querySelector и менять элементы (как в уроке 1, раздел 41), мы описываем, что должно быть при текущем состоянии, а Angular сам применяет изменения к DOM. Шаблон — это декларативное описание интерфейса.Студенческий материал. Его можно использовать как самостоятельный конспект, раздаточный материал и справочник после занятия.
Angular строит приложение из компонентов. Компонент — это связка: данные + логика + разметка + стили.
В уроке 1 (разделы 20–21) мы впервые увидели Angular-компонент, создали первый список задач через CLI и подключили его к корню. В уроке 2 мы разобрали JavaScript и TypeScript, на которых строится класс компонента. Теперь соединим всё вместе и посмотрим на компонент детально.
Компонент объединяет три части:
Экран TaskFlow собирается из нескольких компонентов:
App (корень)
└── TaskList
├── TaskItem (повторяется для каждой задачи)
├── FilterBar
├── StatsBar
└── TaskForm
В этом уроке мы построим эти компоненты и свяжем их данными через шаблон. Именно шаблон — наша главная тема.
Современный Angular (начиная с 17 версии и в нашем проекте 22) использует standalone API. Компонент не обязан жить внутри NgModule — он сам объявляет, что ему нужно для работы, через поле imports.
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 | Стили прямо в коде или в отдельных файлах. |
Декоратор @Component сообщает Angular, как обрабатывать класс: считать его компонентом, какой у него шаблон и как его стилизовать.
Selector определяет способ встраивания. Чаще всего это имя тега с префиксом app:
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-.... Селекторы-атрибуты и классы чаще нужны для директив, о которых речь пойдёт в других уроках.Небольшую разметку удобно держать строкой (шаблонная строка в обратных кавычках), большую — в отдельном файле:
// маленький компонент — всё рядом
@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 { }
ng serve --open (ниже). Песочницы дают быстрый аналог на чистом JS/HTML, чтобы проверить идею за секунды, не поднимая проект.Перед кодом убедимся, что проект на месте и сервер разработки запускается. Проект мы создали в уроке 1 (раздел 36).
# Запуск сервера разработки (откроет http://localhost:4200)
cd task-flow-angular
ng serve --open
Если проект уже создан, повторно его создавать не нужно. Проверка — команда в отдельном терминале:
ng version
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. Пока он не создан, на время разработки можно закомментировать строку импорта или создавать компонент заранее.
Интерполяция {{ }} подставляет значение выражения из класса в текст разметки. Это первый и самый частый способ показать данные.
{{ 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>
Интерполяция используется повсеместно: заголовки, подписи, сообщения, счётчики. Например, вывести счётчик и время обновления:
<p>Всего задач: {{ tasks.length }}</p>
<p>Осталось: {{ tasks.filter(t => !t.completed).length }}</p>
<p>Обновлено: {{ updatedAt.toLocaleTimeString() }}</p>
Здесь внутри {{ }} вызван метод filter() прямо в выражении. Это допустимо, но для сложных расчётов лучше выносить логику в метод или computed-значение — шаблон останется читаемым, а производительность выше (раздел 28.9).
ng serve, но суть интерполяции та же, что у подстановки переменных. Нажмите «Запустить», чтобы увидеть результат на чистом JS/HTML:Шаблонное выражение — это выражение (вспомните разницу «выражение vs инструкция» из урока 2, раздел 2). Поэтому в нём есть ограничения.
| Можно | Нельзя |
|---|---|
Арифметика: {{ a + b }} | Объявление переменных через let |
Тернарный оператор: {{ ok ? 1 : 0 }} | Условные блоки if без фигурных скобок |
Вызов методов: {{ getTitle() }} | Циклы for |
Доступ к свойствам: {{ task.title }} | Операторы присваивания |
Оператор необязательности: task?.title | new, global, случайные глобалы |
| Строки и шаблонные строки | Объявление функций |
Поэтому сложную логику выносите в методы класса:
// в классе
getStatus() {
return this.completed ? 'готово' : 'в работе';
}
<!-- в шаблоне -->
<p>{{ getStatus() }}</p>
{{ 1 + 1 }} — увидите 2. А теперь попробуйте {{ let x = 1 }} или {{ if (ok) 1 }} — Angular выдаст ошибку, потому что в выражении нельзя объявлять переменные и писать инструкции. Так на практике почувствуете границу «можно / нельзя».Property binding [свойство]="выражение" передаёт значение из класса в свойство DOM-элемента или директивы. Говорят «данные вниз»: от TypeScript к представлению.
document.getElementById('x').textContent = ..., вы вручную меняете узел этого дерева. Это работает, но опасно: забыли обновить — на экране неверное значение; добавили узел, но не удалили старый — остаётся мусор. Angular берёт эту рутину на себя: вы описываете в шаблоне ЖЕЛАЕМЫЙ вид через {{ }}, [ ] и ( ), а фреймворк сам синхронизирует DOM с данными. Вы думаете о данных, а не о том, как их «засунуть» в страницу.
<input [value]="title" />
<button [disabled]="completed">Начать</button>
<img [src]="imageUrl" [alt]="title" />
Когда значение в классе изменяется, Angular обновляет свойство элемента. Сравните: value="title" (без скобок) — это статический атрибут. [value]="title" (в скобках) — динамическая привязка.
[ ]. Иногда нужно именно атрибут — об этом следующий раздел.Пример из TaskFlow — отключить кнопку, пока не введено название:
// в классе
title = '';
get canSave() {
return this.title.trim().length > 0;
}
<input [value]="title" />
<button [disabled]="!canSave">Сохранить</button>
[value] и [disabled] привязаны к свойствам DOM-элемента; в чистом JS то же делает чтение свойств и disabled. Введите текст — кнопка разблокируется:Иногда свойство и атрибут различаются. Чтобы связаться именно с атрибутом, используем [attr.имя].
<td [attr.colspan]="colspan">Объединённая ячейка</td>
<input [attr.aria-label]="label" />
Класс CSS добавляется по условию — это связь «внешний вид зависит от состояния» из урока 1 (раздел 10).
<li [class.done]="task.completed">
{{ task.title }}
</li>
[class.done] добавляет/убирает CSS-класс по условию; в чистом JS — classList.toggle. Нажмите кнопку — класс переключится:Класс 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>
Event binding (событие)="действие()" реагирует на событие DOM-элемента или компонента и вызывает метод класса. Это «событие вверх»: от пользователя к логике.
(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>
(click)="() => toggle()" не работает, потому что Angular не отслеживает такие обработчики. Всегда вызывайте метод класса напрямую — (click)="toggle()".(click) вызывает метод класса; в чистом JS то же делает addEventListener. Нажмите «Запустить» и кликайте кнопку:Внутри обработчика доступна специальная переменная $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>
Две пары скобок образуют ментальную модель связи класса и шаблона.
Интерполяция {{ }} — частный случай чтения данных (вниз). Event binding ( ) — это вверх. Никакой из них по отдельности не даёт «двусторонней» связи — для неё нужны оба вместе, о чём поговорим в разделе 21.
Современный 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>
}
*ngIf в проекте — используйте @if. Старые структурные директивы полезно знать для чтения legacy-кода, но новые проекты пишут на @if, @for, @switch.@if/@else выбирает блок по условию; в чистом JS — if/else. Нажмите кнопку, чтобы переключить наличие задач:@for перебирает коллекцию и выводит содержимое блока для каждого элемента. Это основной способ построить список.
@for — как конвейер на почте: письма (задачи) одно за другим проходят через аппарат, и для каждого печатается свой конверт (строка списка). Написали один шаблон строки — и он повторился для всех задач автоматически.Контекст. В TaskFlow @for строит строки списка из массива tasks. Ключ track task.id помогает Angular не перепутать задачи при обновлении.
@for (task of tasks; track task.id) {
<app-task-item [task]="task" />
}
@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 | Общее количество элементов в коллекции. |
@for перебирает массив и создаёт элементы; в чистом JS это map() + склейка строк. Нажмите «Запустить», чтобы увидеть список:@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/@else — когда условий несколько и они логические.Ещё пример — типы задач из будущих версий TaskFlow:
@switch (task.kind) {
@case ('task') { <span>Задача</span> }
@case ('note') { <span>Заметка</span> }
@case ('subtask') { <span>Подзадача</span> }
@default { <span>Другое</span> }
}
@switch/@case выбирает одну ветку по значению; в чистом JS — цепочка if/else. Нажимайте кнопку, чтобы менять приоритет: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>
}
}
Частая задача — показать список, отфильтрованный по статусу. В шаблоне 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) работает в классе, а шаблон остаётся простым и понятным.
Модель 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() не упадёт, если даты нет, а ?? подставит запасное значение.
Соберём первый осмысленный экран: список задач на основе модели 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>
}
Обратите внимание:
@if показывает либо список, либо сообщение;@for перебирает задачи;[task]="task" передаёт объект задачи в дочерний компонент;completedCount — вычисляемое значение через filter() из урока 2.Мы будем изучать React со второй половины курса. Полезно уже сейчас провести параллель, как это делалось в уроке 1 (раздел 24).
В Angular список строится через @for ... track. В React — через JavaScript-метод map() с атрибутом key:
// React (позже в курсе)
<ul>
{tasks.map(task => (
<TaskItem key={task.id} task={task} />
))}
</ul>
| Задача | Angular | React |
|---|---|---|
| Перебор списка | @for | .map() |
| Ключ элемента | track | key |
| Условие | @if | && или тернарник |
| Переключатель | @switch | switch или объект-маппинг |
| Пустой список | @empty | условие по length |
tasks и методы map/filter/find, вы сможете отобразить его любой библиотекой.Большой экран собирается из маленьких компонентов. Это и есть композиция — принцип, который мы разбирали для React в уроке 1 (раздел 23), но он в равной мере относится к Angular.
Компонент решает одну задачу. TaskItem показывает одну задачу, FilterBar — фильтры, TaskForm — создание. Это делает код читаемым и переиспользуемым.
Почему так важно делить экран на компоненты?
Композиция — общая идея и для Angular, и для React. В Angular связь между компонентами задаётся декораторами и шаблоном, в React — props и callbacks, но принцип «один компонент — одна ответственность» одинаков.
Чтобы дочерний компонент получил данные, родитель передаёт их через 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>();
}
Форма входа:
input.required<Task>() — обязательный вход;input<Task>() — необязательный вход (может не передаваться);input('default', { alias: 'x' }) — со значением по умолчанию и псевдонимом.В шаблоне читаем через вызов: 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" />
}
В уроке 1 (раздел 40) мы встретились с signal() и update(). Сигнал — реактивное значение: когда оно меняется, 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.По умолчанию стили компонента изолированы: классы внутри одного компонента не «протекают» в другие. Это называется 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. Базовое правило — стилизовать собственные элементы компонента, а «сквозные» состояния (например, зачёркивание выполненных задач) держать внутри своего же компонента.
Данные могут быть null или undefined — особенно когда они приходят от API. Обращение к свойству такого значения вызывает ошибку. В шаблоне защищаемся оператором необязательности ?.
@if (task) {
<p>{{ task.title }}</p>
}
<p>{{ task?.title }}</p>
<p>{{ task?.dueDate?.toLocaleDateString() ?? 'без срока' }}</p>
?. возвращает undefined вместо ошибки, если промежуточное значение пустое. ?? даёт запасное значение, когда слева null/undefined.
=== и проверка на 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>
}
Создадим компонент одной задачи. Он получает объект 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. Убедитесь, что выполненные задачи отображаются зачёркнутыми, а срочные — с пометкой «Срочно».
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.
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 по мере желания.
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()". Убедитесь, что счётчики корректно пересчитываются.
Полноценные формы 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). Родитель слушает его через event binding и реагирует. Передачу событий между компонентами детально рассмотрим в уроке 4; здесь достаточно увидеть, что родитель может «поймать» событие (created).Добавьте TaskForm в TaskList. Пока метод-обработчик в TaskList может просто выводить значение в консоль — связывание с массивом разберём в уроке 4.
Мы уже упомянули различие между HTML-атрибутом и свойством DOM. Разберём это подробнее, потому что от него зависят некоторые биндинги.
<input value="текст">.input.value.После загрузки страницы атрибут и свойство связаны, но при взаимодействии пользователя они могут разойтись. 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, нестандартные атрибуты.Шаблонная ссылка #имя даёт имя элементу в шаблоне, чтобы обращаться к нему в другом месте этого же шаблона.
<input #titleInput (input)="onChange(titleInput.value)" />
Здесь #titleInput — ссылка на элемент, а titleInput.value читает его значение без обращения к $event.
Шаблонные ссылки полезны, когда нужно:
$event.target;
<form>
<input #t (input)="onChange(t.value)" placeholder="Задача" />
<button (click)="add(t.value)">Добавить</button>
</form>
output(), которое мы изучим в уроке 4. На этом уроке достаточно понимать идею ссылки.Мы разобрали компоненты по отдельности. Теперь соединим их так, чтобы данные и события двигались в нужном направлении.
Задача: TaskForm собирает название, TaskList владеет массивом задач. Когда пользователь добавляет задачу, TaskForm сообщает об этом наверх, а TaskList добавляет запись.
Схема принципов единая для всего курса: данные вниз, события вверх. Событие (created) идёт вверх к владельцу данных; владелец меняет состояние; шаблон показывает новое состояние.
Соединим 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>
Что здесь важно:
Math.max(0, ...tasks().map(t => t.id)) + 1 — вычисляем следующий id через map и spread (урок 2, разделы 9 и 12);tasks.update(...) — обновляем сигнал, создавая новый массив с префиксом новой задачи;update()/set(), создавая новое значение. Если мутировать внутренний массив, Angular может не заметить изменение. Это следствие принципа «неизменяемого состояния» из урока 2.Обновите TaskList из раздела 25: подключите TaskForm через событие (created), добавьте метод onCreated и проверьте, что новая задача появляется в списке.
Данные задач часто бывают вложенными: у задачи есть свойства, внутри массивы, внутри объектов. Разберём вывод таких структур.
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 уже исключила пустое значение. Без ! компилятор может ругаться. Используйте его только после проверки.Если вложенность становится глубокой, лучше выносить фрагмент в отдельный компонент. Вложенный вывод читается тяжело — композиция решает эту проблему.
Мы разобрали [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>
В уроке 1 (раздел 28) мы поговорили о доступности. В шаблонах Angular это выражается через правильные атрибуты и связывание с состоянием.
<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" заставляет скринридер озвучивать изменения, даже если пользователь не сфокусирован на элементе.
aria-label; 2) отмечайте выбранный фильтр через aria-pressed; 3) добавляйте текст подсказки для состояний нагрузки и пустого списка. Это небольшие строки, а доступность растёт заметно.Соберём весь экран из практик 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.
Соберите мастер-шаблон из этого раздела: отдельные компоненты TaskItem, TaskForm, FilterBar, StatsBar внутри TaskList. Проверьте: добавление задачи, фильтрацию по статусам и корректную статистику.
Шаблон выполняется Angular многократно. Правильное написание выражений влияет на скорость. Разберём три важные практики.
Без 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>
}
Сведём все конструкции урока в одну справочную таблицу. Она пригодится как конспект.
| Задача | Синтаксис | Пример |
|---|---|---|
| Показать значение | {{ }} | {{ 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) |
Сделаем чекбокс рабочим: отметка меняет состояние задачи. Так мы свяжем событие и обновление сигнала — сердце интерактивности.
// task-list.ts (добавляем метод)
toggle(taskId: number) {
this.tasks.update(current =>
current.map(t =>
t.id === taskId ? { ...t, completed: !t.completed } : t
)
);
}
Метод использует три важные конструкции из урока 2:
update() — замена значения сигнала новым массивом;map() — преобразование элементов (раздел 9);{ ...t, completed: !t.completed } — новая копия объекта через spread (раздел 12), чтобы не мутировать исходный.Теперь 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>
}
Полный цикл:
toggle() внутри TaskItem;toggled;toggle(task.id);Добавьте метод toggle в TaskList и событие toggled в TaskItem. Проверьте, что отметка выполнения обновляет и счётчики StatsBar.
Шесть заданий. Сначала попробуйте сами, затем сверьтесь с подсказками в разделе 30.
В TaskItem выведите «Выполнено» или «В работе» через @if в зависимости от task().completed.
В TaskList добавьте метод activeCount(), который считает невыполненные задачи через filter(), и выведите его.
Сделайте массив задач пустым и убедитесь, что через @empty или @else показывается сообщение «Пока нет задач».
Добавьте рядом с задачей, у которой priority === 'high', пометку «Срочно» через @if и класс [class.urgent].
Добавьте кнопку, которая переключает показ выполненных задач. Используйте сигнал showDone и метод для его переключения.
Через переменную $index выведите номер перед каждой задачей: «1. Изучить компоненты».
Совет: сначала сравните свою идею с требованием, а потом уже открывайте код.
@if (task().completed) { <span>Выполнено</span> } @else { <span>В работе</span> }activeCount() { return this.tasks().filter(t => !t.completed).length; }@for (... ) { ... } @empty { ... } — блок @empty сработает при пустом массиве.@if (task().priority === 'high') { <span class="urgent">Срочно</span> }. Для подсветки самого блока — [class.urgent]="task().priority === 'high'".showDone = signal(false) и метод toggleShowDone() { this.showDone.update(v => !v); }. В шаблоне кнопка (click)="toggleShowDone()", а фильтрация через метод, который учитывает showDone().@for (task of tasks(); track task.id; let i = $index) вывести {{ i + 1 }}. {{ task.title }}.Понимание текста ошибки ускоряет работу. Разберём типичные ситуации.
| Симптом | Причина | Решение |
|---|---|---|
| «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» | Попытались написать стрелочную функцию в биндинге. | Вынесите логику в метод класса. |
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 подсказывает возможные причины — читайте их, а не переписывайте всё «вслепую».
За этот урок мы прошли путь от одного компонента до маленького интерфейса TaskFlow:
Этот урок — фундамент шаблонов Angular. С ним вы сможете читать и писать компоненты, связывать данные через биндинги, строить условную разметку и списки, а также собирать экран из нескольких компонентов.
{{ }}, [ ]) и реагировать на события (( ));@if, @for, @switch);input() и события output().selector в @Component?{{ }} отличается от property binding [ ]?( )?@for нужен track?@empty отличается от состояния «список не загружен»?@switch, а когда @if/@else??. и ?? в шаблоне?| Термин | Определение |
|---|---|
| Компонент | Единица 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-pressed | ARIA-атрибут состояния нажатой кнопки-переключателя. |
Для самостоятельного углубления:
@if/@else и @switch.@for с track и @empty.[class.*].[ ] и ( ).input() и читать сигнал в шаблоне.?. и ??.Закрепите материал: соберите экран списка задач из компонентов.
Создайте standalone-компонент, получающий задачу через вход:
@Component({
selector: 'app-task-item',
standalone: true,
template: `<li [class.done]="task().completed">{{ task().title }}</li>`
})
export class TaskItem {
task = input.required<Task>();
}
@for (task of tasks(); track task.id) {
<app-task-item [task]="task" />
} @empty {
<p>Задач пока нет.</p>
}
| Термин | Жизненная аналогия | Коротко |
|---|---|---|
@Component | Деталь конструктора LEGO | Декоратор, превращающий класс в готовую «запчасть»: данные + логика + разметка + стили в одном. |
| Шаблон (template) | Экран, показывающий состояние | Разметка, связанная с данными: описывает, что видит пользователь при текущем состоянии. |
Интерполяция {{ }} | Вписывание имени в бланк | Подставляет значение переменной из класса прямо в текст страницы. |
@if | Зонт | Показывает блок разметки, только когда условие истинно (как зонт — только в дождь). |
@for | Почтовый конвейер | Повторяет шаблон строки для каждого элемента списка (как конверты для каждого письма). |
@switch | Переключатель света | Выбирает одну ветку из нескольких по значению (как одна лампа из many). |
Привязка классов [class.имя] | Табличка на двери | Добавляет или убирает CSS-класс по условию — меняет «табличку» на двери компонента. |
1. Интерполяция {{ title }} подставляет в шаблон текущее значение переменной title из класса компонента. Верно или неверно?
Ответ: Верно. Как вписывание имени в бланк: вместо скобок {{ имя }} подставляется реальное значение переменной из класса.
2. Блок @if показывает свою разметку только тогда, когда условие в скобках истинно. Верно или неверно?
Ответ: Верно. Как зонт: вы достаёте его и видите блок «Зонт открыт» только в дождь (когда условие true); в солнечную погоду блок не рисуется.
3. В цикле @for обязательно указывать track — стабильный ключ (обычно id), чтобы Angular не перепутал задачи при обновлении. Верно или неверно?
Ответ: Верно. Как почтовый конвейер: каждому письму нужен свой адрес, чтобы не перепутать получателей. Без track Angular выдаст предупреждение о производительности.