Skip to content

Custom Directives

Директивы — классы, которые добавляют поведение или изменяют внешний вид DOM-элементов.

1. Атрибутные директивы

Простая директива

typescript
import { Directive, ElementRef, OnInit } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective implements OnInit {
  constructor(private el: ElementRef) {}

  ngOnInit() {
    this.el.nativeElement.style.backgroundColor = 'yellow';
  }
}
html
<p appHighlight>Этот текст выделен</p>

Директива с параметрами

typescript
@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  @Input('appHighlight') color = 'yellow';
  @Input() defaultColor = 'transparent';

  constructor(private el: ElementRef) {}

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.color || this.defaultColor);
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight('');
  }

  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}
html
<!-- Цвет через основной Input -->
<p [appHighlight]="'lightblue'">Голубой при наведении</p>

<!-- Цвет по умолчанию -->
<p appHighlight defaultColor="lightgreen">Зелёный при наведении</p>

Директива с Renderer2 (безопасный подход)

Renderer2 безопаснее прямого доступа к DOM через ElementRef, особенно при серверном рендеринге (SSR):

typescript
import { Directive, Renderer2, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appTooltip]'
})
export class TooltipDirective {
  @Input('appTooltip') text = '';
  private tooltipEl: HTMLElement | null = null;

  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {}

  @HostListener('mouseenter') show() {
    this.tooltipEl = this.renderer.createElement('span');
    const text = this.renderer.createText(this.text);
    this.renderer.appendChild(this.tooltipEl, text);
    this.renderer.appendChild(this.el.nativeElement, this.tooltipEl);
    this.renderer.addClass(this.tooltipEl, 'tooltip');
    this.renderer.setStyle(this.tooltipEl, 'position', 'absolute');
    this.renderer.setStyle(this.tooltipEl, 'background', '#333');
    this.renderer.setStyle(this.tooltipEl, 'color', '#fff');
    this.renderer.setStyle(this.tooltipEl, 'padding', '4px 8px');
    this.renderer.setStyle(this.tooltipEl, 'borderRadius', '4px');
    this.renderer.setStyle(this.tooltipEl, 'fontSize', '12px');
  }

  @HostListener('mouseleave') hide() {
    if (this.tooltipEl) {
      this.renderer.removeChild(this.el.nativeElement, this.tooltipEl);
      this.tooltipEl = null;
    }
  }
}
html
<button [appTooltip]="'Нажмите для сохранения'">Сохранить</button>

2. @HostListener и @HostBinding

@HostListener — слушает события на хост-элементе

typescript
@Directive({
  selector: '[appClickTracker]'
})
export class ClickTrackerDirective {
  clickCount = 0;

  @HostListener('click', ['$event'])
  onClick(event: MouseEvent) {
    this.clickCount++;
    console.log(`Клик #${this.clickCount} по координатам (${event.clientX}, ${event.clientY})`);
  }

  @HostListener('document:keydown.escape')
  onEscape() {
    console.log('Нажат Escape');
  }
}

@HostBinding — привязывает свойство хост-элемента

typescript
@Directive({
  selector: '[appDropdown]'
})
export class DropdownDirective {
  @HostBinding('class.open') isOpen = false;
  @HostBinding('style.border') border = '';

  @HostListener('click') toggle() {
    this.isOpen = !this.isOpen;
    this.border = this.isOpen ? '2px solid blue' : '';
  }
}
html
<div appDropdown>
  <button>Меню</button>
  <ul *ngIf="...">
    <li>Пункт 1</li>
    <li>Пункт 2</li>
  </ul>
</div>
<!-- При клике добавляется/убирается класс 'open' -->

3. Структурные директивы

Структурные директивы изменяют структуру DOM (добавляют/удаляют элементы).

Простая структурная директива

typescript
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appUnless]'
})
export class UnlessDirective {
  private hasView = false;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {}

  @Input() set appUnless(condition: boolean) {
    if (!condition && !this.hasView) {
      // Условие false — показываем элемент
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (condition && this.hasView) {
      // Условие true — убираем элемент
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}
html
<!-- Показывается когда isLoggedIn === false -->
<p *appUnless="isLoggedIn">Пожалуйста, войдите в систему</p>

Директива повторения (аналог ngFor)

typescript
@Directive({
  selector: '[appRepeat]'
})
export class RepeatDirective {
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {}

  @Input() set appRepeat(times: number) {
    this.viewContainer.clear();
    for (let i = 0; i < times; i++) {
      this.viewContainer.createEmbeddedView(this.templateRef, {
        $implicit: i,
        index: i,
        total: times
      });
    }
  }
}
html
<p *appRepeat="5; let i; let total = total">
  Элемент {{ i + 1 }} из {{ total }}
</p>

Директива с ролевым доступом

typescript
@Directive({
  selector: '[appHasRole]'
})
export class HasRoleDirective implements OnInit {
  @Input('appHasRole') requiredRole = '';
  private hasView = false;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef,
    private authService: AuthService
  ) {}

  ngOnInit() {
    const userRole = this.authService.getUserRole();
    if (userRole === this.requiredRole && !this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (userRole !== this.requiredRole && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}
html
<button *appHasRole="'admin'">Удалить пользователя</button>
<div *appHasRole="'editor'">Редактирование контента</div>

4. Директива с экспортом

Директиву можно экспортировать для использования через template variable:

typescript
@Directive({
  selector: '[appToggle]',
  exportAs: 'appToggle'
})
export class ToggleDirective {
  isOpen = false;

  toggle() {
    this.isOpen = !this.isOpen;
  }

  open() {
    this.isOpen = true;
  }

  close() {
    this.isOpen = false;
  }
}
html
<div appToggle #menu="appToggle">
  <button (click)="menu.toggle()">
    {{ menu.isOpen ? 'Закрыть' : 'Открыть' }}
  </button>
  <ul *ngIf="menu.isOpen">
    <li>Пункт 1</li>
    <li>Пункт 2</li>
  </ul>
</div>

5. Standalone Directive

typescript
@Directive({
  selector: '[appAutoFocus]',
  standalone: true
})
export class AutoFocusDirective implements AfterViewInit {
  constructor(private el: ElementRef) {}

  ngAfterViewInit() {
    this.el.nativeElement.focus();
  }
}
typescript
@Component({
  standalone: true,
  imports: [AutoFocusDirective],
  template: `<input appAutoFocus placeholder="Автофокус">`
})
export class SearchComponent {}

6. Composition API для директив (Angular 15+)

Директивы можно компоновать через hostDirectives:

typescript
@Directive({ standalone: true, selector: '[appDraggable]' })
export class DraggableDirective { /* ... */ }

@Directive({ standalone: true, selector: '[appResizable]' })
export class ResizableDirective { /* ... */ }

@Component({
  selector: 'app-widget',
  hostDirectives: [DraggableDirective, ResizableDirective],
  template: `<div>Виджет с drag & resize</div>`
})
export class WidgetComponent {}

Сводная таблица

Тип директивыНазначениеПример
АтрибутнаяИзменяет вид/поведение[appHighlight], [appTooltip]
СтруктурнаяДобавляет/удаляет DOM*appUnless, *appHasRole
С экспортомДоступ через #ref#menu="appToggle"
StandaloneБез NgModulestandalone: true
Host DirectiveКомпозицияhostDirectives: [...]