Skip to main content

Що таке директиви та які їх типи існують в Angular?

Angular директива - це клас з декоратором @Directive, який Angular використовує для розширення або зміни HTML-елементів під час компіляції шаблону.

Теорія

TL;DR

  • Директиви - це HTML-плагіни: чіпляєш до будь-якого елемента і отримуєш нові стилі, події або зміни DOM без переписування самого елемента
  • 3 типи: Атрибутні (декорують існуючі елементи), Структурні (додають або видаляють вузли DOM), Компоненти (директиви з власним шаблоном)
  • Атрибутний селектор: [appHighlight]. Структурний використовує префікс *: *ngIf. Компонент - тег: <app-card>
  • Потрібна логіка для DOM без UI? Директива. Потрібен блок UI з розміткою? Компонент.

Швидкий приклад

Класична appHighlight атрибутна директива показує весь патерн приблизно в 12 рядках:

html
<div appHighlight>Наведи мишку</div>
typescript
import { Directive, ElementRef, HostListener } from '@angular/core'; @Directive({ selector: '[appHighlight]' }) export class HighlightDirective { constructor(private el: ElementRef) {} @HostListener('mouseenter') onEnter() { this.el.nativeElement.style.backgroundColor = 'yellow'; } @HostListener('mouseleave') onLeave() { this.el.nativeElement.style.backgroundColor = null; } }

Angular знаходить атрибут [appHighlight], впроваджує ElementRef, і @HostListener підключає події миші. Шаблон не потрібен.

Ключова різниця

Атрибутні директиви декорують існуючий елемент: змінюють стиль, клас або поведінку, але структуру DOM не чіпають. Структурні директиви переписують DOM. *ngIf повністю видаляє елемент коли умова хибна. *ngFor клонує шаблонний вузол для кожного елемента масиву. Компоненти - це теж директиви, тільки з доданим шаблоном і інкапсуляцією стилів. Такий поділ дозволяє Angular пропустити компіляцію шаблону для атрибутних директив, тоді як структурні кожну зміну пропускають через ViewContainerRef.

Коли використовувати

  • Зміна стилів або класів елемента: атрибутна директива (ngClass, ngStyle, кастомна стилізація валідатора)
  • Умовний рендеринг або ітерація: структурна (*ngIf, *ngFor, *ngSwitch)
  • Блок UI з власною розміткою: компонент, не директива
  • Обробники подій на хост-елементі: атрибутна з @HostListener або @HostBinding
  • Логіка без жодної залежності від DOM: сервіс, не директива

Типи директив

ТипСинтаксис селектораВплив на DOMКлючові Angular APIВбудовані приклади
Атрибутна[appHighlight]Відсутній (декорує хост-елемент)ElementRef, HostListener, HostBindingngClass, ngStyle
Структурна*appUnless, *ngIfДодає або видаляє елементиViewContainerRef, TemplateRef*ngIf, *ngFor, *ngSwitch
Компонент<app-user-card>Рендерить власний шаблон@Component (розширює @Directive)Будь-який кастомний UI-блок

Як Angular обробляє директиви

Під час AOT-компіляції Angular сканує шаблони, знаходить відповідності між селекторами і метаданими @Directive, потім генерує фабричні функції. В рантаймі фреймворк створює екземпляри директив через dependency injection і запускає lifecycle hooks: спочатку ngOnInit, потім ngAfterViewInit. Для структурних директив будь-яка зміна стану викликає createEmbeddedView() або clear() на ViewContainerRef. Саме так *ngIf видаляє і вставляє вузли DOM без повного перерендеру. Атрибутні директиви цей крок пропускають, тому вони дешевші у великих списках.

Типові помилки

1. Прямий доступ до nativeElement замість Renderer2

typescript
// Ламає SSR (Angular Universal) constructor(el: ElementRef) { el.nativeElement.style.color = 'red'; } // Правильно: Renderer2 працює і в браузері, і на сервері constructor(private el: ElementRef, private renderer: Renderer2) { renderer.setStyle(el.nativeElement, 'color', 'red'); }

Прямі зміни через nativeElement спричиняють помилки гідратації в Angular Universal. Renderer2 абстрагує DOM так, щоб Angular правильно обробляв обидва середовища.

2. Забутий TemplateRef і ViewContainerRef у структурній директиві

typescript
// Шаблон ніколи не відрендериться @Directive({ selector: '[appIf]' }) export class AppIfDirective { @Input() appIf: boolean; // Angular немає куди вставити шаблон } // Правильно export class AppIfDirective { constructor( private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef ) {} @Input() set appIf(condition: boolean) { condition ? this.viewContainer.createEmbeddedView(this.templateRef) : this.viewContainer.clear(); } }

Це найпоширеніший баг «директива не рендериться». Синтаксис * - це лише синтаксичний цукор. Angular все одно потребує ViewContainerRef, щоб фізично вставити шаблон у DOM.

3. Кілька структурних директив на одному елементі

html
<!-- Angular застосовує тільки першу структурну директиву, друга ігнорується --> <div *ngFor="let item of items" *ngIf="item.active">{{ item.name }}</div> <!-- Правильно: вкладай --> <div *ngFor="let item of items"> <span *ngIf="item.active">{{ item.name }}</span> </div>

4. Відсутній $event у @HostListener

typescript
@HostListener('click') onClick() {} // Немає доступу до координат або target @HostListener('click', ['$event']) onClick(event: MouseEvent) { console.log(event.target); // Працює як очікується }

Де зустрічається

  • Angular Material: matTooltip додає підказки до будь-якого елемента через один атрибут
  • NG Bootstrap: ngbTooltip так само підключається до Bootstrap CSS класів
  • PrimeNG: pTooltip у enterprise таблицях з даними
  • Nx workspaces: кастомна структурна *nxLoading для lazy-завантаження модулів
  • Reactive Forms: кастомні атрибутні валідатори інтегруються безпосередньо з системою контролів NgForm

Питання на співбесіді

Q: Яка різниця між @Directive і @Component?
A: @Component розширює @Directive і додає шаблон, стилі та інкапсуляцію вигляду. Компонент - це директива, яка знає як відрендерити себе. Звичайний @Directive не має шаблону і не має пов'язаного view.

Q: Як *ngFor використовує trackBy і навіщо це потрібно?
A: trackBy приймає функцію, що повертає унікальний ідентифікатор для кожного елемента. Angular повторно використовує існуючі DOM-вузли при зміні масиву замість того, щоб знищувати і відтворювати кожен елемент. Без trackBy будь-яка зміна масиву (сортування, фільтрація, push) повністю перебудовує список у DOM.

Q: Побудуй кастомну структурну директиву. Що потрібно?
A: Впровадь TemplateRef<any> і ViewContainerRef у конструктор. Додай сеттер через @Input. Коли умова true - виклич this.viewContainer.createEmbeddedView(this.templateRef). Коли false - this.viewContainer.clear(). Використовуй у шаблоні як *appDirectiveName="expression".

Q: Навіщо @HostBinding, якщо можна написати [style.color] прямо в шаблоні?
A: @HostBinding живе всередині класу директиви, правильно працює в ланцюгах успадкування і сумісний з SSR. Прив'язки в шаблоні вимагають, щоб ти контролював цей шаблон, а в бібліотеці директив це не завжди так.

Q: Який перформанс-вплив директив у великих списках?
A: Структурні директиви відтворюють view при кожному проході change detection, якщо батьківський компонент використовує стратегію за замовчуванням. Атрибутні легші, але колбеки @HostListener все одно спрацьовують на кожну відповідну подію. Рішення: ChangeDetectionStrategy.OnPush на батьківському компоненті і trackBy у кожному *ngFor. Signals в Angular 17 скорочують зайві перевірки, реагуючи тільки на конкретні зміни стану.

Приклади

Базовий: SSR-сумісне підсвічування при наведенні

html
<p appHighlight>Наведи мишку для підсвічування</p>
typescript
import { Directive, ElementRef, HostListener, Renderer2 } from '@angular/core'; @Directive({ selector: '[appHighlight]' }) export class HighlightDirective { constructor(private el: ElementRef, private renderer: Renderer2) {} @HostListener('mouseenter') onEnter() { // Renderer2 зберігає сумісність з Angular Universal this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', 'yellow'); } @HostListener('mouseleave') onLeave() { this.renderer.removeStyle(this.el.nativeElement, 'backgroundColor'); } }

Renderer2 замість прямого доступу до nativeElement - це те, що відрізняє швидкий прототип від production-директиви. Той самий атрибут працює на будь-якому елементі в будь-якому шаблоні.

Середній: Кастомна структурна директива

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) { this.viewContainer.createEmbeddedView(this.templateRef); this.hasView = true; } else if (condition && this.hasView) { this.viewContainer.clear(); this.hasView = false; } } }
html
<div *appUnless="isLoading">Контент готовий</div>

Прапорець hasView запобігає дублюванню рендерів при швидких змінах вхідного значення. Це саме той патерн, що лежить в основі *ngIf. Я бачив цю задачу на senior Angular співбесідах, і саме наявність hasView зазвичай відрізняє хорошу відповідь від відмінної.

Просунутий: Структурна директива з async-вхідним значенням

typescript
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[appUnlessLoading]' }) export class UnlessLoadingDirective { private hasView = false; @Input() set appUnlessLoading(loading: Promise<boolean> | boolean) { if (typeof loading === 'boolean') { this.updateView(loading); } else { // Асинхронне розв'язання: Promise повертає фінальний стан завантаження loading.then(result => this.updateView(result)); } } constructor( private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef ) {} private updateView(loading: boolean) { if (!loading && !this.hasView) { this.viewContainer.createEmbeddedView(this.templateRef); this.hasView = true; } else if (loading && this.hasView) { this.viewContainer.clear(); this.hasView = false; } } }
html
<div *appUnlessLoading="fetchingUsers$ | async">Користувачів завантажено</div>

Головна пастка: без прапорця hasView швидкі async-зміни викликають createEmbeddedView кілька разів і залишають дублікати DOM-вузлів у view. clear() видаляє всі view перед створенням нового, але перевірка hasView дозволяє уникнути зайвої роботи з DOM взагалі.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?