Angular: создание кастомных директив

Кастомные директивы в Angular позволяют добавлять поведение к DOM-элементам: изменять разметку, внешний вид и реакцию на события. Эта статья пошагово объясняет, как создать атрибутную и структурную директивы, подключить их в модуле, передавать параметры и тестировать. В конце — чек-листы, шаблоны и советы по отладке.
Что такое директивы?
Директивы — это фрагменты кода в Angular, которые расширяют поведение HTML-элементов или компонентов. Простая формулировка: директива «подключается» к элементу и может изменить его разметку, стиль, слушать события или управлять созданием/удалением частей DOM.
Кратко о типах:
- Атрибутные директивы (attribute directives) — меняют внешний вид или поведение элемента. Пример: подсветка, изменение стилей.
- Структурные директивы (structural directives) — добавляют, удаляют или перемещают элементы в DOM с помощью представлений (views). Примеры: ngIf, ngFor.
Определение в одну строку: директива — класс с декоратором @Directive, который сообщает Angular, как и где применять этот класс.
Преимущества использования директив
- Переиспользуемость: одна директива — много компонентов.
- Разделение ответственности: логика поведения вынесена из компонентов в отдельные сущности.
- Тестируемость: директивы легко покрывать юнит-тестами отдельно от UI.
Важно: директивы не должны становиться контейнерами бизнес-логики. Их задача — взаимодействие с DOM и представлением.
Настройка проекта Angular
Убедитесь, что у вас установлен Angular CLI и создан проект. Примеры команд (используйте терминал):
npm install -g @angular/cli
ng new custom-directives-appПосле создания проекта откройте его в редакторе и переходите к src/app.
Создание атрибутной директивы (простейший пример)
Создайте файл src/app/highlight.directive.ts и определите директиву:
import { Directive } from '@angular/core';
@Directive({
selector: '[myHighlight]',
})
export class HighlightDirective {
constructor() {}
}Пояснение: selector: ‘[myHighlight]’ значит — директиву можно применять как атрибут:
.
Пример использования в шаблоне:
Some text
Добавление поведения: доступ к элементу через ElementRef
Чтобы изменить DOM-элемент, используйте ElementRef. Пример: установка фонового цвета.
import { Directive, ElementRef } from '@angular/core';
@Directive({
selector: '[myHighlight]'
})
export class HighlightDirective {
constructor(private element: ElementRef) {
this.element.nativeElement.style.backgroundColor = 'lightblue';
}
}Объяснение: Angular внедрит (DI) ElementRef, который содержит nativeElement — реальный DOM-узел. Менять стиль напрямую просто, но осторожно: это обходит механизм Angular верификации безопасности.
Важно: прямой доступ к nativeElement может привести к уязвимостям (XSS) и усложнить серверный рендеринг. По возможности используйте Renderer2.
Более правильный способ: Renderer2
Renderer2 — абстракция для безопасных манипуляций с DOM, совместимая с платформами, где нет браузерного DOM (SSR).
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, 'backgroundColor', 'lightblue');
}
}Используйте Renderer2, если проект предполагает универсальный рендеринг или вам важна безопасность манипуляций с DOM.
Подключение директивы в модуле
Не забудьте объявить директиву в app.module.ts:
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 { }После этого директива доступна во всех шаблонах компонентов, объявленных в модуле.
Передача значения в директиву с помощью @Input
Чтобы директива принимала параметр (например, цвет), используйте @Input и setter:
import { Directive, ElementRef, Input } from '@angular/core';
@Directive({
selector: '[myHighlight]'
})
export class HighlightDirective {
@Input() set myHighlight(color: string) {
this.element.nativeElement.style.backgroundColor = color;
}
constructor(private element: ElementRef) { }
}Пример использования:
Some text
Подсказка: для реактивного обновления используйте setter с приведением типов и проверкой входных данных.
Создание структурной директивы (пример, аналог ngIf)
Структурные директивы работают с TemplateRef и ViewContainerRef: они создают или удаляют представления.
Создайте src/app/condition.directive.ts с кодом:
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[condition]'
})
export class ConditionDirective {
@Input() set condition(value: boolean) {
if (value) {
this.viewContainer.createEmbeddedView(this.template);
} else {
this.viewContainer.clear();
}
}
constructor(
private template: TemplateRef,
private viewContainer: ViewContainerRef
) {}
} Использование:
Hello There!!!
Пояснение: синтаксис *condition преобразуется компилятором в вызов с помощью
Когда использовать структурную директиву, а когда — атрибутную
- Нужно добавить/удалить элемент целиком — структурная директива.
- Нужно изменить стиль/поведение существующего элемента — атрибутная директива.
Ментальная модель: структурная директива манипулирует деревом компонентов (контейнеры и представления), атрибутная — объектом-элементом (стили, слушатели).
Критерии приёмки
- Директива объявлена в модуле и импортируется без ошибок.
- Применение директивы в шаблоне изменяет DOM согласно ожиданию.
- Для директив с @Input значения корректно принимаются и применяются при изменении.
- Нет прямых ошибок безопасности при использовании nativeElement; если используется nativeElement, это одобрено командой безопасности.
- Unit-тесты покрывают ключевые ветви: создание, обновление и удаление представлений (для структурных).
Тестирование директив
Unit-тесты для директивы проверяют:
- Инициализацию: директива создаётся и не выбрасывает исключений.
- Поведение setter @Input: изменение входного значения приводит к ожидаемому эффекту.
- Для структурных директив: при true — содержимое отображается, при false — скрывается.
Пример теста (псевдокод):
- Создать TestComponent с шаблоном, использующим директиву.
- Вставить в TestBed, получить fixture и проверить innerHTML/стили.
Отладка и лучшие практики
- Используйте Renderer2 вместо прямых обращений к nativeElement, чтобы быть совместимым с SSR.
- Минимизируйте побочные эффекты в конструкторе директивы; предпочтительнее делать инициализацию в ngOnInit.
- Валидация входных данных: проверяйте типы и значения перед применением стилей.
- Не храните бизнес-логику в директивах; держите их ответственными только за представление.
- Для сложных операций используйте hostListener и hostBinding или экспортируйте API директивы для взаимодействия с компонентом.
Важно: при манипуляции DOM учитывайте производительность — частые прямые изменения стиля могут привести к переработке компоновки (reflow).
Альтернативные подходы
- Компонент-обёртка вместо директивы — если нужно контролировать шаблон и структуру дочерних элементов.
- Pipes — если задача связана с преобразованием данных в шаблоне, а не с DOM.
- Сервисы/State Management — если поведение глобальное и должно быть разделяемым между различными компонентами.
Когда директивы не подходят (контрпримеры)
- Нужна сложная логика рендеринга с собственным шаблоном — лучше компонент.
- Требуется доступ к жизненному циклу дочерних компонентов на уровне шаблона — предпочтите компоненты с ng-content.
- Логика тесно связана с бизнес-правилами и должна быть тестируема без DOM — выносите в сервисы.
Рольные чек-листы
Разработчик:
- Объявить директиву в модуле.
- Использовать Renderer2 для стилей.
- Покрыть unit-тестами ключевое поведение.
Тестировщик:
- Проверить визуальные изменения в разных сценариях.
- Убедиться в отсутствии регрессий и утечек памяти (при создании/удалении представлений).
Владелец продукта:
- Подтвердить пользовательские сценарии применения директивы.
- Убедиться, что UX не ломается при отключении/включении директивы.
Шпаргалка: полезные API и шаблоны
- @Directive({ selector: ‘[name]’ }) — объявление директивы.
- ElementRef, Renderer2 — доступ и безопасные манипуляции с DOM.
- @Input() — передача параметров в директиву.
- TemplateRef, ViewContainerRef — создание структурных представлений.
- hostBinding, hostListener — привязки к свойствам и событиям host-элемента.
Примеры быстрых сниппетов:
// host listener
import { HostListener } from '@angular/core';
@HostListener('mouseenter') onEnter() { /* ... */ }
// host binding
import { HostBinding } from '@angular/core';
@HostBinding('class.active') isActive = false;Технические ограничения и совместимость
- На серверной части (Angular Universal) избегайте прямых обращений к window/document без проверки (typeof window !== ‘undefined’).
- Для сторонних платформ (web workers) используйте Renderer2 и абстракции.
- Совместимость с Ivy: современные директивы работают с Ivy/NG View Engine, но проверьте target и compilation options в tsconfig для старых проектов.
Короткое руководство: шаги реализации (минимальная методология)
- Выбрать тип директивы — атрибутная или структурная.
- Сгенерировать файл и объявить класс с @Directive.
- Реализовать поведение через ElementRef/Renderer2 или TemplateRef/ViewContainerRef.
- Добавить @Input/@Output при необходимости API.
- Объявить директиву в модуле.
- Написать unit-тесты и протестировать в браузере (ng serve).
Краткое глоссарий
- ElementRef — обёртка над нативным DOM-элементом.
- Renderer2 — API для безопасных операций с DOM.
- TemplateRef — шаблон, который можно рендерить как представление.
- ViewContainerRef — контейнер для вставки представлений.
Пример запуска и проверки
Запустите dev-сервер:
ng serveОткройте http://localhost:4200 и проверьте элементы, к которым применена директива.
Резюме
Кастомные директивы — мощный инструмент Angular для расширения поведения DOM и создания переиспользуемых UI-паттернов. Используйте Renderer2 для безопасности и совместимости, покрывайте директивы тестами, и выбирайте между компонентом и директивой, исходя из того, нужно ли вам управлять шаблоном или только поведением элемента.
Важно: экспортируйте только тот минимальный API директивы, который действительно нужен, и избегайте смешивания бизнес-логики с манипуляциями DOM.