Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке директиви та які їх типи існують в Angular?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Angular директива** - це клас з `@Directive`, який розширює поведінку HTML-елементів. Три типи: атрибутні змінюють вигляд або поведінку (`ngClass`, `ngStyle`), структурні додають або видаляють елементи DOM (`*ngIf`, `*ngFor`), компонентні поєднують обидва підходи з власним шаблоном. ```typescript @Directive({ selector: '[appHighlight]' }) export class HighlightDirective { @HostListener('mouseenter') onHover() { /* змінити стиль */ } } ``` **Головне правило:** потрібна логіка для DOM без UI? Директива. Потрібен UI? Компонент.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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`, `HostBinding` | `ngClass`, `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 взагалі.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.