Перейти к содержимому

8. Встроенные директивы

Директивы — это суперспособности HTML 🦸. Они говорят Angular что делать с элементами DOM: прятать, повторять, менять стили. Думай о них как о специальных атрибутах с магией внутри.


Компонент — это директива со своим шаблоном. Все компоненты — директивы, но не все директивы — компоненты.

ТипЧто делаетПримеры
СтруктурныеДобавляют/удаляют DOM-элементы*ngIf, *ngFor, *ngSwitch
АтрибутныеМеняют внешний вид / поведение[ngClass], [ngStyle], [(ngModel)]
КомпонентныеСоздают DOM с шаблоном<app-header>, <mat-button>

Структурные директивы изменяют структуру DOM — добавляют, удаляют или переставляют элементы. Они всегда начинаются со звёздочки *.

// В шаблоне компонента
@Component({
template: `
<!-- Простой *ngIf -->
<div *ngIf="isLoggedIn">
Привет, {{ user.name }}! 👋
</div>
<!-- С else блоком -->
<div *ngIf="isLoggedIn; else loginPrompt">
Вы вошли как {{ user.name }}
</div>
<ng-template #loginPrompt>
<button (click)="login()">Войти</button>
</ng-template>
<!-- С then и else -->
<ng-container *ngIf="status === 'loaded'; then dataBlock; else loadingBlock">
</ng-container>
<ng-template #dataBlock>
<app-data />
</ng-template>
<ng-template #loadingBlock>
<app-spinner />
</ng-template>
`
})
export class AppComponent {
isLoggedIn = false;
status: 'loading' | 'loaded' | 'error' = 'loading';
}

💡 ng-template — невидимый контейнер. Он НЕ рендерится в DOM. Angular использует его как «заготовку» для условных и цикличных шаблонов.


@Component({
template: `
<ul>
<li
*ngFor="let item of items;
let i = index;
let first = first;
let last = last;
let even = even;
let odd = odd;
trackBy: trackById"
[class.first-item]="first"
[class.last-item]="last"
[style.background]="even ? '#f0f0f0' : 'white'"
>
{{ i + 1 }}. {{ item.name }}
<span *ngIf="first"> 🥇 Первый!</span>
<span *ngIf="last"> 🏁 Последний</span>
</li>
</ul>
`
})
export class ListComponent {
items = [
{ id: 1, name: 'Angular' },
{ id: 2, name: 'TypeScript' },
{ id: 3, name: 'RxJS' }
];
// trackBy — ключ производительности! 🔑
// Без него Angular перерендерит ВЕСЬ список при любом изменении
trackById(index: number, item: { id: number }): number {
return item.id; // Angular знает какой элемент изменился
}
}
@Component({
template: `
<div [ngSwitch]="currentView">
<app-list-view *ngSwitchCase="'list'" />
<app-grid-view *ngSwitchCase="'grid'" />
<app-table-view *ngSwitchCase="'table'" />
<app-default-view *ngSwitchDefault />
</div>
<div class="controls">
<button (click)="currentView = 'list'">📋 Список</button>
<button (click)="currentView = 'grid'">🔲 Сетка</button>
<button (click)="currentView = 'table'">📊 Таблица</button>
</div>
`
})
export class DashboardComponent {
currentView: 'list' | 'grid' | 'table' = 'list';
}

🔍 Десахаризация: что прячется за звёздочкой *?

Заголовок раздела «🔍 Десахаризация: что прячется за звёздочкой *?»

Звёздочка — синтаксический сахар. Angular автоматически разворачивает её в <ng-template>:

<!-- Что пишем мы -->
<div *ngIf="condition">Контент</div>
<!-- Что Angular создаёт внутри -->
<ng-template [ngIf]="condition">
<div>Контент</div>
</ng-template>
<!-- *ngFor тоже разворачивается -->
<li *ngFor="let item of items">{{ item }}</li>
<!-- В этот ng-template -->
<ng-template ngFor let-item [ngForOf]="items">
<li>{{ item }}</li>
</ng-template>

Понимание десахаризации помогает отлаживать сложные случаи с несколькими директивами на одном элементе 🕵️


Angular 17 ввёл встроенные управляющие конструкции — они быстрее, типобезопаснее и не требуют CommonModule:

<!-- @if вместо *ngIf — поддерживает else if! -->
@if (isLoggedIn) {
<div>Добро пожаловать, {{ user.name }}!</div>
} @else if (isLoading) {
<app-spinner />
} @else {
<button (click)="login()">Войти</button>
}
<!-- @for вместо *ngFor — track ОБЯЗАТЕЛЕН -->
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
} @empty {
<!-- Показывается если список пустой! -->
<li>Список пуст 😢</li>
}
<!-- @switch вместо *ngSwitch -->
@switch (status) {
@case ('loading') { <app-spinner /> }
@case ('error') { <app-error-message /> }
@case ('empty') { <app-empty-state /> }
@default { <app-content /> }
}

🚀 Почему новый синтаксис лучше?

  • Компилируется в более эффективный код
  • Не нужен импорт CommonModule / NgIf / NgFor
  • IDE лучше понимает типы внутри блоков
  • @empty в @for — killer-фича которой не было в *ngFor

Атрибутные директивы не меняют структуру DOM — они меняют внешний вид или поведение существующих элементов.

@Component({
template: `
<!-- Объект: имя класса -> условие -->
<div [ngClass]="{
'active': isActive,
'disabled': isDisabled,
'highlight': count > 5
}">
Динамические классы
</div>
<!-- Массив классов -->
<div [ngClass]="['base-style', themeClass, sizeClass]">
Несколько классов сразу
</div>
<!-- Тернарный оператор -->
<button [ngClass]="isActive ? 'btn btn-primary' : 'btn btn-secondary'">
Кнопка
</button>
<!-- Метод компонента -->
<div [ngClass]="getStatusClasses()">
Статус: {{ status }}
</div>
`
})
export class StyleDemoComponent {
isActive = true;
isDisabled = false;
count = 7;
themeClass = 'dark-theme';
sizeClass = 'size-lg';
status: 'online' | 'offline' | 'busy' = 'online';
getStatusClasses(): Record<string, boolean> {
return {
'status-badge': true,
'status-online': this.status === 'online',
'status-offline': this.status === 'offline',
'status-busy': this.status === 'busy',
};
}
}
@Component({
template: `
<!-- Объект стилей -->
<p [ngStyle]="{
'color': textColor,
'font-size': fontSize + 'px',
'font-weight': isBold ? 'bold' : 'normal',
'opacity': isVisible ? 1 : 0,
'transition': 'all 0.3s ease'
}">
Стилизованный текст
</p>
<!-- Управление через контролы -->
<input type="range" [(ngModel)]="fontSize" min="12" max="32">
<input type="color" [(ngModel)]="textColor">
<input type="checkbox" [(ngModel)]="isBold"> Жирный
`
})
export class DynamicStyleComponent {
textColor = '#dd0031';
fontSize = 16;
isBold = false;
isVisible = true;
}

<!-- ❌ БЕЗ trackBy: Angular пересоздаёт ВСЕ DOM-элементы при любом изменении массива -->
<li *ngFor="let user of users">{{ user.name }}</li>
<!-- ✅ С trackBy: Angular обновляет ТОЛЬКО изменившиеся элементы -->
<li *ngFor="let user of users; trackBy: trackByUserId">{{ user.name }}</li>
@Component({
changeDetection: ChangeDetectionStrategy.OnPush, // ещё +50% к скорости!
template: `
<li *ngFor="let item of items$ | async; trackBy: trackById">
{{ item.name }}
</li>
`
})
export class OptimizedListComponent {
items$ = this.store.select(selectItems);
// Функция должна возвращать уникальный идентификатор
trackById = (_index: number, item: Item): number => item.id;
constructor(private store: Store) {}
}

🎯 Правило большого пальца: Всегда используй trackBy для динамических списков. Без него Angular удаляет и пересоздаёт все DOM-узлы при каждом изменении массива — даже если изменился только один элемент.


Возможность*ngFor@for (Angular 17+)
Синтаксис*ngFor="let i of items"@for (i of items; track i.id)
track обязателенНетДа ✅
Пустой списокНет встроенного@empty {} блок
ПроизводительностьХорошаяЛучше
Нужен импортNgFor / CommonModuleНет