1. Пример компонента
Итак, вы освоили азы. Ваш код уже может условно отображать кнопки, списки задач динамически заполняются данными, а элементы меняют цвет, когда вы на них наводите мышкой. Все выглядит круто! Но потом вы начинаете замечать странности, или просто хотите сделать что-то чуть более сложное, и тут же возникает: "А как это сделать?" или "Почему это не работает?".
Это абсолютно нормально! Директивы, хоть и кажутся простыми, имеют свои особенности, "подводные камни" и, конечно же, лучшие практики. В этой лекции мы с вами разберем самые частые вопросы и заблуждения, связанные с использованием встроенных директив Angular. Мы будем строить наше приложение "Менеджер задач", которое позволит нам наглядно увидеть все эти нюансы.
Давайте создадим основу для нашего приложения:
Создадим новый компонент TaskListComponent:
ng generate component TaskList
И пропишем его в app.component.ts:
// src/app/app.component.ts
import { Component } from '@angular/core';
import { TaskListComponent } from './task-list/task-list.component'; // Импортируем компонент
@Component({
selector: 'app-root',
standalone: true, // Мы используем Standalone Components!
imports: [TaskListComponent], // Добавляем в imports
template: `
<header class="app-header">
<h1>Мой Менеджер Задач</h1>
</header>
<main>
<app-task-list></app-task-list> <!-- Используем наш компонент -->
</main>
<footer class="app-footer">
<p>© 2024 Мои Задачи. Все права защищены.</p>
</footer>
`,
styles: `
.app-header {
background-color: #2c3e50;
color: white;
padding: 1rem;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.app-header h1 {
margin: 0;
}
main {
padding: 2rem;
max-width: 800px;
margin: 20px auto;
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.app-footer {
text-align: center;
padding: 1rem;
margin-top: 2rem;
color: #777;
border-top: 1px solid #eee;
}
`
})
export class AppComponent {
title = 'angular-task-manager';
}
Теперь в task-list.component.ts и task-list.component.html мы будем добавлять наши примеры.
// src/app/task-list/task-list.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; // Обязательно для *ngIf, *ngFor и т.д.
interface Task {
id: number;
title: string;
completed: boolean;
}
@Component({
selector: 'app-task-list',
standalone: true,
imports: [CommonModule], // CommonModule предоставляет доступ к ngIf, ngFor, ngClass и ngStyle
templateUrl: './task-list.component.html',
styleUrl: './task-list.component.css'
})
export class TaskListComponent {
tasks: Task[] = [
{ id: 1, title: 'Изучить директивы Angular', completed: false },
{ id: 2, title: 'Сделать домашнее задание', completed: true },
{ id: 3, title: 'Купить продукты', completed: false },
{ id: 4, title: 'Позвонить другу', completed: false }
];
showCompletedTasks: boolean = true;
highlightUrgent: boolean = true;
toggleCompletedTasks(): void {
this.showCompletedTasks = !this.showCompletedTasks;
}
toggleUrgentHighlight(): void {
this.highlightUrgent = !this.highlightUrgent;
}
}
<!-- src/app/task-list/task-list.component.html -->
<section class="task-manager">
<h2>Список Задач</h2>
<div class="controls">
<button (click)="toggleCompletedTasks()">
{{ showCompletedTasks ? 'Скрыть выполненные' : 'Показать выполненные' }}
</button>
<button (click)="toggleUrgentHighlight()">
{{ highlightUrgent ? 'Убрать срочность' : 'Выделить срочные' }}
</button>
</div>
<ul class="task-list">
<!-- Здесь будут наши примеры -->
</ul>
</section>
И немного CSS для красоты:
/* src/app/task-list/task-list.component.css */
.task-manager {
background-color: #f9f9f9;
padding: 1.5rem;
border-radius: 8px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
}
.task-manager h2 {
color: #34495e;
text-align: center;
margin-bottom: 1.5rem;
}
.controls {
display: flex;
justify-content: center;
gap: 1rem;
margin-bottom: 2rem;
}
.controls button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 5px;
background-color: #3498db;
color: white;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
.controls button:hover {
background-color: #2980b9;
}
.task-list {
list-style: none;
padding: 0;
margin: 0;
}
.task-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
margin-bottom: 0.75rem;
background-color: white;
border-left: 5px solid #bdc3c7;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.task-item.completed {
border-left-color: #2ecc71;
opacity: 0.7;
}
.task-item.urgent {
border-left-color: #e74c3c;
box-shadow: 0 0 15px rgba(231, 76, 60, 0.3);
transform: scale(1.02);
}
.task-title {
font-weight: 500;
color: #2c3e50;
}
.task-item.completed .task-title {
text-decoration: line-through;
color: #95a5a6;
}
.task-status {
padding: 0.3rem 0.6rem;
border-radius: 4px;
font-size: 0.8em;
font-weight: bold;
}
.task-status.pending {
background-color: #f39c12;
color: white;
}
.task-status.done {
background-color: #2ecc71;
color: white;
}
Теперь у нас есть базовая структура, в которую мы будем вставлять примеры.
2. Частые вопросы по *ngIf
*ngIf – это ваш швейцарский нож для условного отображения контента. Но у него есть свои секреты.
Q1: Как показать/скрыть элемент, не удаляя его из DOM? *ngIf vs [hidden]
Это, пожалуй, самый первый вопрос, который возникает у новичков. Мы знаем, что *ngIf "вырезает" элемент из DOM, если условие ложно. А что, если я хочу, чтобы элемент оставался в DOM, но просто был невидимым? Например, если элемент сложный, и его постоянное создание/удаление дорогое для производительности, или если мне нужно, чтобы его состояние сохранялось?
На помощь приходит атрибут [hidden]. Это обычный HTML-атрибут hidden, который Angular динамически добавляет или удаляет, основываясь на вашем условии. Когда hidden присутствует, браузер автоматически применяет display: none; к элементу, делая его невидимым, но он остается частью DOM-дерева.
| Критерий | *ngIf="условие" | [hidden]="условие" |
|---|---|---|
| В DOM-дереве | Нет, если условие false | Да, всегда (просто скрыт) |
| Производительность | Высокие затраты на создание/удаление при частых изменениях | Низкие затраты, только изменение стиля |
| Сохранение состояния | Нет (при удалении элемент и его состояние теряются) | Да (состояние сохраняется) |
| Использование | Для условной отрисовки, когда элемент не нужен, если условие не выполнено. | Для временного скрытия, когда элемент нужен для JS-логики или должен быть всегда готов к показу. |
Пример:
Представим, что у нас есть задача, и мы хотим показать дополнительную информацию о ней, когда она выбрана, но не удалять ее из DOM.
<li *ngFor="let task of tasks; trackBy: trackByTaskId"
[ngClass]="{'completed': task.completed, 'urgent': !task.completed && highlightUrgent}"
class="task-item">
<span class="task-title">{{ task.title }}</span>
<span [hidden]="!task.completed" class="task-status done">Выполнена</span>
<span [hidden]="task.completed" class="task-status pending">В процессе</span>
</li>
В этом примере мы используем [hidden] для статуса задачи. Почему? Потому что статус всегда есть, просто мы показываем либо "Выполнена", либо "В процессе". Элементы <span> всегда в DOM, и это не вызывает лишних перерисовок. Если бы мы использовали *ngIf, Angular постоянно бы удалял и создавал эти span элементы, что для такой простой задачи избыточно.
Q2: Как использовать *ngIf с else?
Часто нам нужно не просто показать или скрыть что-то, а показать одно, если условие истинно, и что-то другое, если ложно. Для этого *ngIf имеет синтаксис else. Но есть нюанс: else требует использования ng-template.
Что такое ng-template? Это очень специальный элемент Angular. Он никогда сам по себе не отображается в DOM. Это просто "шаблон", который Angular может использовать для отрисовки другого контента. Думайте о нем как о заготовке или чертеже, который Angular использует, чтобы построить что-то реальное в DOM, когда придет время.
Синтаксис:
<div *ngIf="условие; else elseBlock">
// Это отображается, если условие истинно
</div>
<ng-template #elseBlock>
// Это отображается, если условие ложно
</ng-template>
Пример:
Добавим в наш "Менеджер задач" сообщение, если задач вообще нет.
<ul class="task-list">
<li *ngFor="let task of tasks; trackBy: trackByTaskId"
[ngClass]="{'completed': task.completed, 'urgent': !task.completed && highlightUrgent}"
class="task-item">
<span class="task-title">{{ task.title }}</span>
<span [hidden]="!task.completed" class="task-status done">Выполнена</span>
<span [hidden]="task.completed" class="task-status pending">В процессе</span>
</li>
</ul>
<div *ngIf="tasks.length === 0; else tasksExist">
<p class="no-tasks-message">Пока нет задач. Время добавить что-то новое!</p>
</div>
<ng-template #tasksExist>
<!-- Здесь будет отображаться, если задачи есть (т.е. наш список задач) -->
</ng-template>
Мы также можем использовать else в цепочке else if, чтобы реализовать более сложные сценарии.
<div *ngIf="status === 'loading'; else elseBlock">
Загружаем данные...
</div>
<ng-template #elseBlock>
<div *ngIf="status === 'error'; else contentBlock">
Ошибка загрузки данных!
</div>
</ng-template>
<ng-template #contentBlock>
<!-- Основной контент -->
Данные успешно загружены.
</ng-template>
Это очень мощный механизм, который позволяет создавать сложные, но при этом понятные логические ветвления в ваших шаблонах.
Q3: Почему *ngIf иногда ведет себя "странно" с асинхронными данными?
Представьте, что вы загружаете список задач с сервера. Данные приходят не мгновенно. Если вы попытаетесь обратиться к свойству этих данных до того, как они загрузились, Angular попытается отрисовать элемент, но tasks будет undefined или null. Это приведет к ошибке, потому что вы не можете получить tasks.length от undefined.
<!-- ОШИБКА: Если tasks еще null или undefined -->
<div *ngIf="tasks">
<p>Количество задач: {{ tasks.length }}</p>
</div>
Если tasks изначально null, *ngIf="tasks" будет false, и блок не отобразится. Но если вы попытаетесь получить tasks.length где-то еще, вне *ngIf (или если tasks станет null после инициализации), то получите ошибку!
Решения:
- Проверка на null / undefined: Самый прямой способ – явно проверять, существуют ли данные.
<div *ngIf="tasks !== null && tasks !== undefined"> <p>Количество задач: {{ tasks.length }}</p> </div> - Оператор безопасной навигации (?.): Это более лаконичный способ. Он позволяет безопасно обращаться к свойствам объектов, которые могут быть null или undefined. Если tasks будет null или undefined, всё выражение вернёт undefined, и ошибки не будет.
<div *ngIf="tasks"> <p>Количество задач: {{ tasks?.length }}</p> <!-- Обратите внимание на ?. --> </div> - Комбинация *ngIf и as (для async pipe): Если вы работаете с Observables, есть специальный async пайп, который автоматически управляет подпиской и отпиской, а также позволяет сохранить результат в локальную переменную. Но это тема будущих лекций.
3. Частые вопросы по *ngFor
*ngFor – это ваш верный спутник в отрисовке списков. Но он тоже имеет свои особенности, особенно в плане производительности.
Q1: Что такое trackBy и почему это важно?
Представьте, что у вас есть список из 1000 задач. Если вы просто удалите одну задачу из середины списка, Angular, без trackBy, не знает, какая именно задача была удалена. Он может решить, что весь список изменился, и перерисовать все 1000 элементов DOM! Это очень дорого с точки зрения производительности.
trackBy – это функция, которую вы предоставляете *ngFor. Эта функция должна возвращать уникальный идентификатор для каждого элемента в списке (например, id задачи). Когда список tasks изменяется (добавляются, удаляются, переупорядочиваются элементы), Angular использует trackBy для отслеживания каждого конкретного элемента. Если задача с определенным ID переместилась, Angular просто переместит соответствующий элемент DOM. Если задача удалена, он удалит только один элемент DOM. Это значительно повышает производительность, особенно для больших или часто меняющихся списков.
Пример:
// src/app/task-list/task-list.component.ts
// ...
export class TaskListComponent {
tasks: Task[] = [
{ id: 1, title: 'Изучить директивы Angular', completed: false },
{ id: 2, title: 'Сделать домашнее задание', completed: true },
{ id: 3, title: 'Купить продукты', completed: false },
{ id: 4, title: 'Позвонить другу', completed: false }
];
// ... (остальной код)
trackByTaskId(index: number, task: Task): number {
return task.id; // Возвращаем уникальный ID задачи
}
}
А в HTML мы уже используем trackBy: trackByTaskId:
<ul class="task-list">
<li *ngFor="let task of tasks; trackBy: trackByTaskId"
[ngClass]="{'completed': task.completed, 'urgent': !task.completed && highlightUrgent}"
class="task-item">
<!-- ... -->
</li>
</ul>
Всегда используйте trackBy для списков, особенно если они могут изменяться. Это "простой трюк" для "бесплатного" ускорения вашего приложения.
Q2: Как получить индекс элемента в *ngFor?
Часто нам нужно знать порядковый номер элемента в списке. *ngFor предоставляет специальную переменную index, которую можно получить так:
<ul class="task-list">
<li *ngFor="let task of tasks; let i = index; trackBy: trackByTaskId"
[ngClass]="{'completed': task.completed, 'urgent': !task.completed && highlightUrgent}"
class="task-item">
<span class="task-number">{{ i + 1 }}. </span> <!-- Добавляем номер задачи -->
<span class="task-title">{{ task.title }}</span>
<span [hidden]="!task.completed" class="task-status done">Выполнена</span>
<span [hidden]="task.completed" class="task-status pending">В процессе</span>
</li>
</ul>
Теперь перед каждой задачей будет её номер по порядку (начиная с 1, так как index начинается с 0).
Q3: Как получить другие переменные *ngFor (first, last, even, odd)?
*ngFor предоставляет не только index, но и другие полезные локальные переменные, которые дают информацию о положении текущего элемента в итерации:
- let first = first: true, если это первый элемент в списке.
- let last = last: true, если это последний элемент в списке.
- let even = even: true, если индекс элемента четный.
- let odd = odd: true, если индекс элемента нечетный.
Пример: Давайте выделим первый и последний элементы списка, а также сделаем фон для четных строк.
<ul class="task-list">
<li *ngFor="let task of tasks; let i = index; let f = first; let l = last; let e = even; trackBy: trackByTaskId"
[ngClass]="{
'completed': task.completed,
'urgent': !task.completed && highlightUrgent,
'first-item': f, /* Применит класс 'first-item' к первому элементу */
'last-item': l, /* Применит класс 'last-item' к последнему элементу */
'even-item': e /* Применит класс 'even-item' к четным элементам */
}"
class="task-item">
<span class="task-number">{{ i + 1 }}. </span>
<span class="task-title">{{ task.title }}</span>
<span [hidden]="!task.completed" class="task-status done">Выполнена</span>
<span [hidden]="task.completed" class="task-status pending">В процессе</span>
</li>
</ul>
Добавим стили:
.task-item.first-item {
border-top-right-radius: 15px;
border-top-left-radius: 15px;
}
.task-item.last-item {
border-bottom-right-radius: 15px;
border-bottom-left-radius: 15px;
margin-bottom: 0; /* Убираем отступ для последнего элемента */
}
.task-item.even-item {
background-color: #f0f4f7; /* Немного другой фон для четных строк */
}
Q4: Почему *ngFor не всегда обновляется, когда изменяется массив?
Это одна из самых частых ошибок новичков, и она связана с тем, как JavaScript и Angular отслеживают изменения объектов и массивов.
// НЕПРАВИЛЬНО: *ngFor может не обновиться
this.tasks[0].title = 'Новое название задачи'; // Изменяем элемент по индексу
this.tasks.pop(); // Удаляем элемент
Angular отслеживает изменения ссылки на массив, а не его внутреннее содержимое. Если вы изменяете элемент внутри существующего массива или удаляете/добавляете элементы методами, которые мутируют (изменяют) сам массив (такими как push(), pop(), splice() и прямое присвоение по индексу), ссылка на массив this.tasks не меняется. Angular может не заметить эти изменения и не перерисовать *ngFor должным образом.
Правильный подход: Иммутабельность.
Чтобы *ngFor всегда корректно реагировал на изменения, нужно всегда создавать новую ссылку на массив, когда вы его изменяете. Это называется принципом иммутабельности.
Пример: Добавление новой задачи в наш менеджер.
В task-list.component.ts добавим метод для добавления задачи:
// src/app/task-list/task-list.component.ts
// ...
export class TaskListComponent {
// ... (существующие свойства и методы)
nextTaskId: number = 5; // Для генерации уникальных ID
addTask(title: string): void {
const newTask: Task = {
id: this.nextTaskId++,
title: title,
completed: false
};
// Правильно: создаем новый массив, добавляя в него старые элементы и новую задачу
this.tasks = [...this.tasks, newTask];
// Или, если нужно добавить в начало:
// this.tasks = [newTask, ...this.tasks];
// Неправильно (мутирует исходный массив, Angular может не заметить):
// this.tasks.push(newTask);
}
// Метод для удаления задачи
deleteTask(id: number): void {
// Правильно: создаем новый массив, отфильтровывая удаляемую задачу
this.tasks = this.tasks.filter(task => task.id !== id);
}
}
И добавим кнопки для этих действий в HTML:
<section class="task-manager">
<h2>Список Задач</h2>
<div class="controls">
<button (click)="toggleCompletedTasks()">
{{ showCompletedTasks ? 'Скрыть выполненные' : 'Показать выполненные' }}
</button>
<button (click)="toggleUrgentHighlight()">
{{ highlightUrgent ? 'Убрать срочность' : 'Выделить срочные' }}
</button>
</div>
<div class="add-task">
<input #taskInput type="text" placeholder="Новая задача..." />
<button (click)="addTask(taskInput.value); taskInput.value=''">Добавить задачу</button>
</div>
<ul class="task-list">
<li *ngFor="let task of tasks; let i = index; let f = first; let l = last; let e = even; trackBy: trackByTaskId"
[ngClass]="{
'completed': task.completed,
'urgent': !task.completed && highlightUrgent,
'first-item': f,
'last-item': l,
'even-item': e
}"
class="task-item">
<span class="task-number">{{ i + 1 }}. </span>
<span class="task-title">{{ task.title }}</span>
<span [hidden]="!task.completed" class="task-status done">Выполнена</span>
<span [hidden]="task.completed" class="task-status pending">В процессе</span>
<button (click)="deleteTask(task.id)" class="delete-button">X</button>
</li>
</ul>
<div *ngIf="tasks.length === 0">
<p class="no-tasks-message">Пока нет задач. Время добавить что-то новое!</p>
</div>
</section>
И стили для новой кнопки и инпута:
.add-task {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
justify-content: center;
}
.add-task input {
flex-grow: 1;
padding: 0.75rem;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 1rem;
max-width: 300px;
}
.add-task button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 5px;
background-color: #27ae60;
color: white;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
.add-task button:hover {
background-color: #229954;
}
.delete-button {
background-color: #e74c3c;
color: white;
border: none;
border-radius: 50%;
width: 28px;
height: 28px;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s ease;
margin-left: 1rem;
}
.delete-button:hover {
background-color: #c0392b;
}
.no-tasks-message {
text-align: center;
color: #7f8c8d;
font-style: italic;
padding: 1rem;
border: 1px dashed #ccc;
border-radius: 5px;
margin-top: 1.5rem;
}
4. Частые вопросы по ngClass и ngStyle
Эти директивы помогают нам динамически применять стили. Но и здесь есть свои нюансы.
Q1: Какая разница между [class.className]="condition" и ngClass?
Обе конструкции позволяют динамически добавлять или удалять CSS-классы.
[class.className]="condition": Это синтаксис для одного конкретного класса. Он очень читаем и прост, если вам нужно добавить или удалить только один класс на основе условия.
<button [class.active]="isActive">Кнопка</button>
Здесь класс active будет добавлен, если isActive – true, и удален, если false.
[ngClass]: Эта директива предназначена для более сложных сценариев, когда вам нужно управлять несколькими классами, или когда названия классов формируются динамически. Она принимает три разных типа значений:
- Строка: список классов, разделенных пробелами (например, 'class1 class2').
- Массив строк: ['class1', 'class2', ...]
- Объект: ключ – это название класса, значение – это булево условие ({'class1': true, 'class2': false}).
Пример:
В нашем приложении ngClass используется для применения классов completed и urgent.
<li [ngClass]="{'completed': task.completed, 'urgent': !task.completed && highlightUrgent}"
class="task-item">
<!-- ... -->
</li>
Здесь мы используем объектный синтаксис, который очень удобен, когда нужно применить несколько классов по разным условиям. Если бы нам нужно было только completed, мы могли бы написать:
<li [class.completed]="task.completed" class="task-item">
Какой выбрать? Используйте [class.className] для одного класса, [ngClass] с объектом – для нескольких классов, управляемых условиями. Если классы приходят откуда-то динамически (например, из массива строк), тогда [ngClass] с массивом будет лучшим выбором.
Q2: Какая разница между [style.property]="value" и ngStyle?
Аналогично классам, обе конструкции управляют стилями.
[style.property]="value": Для установки одного конкретного CSS-свойства.
<div [style.color]="'red'">Красный текст</div>
Или с юнитами:
<div [style.font-size.px]="20">20px текст</div>
[ngStyle]: Для множества стилей, или когда стили формируются динамически. Принимает объект, где ключ – это название CSS-свойства (в camelCase, например, backgroundColor), а значение – его значение.
Пример:
Допустим, мы хотим, чтобы срочные задачи имели динамически заданную толщину границы.
<li *ngFor="let task of tasks; let i = index; let f = first; let l = last; let e = even; trackBy: trackByTaskId"
[ngClass]="{
'completed': task.completed,
'urgent': !task.completed && highlightUrgent,
'first-item': f,
'last-item': l,
'even-item': e
}"
[ngStyle]="{ 'border-width': !task.completed && highlightUrgent ? '7px' : '5px' }"
class="task-item">
<!-- ... -->
</li>
Здесь мы задаем толщину левой границы (которая у нас есть в CSS как border-left) в 7px для срочных задач и 5px для обычных. Это демонстрирует, как ngStyle позволяет применять стили на основе логики.
В целом, [style.property] для одного стиля, [ngStyle] – для коллекции стилей.
5. Общие вопросы и Best Practices
Q1: Можно ли использовать несколько структурных директив на одном элементе?
Нет, вы не можете применить две или более структурных директивы (*ngIf, *ngFor, *ngSwitchCase) к одному и тому же элементу. Почему? Потому что каждая структурная директива работает с шаблоном элемента, который она преобразует. Если вы поставите две, Angular не поймет, какой шаблон к какому применять. Это как пытаться использовать два разных молотка одновременно для одной и той же задачи – беспорядок.
Что делать? Используйте ng-container!
ng-container – это еще один магический элемент Angular. Как и ng-template, он не добавляет никаких элементов в DOM. Это просто "логический контейнер", который позволяет вам применять структурные директивы, не создавая лишних div или span в вашем HTML.
Пример:
Допустим, мы хотим отображать только невыполненные задачи, и только если они есть, и выделить их жирным.
<!-- НЕПРАВИЛЬНО: Так нельзя! -->
<!-- <li *ngFor="let task of tasks" *ngIf="!task.completed">...</li> -->
<!-- ПРАВИЛЬНО: Используем ng-container -->
<ng-container *ngFor="let task of tasks; trackBy: trackByTaskId">
<li *ngIf="!task.completed"
[ngClass]="{
'completed': task.completed,
'urgent': !task.completed && highlightUrgent,
'even-item': e
}"
[ngStyle]="{ 'border-width': !task.completed && highlightUrgent ? '7px' : '5px' }"
class="task-item">
<span class="task-title">{{ task.title }}</span>
<span [hidden]="!task.completed" class="task-status done">Выполнена</span>
<span [hidden]="task.completed" class="task-status pending">В процессе</span>
<button (click)="deleteTask(task.id)" class="delete-button">X</button>
</li>
</ng-container>
В этом случае *ngFor создает "экземпляр" ng-container для каждой задачи, а затем *ngIf внутри этой ng-container решает, отображать ли li элемент. Это чисто, эффективно и не засоряет DOM.
Q2: Как избежать "прыжков" элементов при использовании *ngIf?
Когда *ngIf удаляет элемент из DOM, весь окружающий контент может "прыгнуть", чтобы заполнить пустое пространство. Это может выглядеть не очень приятно, особенно если элементы большие.
Если вам нужно, чтобы элемент занимал место, даже если он скрыт, чтобы избежать таких "прыжков", используйте [hidden] вместо *ngIf. Он оставляет элемент в DOM, просто делает его невидимым с помощью display: none;.
Если вам нужен более плавный эффект, можно использовать CSS-переходы и анимации в сочетании с *ngIf (Angular даже имеет свои анимационные API, которые вы изучите позже), но для простых случаев [hidden] часто достаточно.
Пример:
Если бы мы хотели, чтобы место для сообщения "Нет задач" всегда было зарезервировано, даже когда оно не отображается (чтобы список не "прыгал" вверх, если вдруг все задачи удалили):
<div [hidden]="tasks.length > 0" class="no-tasks-message">
<p>Пока нет задач. Время добавить что-то новое!</p>
</div>
<ul class="task-list" [hidden]="tasks.length === 0">
<!-- ... задачи ... -->
</ul>
В этом случае, вместо *ngIf="tasks.length === 0; else tasksExist", мы используем [hidden], чтобы оба блока (no-tasks-message и task-list) всегда присутствовали в DOM, просто один из них скрыт, что делает переключение между ними более плавным.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ