Директивы в Angular: как создавать и применять пользовательские директивы

Angular предоставляет мощный набор встроенных директив и позволяет создавать собственные директивы, чтобы инкапсулировать поведение и повторно использовать его в шаблонах.
Что такое директивы?
Директивы — это классы, помеченные декоратором @Directive, которые изменяют поведение или внешний вид HTML-элемента. Коротко:
- Атрибутные директивы (attribute directives) изменяют внешний вид или поведение существующего элемента.
- Структурные директивы (structural directives) добавляют, удаляют или перемещают элементы в DOM.
Пример встроенных директив: ngIf (структурная) и ngFor (структурная), ngClass/ngStyle (атрибутные).
Ключевая идея: если нужно инкапсулировать небольшое поведение, используйте директивы; если нужен независимый визуальный блок с шаблоном — компонент.
Преимущества использования директив
- Повторное использование логики без копирования шаблонов.
- Разделение ответственности: шаблон отвечает за разметку, директива — за поведение.
- Легкая тестируемость: директивы — обычные классы, их можно покрыть unit-тестами.
- Производительность: структурные директивы управляют созданием/удалением представлений по необходимости.
Важно: плохое управление жизненным циклом или манипуляция DOM напрямую может привести к утечкам памяти и проблемам с SSR.
Начало работы: установка и создание проекта
Установите Angular CLI и создайте проект (код для терминала):
npm install -g @angular/cli ng new custom-directives-app Команда создаст проект с именем custom-directives-app.
Создание атрибутной директивы: базовый highlight
Создайте файл src/app/highlight.directive.ts и определите класс с декоратором @Directive:
import { Directive } from "@angular/core";
@Directive({
selector: "[myHighlight]",
})
export class HighlightDirective {
constructor() {}
} Применить директиву в шаблоне можно так:
Some text
В этом виде директива ничего не делает — это «флаг» в шаблоне. Дальше добавим поведение.
Добавление поведения: ElementRef vs Renderer2
Самый простой способ изменить стиль — инжектировать ElementRef и напрямую менять nativeElement. Но это может привести к проблемам при серверном рендеринге (SSR) и потенциальным уязвимостям. Рекомендуется использовать Renderer2 для безопасной абстракции манипуляций DOM.
Пример с ElementRef (из учебного примера):
import { Directive, ElementRef } from "@angular/core";
@Directive({
selector: "[myHighlight]"
})
export class HighlightDirective {
constructor(private element: ElementRef) {
this.element.nativeElement.style.backgroundColor = 'lightblue';
}
} Рекомендованный пример с Renderer2:
import { Directive, ElementRef, Renderer2 } from "@angular/core";
@Directive({ selector: '[myHighlight]' })
export class HighlightDirective {
constructor(private el: ElementRef, private renderer: Renderer2) {
this.renderer.setStyle(this.el.nativeElement, 'background-color', 'lightblue');
}
} Почему Renderer2: он абстрагирует низкоуровневые операции, работает с SSR и Web Workers и уменьшает риск XSS при корректном использовании.
Приём подключения директивы в модуле
Не забудьте зарегистрировать директиву в AppModule:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HighlightDirective } from './highlight.directive';
@NgModule({
declarations: [
AppComponent,
HighlightDirective,
],
imports: [
BrowserModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { } Теперь директиву можно применять в шаблонах:
Some text
Запустите dev-сервер:
ng serve Откройте http://localhost:4200/ чтобы увидеть результат.

Передача значения в директиву через @Input
Чтобы директива принимала значение (например, цвет), используйте @Input. Можно оформить setter, чтобы реагировать на изменения значения:
import { Directive, ElementRef, Input, Renderer2 } from "@angular/core";
@Directive({ selector: '[myHighlight]' })
export class HighlightDirective {
@Input() set myHighlight(color: string) {
const value = color || 'transparent';
this.renderer.setStyle(this.el.nativeElement, 'background-color', value);
}
constructor(private el: ElementRef, private renderer: Renderer2) {}
} В шаблоне:
Some text
Можно также указать алиас для входного свойства, если хотите другой синтаксис:
@Input('myHighlight') set highlight(color: string) { ... }Жизненный цикл директив и очистка
Директивы поддерживают хуки жизненного цикла (OnInit, OnChanges, OnDestroy и др.). Если вы подписываетесь на события или создаете таймеры, удаляйте подписки в ngOnDestroy(), чтобы избежать утечек.
Пример очистки:
import { OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
export class ExampleDirective implements OnDestroy {
private sub = new Subscription();
ngOnDestroy() {
this.sub.unsubscribe();
}
}Создание структурной директивы: condition
Структурные директивы работают с TemplateRef и ViewContainerRef. TemplateRef представляет шаблон, а ViewContainerRef — контейнер в DOM, куда вставляются представления.
Файл src/app/condition.directive.ts:
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'
@Directive({
selector: "[condition]"
})
export class ConditionDirective {
@Input() set condition(arg: boolean) {
if(arg) {
this.viewContainer.createEmbeddedView(this.template)
} else {
this.viewContainer.clear();
}
}
constructor(
private template: TemplateRef,
private viewContainer: ViewContainerRef
) {}
} Применение в шаблоне (микросинтаксис):
Hello There!!!
Под капотом Angular превращает конструкцию *condition в
Пример расширения для else:
@Input() set condition(arg: boolean) { ... }
@Input('conditionElse') elseTemplate?: TemplateRef; Частые расширения и полезные паттерны
- exportAs: позволяет дать директиве имя для доступа через template reference variable (#ref=”directiveName”).
- HostBinding / HostListener: связывать классы, стили и реакции на события хоста.
- Инъекция зависимостей: директивы могут инжектировать сервисы, другие директивы или элементы.
- Использование ChangeDetectorRef для ручного управления обнаружением изменений при необходимости.
Пример HostListener и HostBinding:
import { Directive, HostListener, HostBinding } from '@angular/core';
@Directive({ selector: '[hoverClass]' })
export class HoverClassDirective {
@HostBinding('class.hover') isHover = false;
@HostListener('mouseenter') onEnter() { this.isHover = true; }
@HostListener('mouseleave') onLeave() { this.isHover = false; }
}Когда директивы не подходят (контрпримеры)
- Вам нужен сложный компонент с собственным шаблоном и стилями — используйте компонент.
- Нужно выполнить вычисление чистой трансформации данных для отображения — лучше pipe (конвейер).
- Когда поведение зависит от маршрутизации и жизненных циклов компонентов, бывает проще реализовать сервис и вызывать его из компонента.
Альтернативные подходы
- Компоненты: для визуально значимого UI-блока с шаблоном.
- Сервисы: для бизнес-логики и хранения состояния.
- Pipes: для преобразования данных в шаблонах при отображении.
Выбор: если задача — повлиять на DOM-элемент (напр., повесить слушатель или поменять стиль) — директива. Если нужно отрисовать фрагмент шаблона — компонент.
Мини-методология: шаги разработки директивы
- Спроектировать API директивы (атрибут/структура, входы, выводы, алиасы).
- Начать с тестов: unit-тесты ожидаемого поведения в шаблоне.
- Реализовать класс с @Directive, инжектировать требуемые зависимости.
- Применять Renderer2 вместо прямых операций с nativeElement.
- Обработать жизненный цикл (OnInit, OnDestroy) и отписки.
- Добавить примеры использования и документацию в README компонента.
Чеклисты по ролям
Developer:
- Определил API директивы и входные значения.
- Использовал Renderer2 вместо nativeElement, где возможно.
- Добавил unit-тесты и e2e-примеры использования.
Reviewer:
- Проверил безопасность манипуляции DOM (XSS-вектора).
- Убедился в корректной очистке подписок.
- Проверил совместимость с SSR при необходимости.
QA:
- Проверил поведение директивы на разных браузерах и размерах экрана.
- Протестировал edge-case (null/undefined/пустые строки в входах).
DevOps/CI:
- Убедился, что bundle size не вырос критически (если директив много).
- Проверил автотесты и линтеры.
Критерии приёмки
- Директива документирована и имеет примеры в репозитории.
- Unit-тесты покрывают основные ветки (true/false, изменения входов).
- Нет утечек (ngOnDestroy отписывает/очищает ресурсы).
- Используется Renderer2 для DOM-операций, когда это важно.
Безопасность и производительность
- Избегайте вставки HTML через innerHTML без санитайзинга.
- Для стилей и классов используйте Renderer2.setStyle / addClass / removeClass.
- Минимизируйте работу в главном потоке: не выполняйте тяжёлые вычисления в директиве; выносите их в сервисы или Web Worker.
- Структурные директивы, которые часто создают/очищают представления, могут привести к перерисовкам — профилируйте при масштабировании.
Пример расширенной директивы: alias, exportAs и реакция на изменения
import { Directive, Input, Renderer2, ElementRef, OnChanges, SimpleChanges } from '@angular/core';
@Directive({ selector: '[myHighlight]', exportAs: 'myHighlight' })
export class HighlightDirective implements OnChanges {
@Input('myHighlight') color?: string;
constructor(private el: ElementRef, private renderer: Renderer2) {}
ngOnChanges(changes: SimpleChanges) {
const c = changes['color']?.currentValue || 'transparent';
this.renderer.setStyle(this.el.nativeElement, 'background-color', c);
}
}Теперь в шаблоне можно получить доступ к директиве через reference:
Text
Принятие решений: когда выбирать директиву или компонент (дерево)
flowchart TD
A[Нужна логика, завязанная на DOM-элементе?] -->|Да| B{Требуется собственный шаблон?}
A -->|Нет| C[Используйте сервис или pipe]
B -->|Да| D[Компонент]
B -->|Нет| E[Директива]
E --> F{Изменяет структуру DOM?}
F -->|Да| G[Структурная директива]
F -->|Нет| H[Атрибутная директива]Примеры тест-кейсов и приёмочных критериев
- Атрибутная директива должна корректно применять значение цвета при установке входа.
- Изменение входного значения должно обновлять стиль без утечек.
- Структурная директива должна показывать/скрывать содержимое при true/false.
- Для else-ветки должно корректно рендериться альтернативное представление.
Совместимость и миграция
- Большинство приёмов работы с директивами остаётся неизменными между версиями Angular, однако API низкоуровневых платформенных точек может эволюционировать. Проверяйте breaking changes в релиз-нотах при мажорных обновлениях.
Локальные примечания и уловки для российских проектов
- Локализация текстов в шаблонах лучше держать в i18n-системе (Angular i18n или сторонние решения), но поведение директив обычно не завязано на локализации.
- При работе с серверной частью (SSR) обязательно тестируйте директивы, которые обращаются к window/document.
Быстрые советы и шпаргалка
- Если нужно просто изменить DOM-атрибут или класс — используйте HostBinding/HostListener.
- Для безопасной модификации DOM — Renderer2.
- Для условного рендеринга — структурная директива с TemplateRef и ViewContainerRef.
- Всегда отписывайтесь от подписок в ngOnDestroy.
Заключение
Директивы — гибкий инструмент Angular для расширения поведения элементов и контроля над DOM. Используйте их для инкапсуляции небольших, повторно используемых паттернов поведения; применяйте Renderer2 для безопасности и совместимости; тестируйте жизненный цикл и очищайте ресурсы. Для более сложного UI выбирайте компоненты, для трансформации данных — pipes, а директивы оставляйте для DOM-поведения.
Похожие материалы
Внезапный рост спама в почте: что делать
Wireshark: захват и анализ сетевых пакетов
Windows 8: устранение синих экранов и падений
Ссылка на часть веб‑страницы: Citebite и Highlighter
Gmail как список для чтения — собрать и организовать