10 Direktiven

10.1 DOM-Manipulation durch Anweisungen

Direktiven erweitern HTML-Elemente mit zusätzlicher Funktionalität. Sie sind Anweisungen, die Angular beim Parsen des DOM erkennt und ausführt. Während Komponenten die Grundbausteine einer Angular-Anwendung bilden, ermöglichen Direktiven die Anreicherung dieser Bausteine mit Verhalten, Styling und strukturellen Änderungen.

Technisch gesehen sind Direktiven TypeScript-Klassen mit einem @Directive-Dekorator, der Metadaten für Angular bereitstellt. Der Selektor im Dekorator bestimmt, auf welche DOM-Elemente die Direktive angewendet wird. Angular integriert Direktiven tief in sein Change Detection-System, wodurch sie reaktiv auf Zustandsänderungen reagieren können.

Der Verarbeitungsprozess läuft systematisch ab: Angular parsed die Templates, identifiziert Direktiven anhand ihrer Selektoren, erstellt Instanzen der Direktiven-Klassen, injiziert Dependencies und führt die Direktiven-Logik aus. Diese Integration in den Change Detection-Zyklus macht Direktiven zu einem zentralen Werkzeug für dynamische, reaktive Benutzeroberflächen.

10.2 Drei Kategorien mit unterschiedlichen Rollen

Angular unterscheidet drei Direktiven-Typen, die jeweils spezifische Aufgaben erfüllen und unterschiedliche Aspekte der DOM-Manipulation adressieren.

Direktiven-Typ Erkennung Hauptzweck Beispiele
Komponenten Eigener Tag UI-Bausteine mit Template <app-hero>
Attribut-Direktiven Attribut ohne * Verhalten/Aussehen ändern ngClass, ngStyle
Struktur-Direktiven Attribut mit * oder @ DOM-Struktur manipulieren *ngIf, @for

Komponenten sind spezialisierte Direktiven mit eigenem Template und Styling. Sie bilden die Architektur der Anwendung. Attribut-Direktiven erweitern existierende Elemente ohne strukturelle Änderungen. Struktur-Direktiven manipulieren die DOM-Hierarchie durch Hinzufügen, Entfernen oder Anordnen von Elementen.

10.3 Attribut-Direktiven im Einsatz

Attribut-Direktiven werden als HTML-Attribute ohne Sternchen-Präfix verwendet. Sie ändern Aussehen oder Verhalten ihrer Host-Elemente, ohne die DOM-Struktur zu verändern. Angular bietet mehrere eingebaute Attribut-Direktiven für häufige Anwendungsfälle.

10.3.1 Dynamische CSS-Klassen mit ngClass

Die ngClass-Direktive verwaltet CSS-Klassen dynamisch basierend auf Ausdrücken. Sie akzeptiert verschiedene Eingabeformate, wobei die Objekt-Syntax am gebräuchlichsten ist:

<div [ngClass]="{'active': isActive, 'disabled': isDisabled, 'highlighted': isSelected}">
  Dynamischer Inhalt
</div>

Jeder Schlüssel im Objekt ist ein Klassenname, der zugehörige Wert ein boolescher Ausdruck. Angular fügt Klassen hinzu, deren Ausdruck true ergibt, und entfernt Klassen bei false. Die Array-Syntax eignet sich für dynamisch generierte Klassensammlungen:

<div [ngClass]="['base-class', userRole === 'admin' ? 'admin-highlight' : '']">
  Inhalt mit bedingten Klassen
</div>

Komplexere Logik gehört in Methoden:

export class StatusComponent {
  status: 'idle' | 'loading' | 'success' | 'error' = 'idle';
  
  getStatusClasses() {
    return {
      'status-indicator': true,
      'status-idle': this.status === 'idle',
      'status-loading': this.status === 'loading',
      'status-success': this.status === 'success',
      'status-error': this.status === 'error'
    };
  }
}

Im Template:

<div [ngClass]="getStatusClasses()">Status-Anzeige</div>

Diese Kapselung verbessert Testbarkeit und Wartbarkeit, besonders bei komplexen Bedingungen.

10.3.2 Inline-Styles mit ngStyle

Die ngStyle-Direktive wendet inline CSS-Styles dynamisch an:

<div [ngStyle]="{'color': textColor, 'font-size': fontSize + 'px', 'font-weight': isBold ? 'bold' : 'normal'}">
  Gestylter Text
</div>

Auch hier vereinfachen Methoden komplexe Stil-Logik:

export class ThemeComponent {
  isDarkTheme = false;
  baseFontSize = 16;
  fontScale = 1;
  
  getThemeStyles() {
    return {
      'color': this.isDarkTheme ? '#ffffff' : '#333333',
      'background-color': this.isDarkTheme ? '#333333' : '#ffffff',
      'font-size': `${this.baseFontSize * this.fontScale}px`,
      'padding': '12px',
      'border-radius': '8px'
    };
  }
}

Die Trennung von Stil-Logik und Template erhöht die Klarheit. Für produktive Anwendungen sind CSS-Klassen jedoch oft die bessere Wahl, da sie Caching durch den Browser ermöglichen und Performance-Vorteile bieten.

10.3.3 Two-Way Binding mit ngModel

Die ngModel-Direktive implementiert bidirektionale Datenbindung zwischen Formularelementen und Komponenten-Eigenschaften. Sie erfordert den Import des FormsModule:

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [FormsModule],
  template: `
    <input [(ngModel)]="username" name="username" required>
    <p>Eingegebener Wert: {{ username }}</p>
  `
})
export class UserFormComponent {
  username = '';
}

Die Syntax [(ngModel)] ist syntaktischer Zucker für eine Kombination aus Property Binding und Event Binding:

<input 
  [ngModel]="username" 
  (ngModelChange)="username = $event" 
  name="username">

Diese explizite Form ermöglicht Zwischenverarbeitung:

<input 
  [ngModel]="username" 
  (ngModelChange)="onUsernameChange($event)" 
  name="username">
onUsernameChange(value: string) {
  this.username = value.toLowerCase().trim();
}

Die Template-Referenz #varName="ngModel" gibt Zugriff auf den Validierungsstatus:

<input 
  [(ngModel)]="email" 
  name="email" 
  required 
  email
  #emailField="ngModel">

<div *ngIf="emailField.invalid && emailField.touched">
  <span *ngIf="emailField.errors?.['required']">E-Mail erforderlich</span>
  <span *ngIf="emailField.errors?.['email']">Ungültiges Format</span>
</div>

Angular’s Formular-Integration nutzt diese Mechanismen für komplexe Validierung und State-Management.

10.3.4 Eigene Attribut-Direktiven entwickeln

Eigene Attribut-Direktiven kapseln wiederverwendbares Verhalten. Eine Highlight-Direktive demonstriert die Grundlagen:

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

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  @Input('appHighlight') highlightColor = 'yellow';
  @Input() defaultColor = 'transparent';

  constructor(private el: ElementRef) {
    this.setColor(this.defaultColor);
  }

  @HostListener('mouseenter') onMouseEnter() {
    this.setColor(this.highlightColor);
  }

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

  private setColor(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

Der @HostListener-Dekorator bindet an DOM-Events des Host-Elements. ElementRef bietet direkten Zugriff auf das native DOM-Element. Die Verwendung ist intuitiv:

<p appHighlight="lightblue">Text mit Hover-Effekt</p>
<p [appHighlight]="dynamicColor" [defaultColor]="'lightgray'">Dynamisch hervorgehoben</p>

Für produktiven Code sollte Renderer2 statt direkter DOM-Manipulation verwendet werden:

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

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {}

  @HostListener('mouseenter')
  highlight() {
    this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', 'yellow');
  }

  @HostListener('mouseleave')
  reset() {
    this.renderer.removeStyle(this.el.nativeElement, 'backgroundColor');
  }
}

Renderer2 ist sicher für Server-Side Rendering und Web Workers, da es DOM-Operationen abstrahiert.

10.4 Struktur-Direktiven manipulieren das DOM

Struktur-Direktiven ändern die DOM-Hierarchie durch Hinzufügen, Entfernen oder Neuanordnen von Elementen. Traditionell werden sie mit einem Sternchen-Präfix gekennzeichnet, das Angular in <ng-template>-Konstrukte transformiert:

<div *ngIf="isVisible">Inhalt</div>

wird zu:

<ng-template [ngIf]="isVisible">
  <div>Inhalt</div>
</ng-template>

Diese Template-Syntax verleiht Struktur-Direktiven ihre Fähigkeit, Elemente vollständig aus dem DOM zu entfernen, nicht nur zu verstecken.

10.4.1 Bedingte Anzeige mit ngIf

Die ngIf-Direktive fügt Elemente bedingt zum DOM hinzu oder entfernt sie vollständig:

<div *ngIf="userLoggedIn">
  Willkommen zurück, {{ userName }}!
</div>

Der Unterschied zu [hidden] ist fundamental: ngIf entfernt das Element aus dem DOM, während hidden es nur visuell versteckt. Dies hat Performance-Implikationen bei komplexen UI-Elementen und Sicherheitsvorteile bei sensiblen Inhalten.

Die else-Syntax bietet Alternative:

<div *ngIf="userLoggedIn; else loginPrompt">
  Willkommen, {{ userName }}!
</div>

<ng-template #loginPrompt>
  <p>Bitte anmelden</p>
  <button (click)="login()">Login</button>
</ng-template>

Für komplexe Szenarien unterstützt ngIf sowohl then als auch else:

<div *ngIf="dataLoaded; then contentView else loadingView"></div>

<ng-template #contentView>
  <app-data-display [data]="data"></app-data-display>
</ng-template>

<ng-template #loadingView>
  <app-loading-spinner></app-loading-spinner>
</ng-template>

Diese Syntax separiert Zustandslogik von UI-Struktur.

10.4.2 Iteration mit ngFor

Die ngFor-Direktive iteriert über Arrays und rendert Templates für jedes Element:

<ul>
  <li *ngFor="let item of items; index as i">
    {{ i + 1 }}. {{ item.name }}
  </li>
</ul>

Angular stellt lokale Variablen während der Iteration bereit:

<div *ngFor="let item of items; 
             index as i; 
             first as isFirst; 
             last as isLast; 
             even as isEven; 
             odd as isOdd"
     [ngClass]="{'first-item': isFirst, 'last-item': isLast, 'stripe': isOdd}">
  {{ i }}. {{ item.name }}
</div>

Die trackBy-Funktion ist essentiell für Performance bei dynamischen Listen:

export class ListComponent {
  items: Item[] = [];
  
  trackByFn(index: number, item: Item): number {
    return item.id;
  }
}

Im Template:

<div *ngFor="let item of items; trackBy: trackByFn">
  {{ item.name }}
</div>

Ohne trackBy zerstört und erstellt Angular bei jeder Listenänderung alle DOM-Elemente neu. Mit trackBy identifiziert Angular unveränderte Elemente anhand ihrer ID und behält sie bei. Bei Listen mit Hunderten Elementen ist der Performance-Unterschied erheblich.

10.4.3 Mehrfachverzweigung mit ngSwitch

Die ngSwitch-Direktive bietet elegante Mehrfachverzweigung:

<div [ngSwitch]="userRole">
  <div *ngSwitchCase="'admin'">
    <h2>Admin Dashboard</h2>
    <p>Vollzugriff auf alle Funktionen</p>
  </div>
  <div *ngSwitchCase="'editor'">
    <h2>Editor-Werkzeuge</h2>
    <p>Inhalte bearbeiten und veröffentlichen</p>
  </div>
  <div *ngSwitchDefault>
    <h2>Benutzer-Ansicht</h2>
    <p>Standard-Benutzeroberfläche</p>
  </div>
</div>

Angular wertet den ngSwitch-Ausdruck aus und rendert nur den passenden Fall. Im Gegensatz zu mehreren ngIf-Direktiven kann ngSwitch immer nur einen Fall gleichzeitig anzeigen.

10.5 Modern Control Flow seit Angular 17

Angular 17 führte eine überarbeitete Syntax für Kontrollstrukturen ein. Die @-basierte Syntax ersetzt die Sternchen-Direktiven durch lesbarere, typsicherere Alternativen.

10.5.1 @if ersetzt *ngIf

Die neue Syntax ist direkter und unterstützt native else-if-Konstrukte:

@if (userLoggedIn) {
  <div>Willkommen, {{ userName }}!</div>
} @else {
  <p>Bitte anmelden</p>
  <button (click)="login()">Login</button>
}

Mehrfachbedingungen ohne verschachtelte Templates:

@if (userRole === 'admin') {
  <admin-dashboard />
} @else if (userRole === 'editor') {
  <editor-tools />
} @else {
  <user-view />
}

Die geschweiften Klammern machen Blockgrenzen explizit, die IDE-Integration ist besser, und TypeScript kann Typen effektiver prüfen.

10.5.2 @for ersetzt *ngFor

Die neue For-Syntax macht Tracking obligatorisch und bietet einen integrierten Empty-State:

<ul>
  @for (item of items; track item.id) {
    <li>{{ item.name }}</li>
  } @empty {
    <li class="no-data">Keine Einträge vorhanden</li>
  }
</ul>

Lokale Variablen nutzen Präfix-Syntax:

@for (item of items; track item.id; let i = $index, isOdd = $odd) {
  <div [class.highlight]="isOdd">
    {{ i + 1 }}. {{ item.name }}
  </div>
}

Verfügbare Variablen: $index, $first, $last, $even, $odd, $count. Das obligatorische track-Keyword erzwingt Best Practices von Beginn an.

10.5.3 @switch ersetzt ngSwitch

Die Switch-Syntax ist konsistent mit @if und @for:

@switch (userRole) {
  @case ('admin') {
    <h2>Admin Dashboard</h2>
  }
  @case ('editor') {
    <h2>Editor Werkzeuge</h2>
  }
  @default {
    <h2>Benutzer-Ansicht</h2>
  }
}

Die visuelle Strukturierung ist klarer als bei der Template-basierten Variante.

10.5.4 @defer für Progressive Loading

Die @defer-Direktive ermöglicht verzögertes Laden von Template-Teilen basierend auf verschiedenen Triggern:

@defer (on viewport) {
  <heavy-component />
} @loading {
  <p>Lade Komponente...</p>
} @error {
  <p>Fehler beim Laden</p>
}

Verfügbare Trigger:

Trigger Beschreibung Verwendung
on viewport Lädt bei Sichtbarkeit Unterhalb des Folds
on interaction Lädt bei Interaktion Click-to-Load
on hover Lädt beim Hovern Hover-to-Load
on idle Lädt im Browser-Leerlauf Nicht-kritische UI
on timer(ms) Lädt nach Timeout Verzögerte Anzeige
when condition Lädt bei Bedingung Bedingte Features

Ein praktisches Beispiel zeigt die Strategie:

<article class="product-detail">
  <!-- Kritischer Content: sofort laden -->
  <header>
    <h1>{{ product.name }}</h1>
    <p class="price">{{ product.price | currency }}</p>
    <button (click)="addToCart()">In den Warenkorb</button>
  </header>

  <!-- Detailinfos: bei Sichtbarkeit laden -->
  @defer (on viewport) {
    <section class="details">
      <app-product-specs [specs]="product.specs"></app-product-specs>
    </section>
  } @loading {
    <p>Lade Details...</p>
  }

  <!-- Bewertungen: bei Interaktion laden -->
  @defer (on interaction(reviewBtn)) {
    <section class="reviews">
      <app-reviews [productId]="product.id"></app-reviews>
    </section>
  } @placeholder {
    <button #reviewBtn>Bewertungen anzeigen</button>
  }

  <!-- Empfehlungen: im Leerlauf laden -->
  @defer (on idle) {
    <app-recommendations [productId]="product.id"></app-recommendations>
  }
</article>

Diese Strategie priorisiert kritischen Content, lädt Details bei Bedarf und nutzt Browser-Leerlauf für nicht-essentielle Features. Die Initial Load Time sinkt drastisch.

10.6 Eigene Struktur-Direktiven erstellen

Eigene Struktur-Direktiven erfordern TemplateRef und ViewContainerRef. Eine Unless-Direktive als Gegenstück zu ngIf:

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

@Directive({
  selector: '[appUnless]',
  standalone: true
})
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;
    }
  }
}

Verwendung:

<div *appUnless="isHidden">
  Nur sichtbar wenn isHidden false ist
</div>

Der TemplateRef repräsentiert das Template, ViewContainerRef den Container, in den das Template gerendert wird. Die createEmbeddedView-Methode fügt das Template hinzu, clear() entfernt alle Views.

Eine komplexere Repeat-Direktive zeigt erweiterte Möglichkeiten:

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

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

  @Input() set appRepeat(count: number) {
    this.viewContainer.clear();
    
    for (let i = 0; i < count; i++) {
      this.viewContainer.createEmbeddedView(this.templateRef, {
        $implicit: i,
        index: i,
        count: count,
        first: i === 0,
        last: i === count - 1
      });
    }
  }
}

Die Verwendung mit Context-Variablen:

<div *appRepeat="5; let i; let isFirst = first; let isLast = last">
  Item {{ i + 1 }} 
  <span *ngIf="isFirst">(Erster)</span>
  <span *ngIf="isLast">(Letzter)</span>
</div>

Das Context-Objekt ermöglicht Datenübergabe vom Template zur Direktive. Die $implicit-Eigenschaft bindet an die let-Variable ohne explizite Zuweisung.

10.7 Signalbasierte Direktiven

Angular Signals bieten feinkörnigere Reaktivität als traditionelle Change Detection. Direktiven können Signals nutzen für effizientere Updates:

import { Directive, ElementRef, signal, computed, input, effect } from '@angular/core';

@Directive({
  selector: '[appResizable]',
  standalone: true,
  host: {
    '[style.resize]': '"both"',
    '[style.overflow]': '"auto"',
    '[style.min-width.px]': 'minWidth()',
    '[style.min-height.px]': 'minHeight()'
  }
})
export class ResizableDirective {
  minWidth = input<number>(100);
  minHeight = input<number>(100);
  
  private width = signal(0);
  private height = signal(0);
  
  currentSize = computed(() => ({
    width: this.width(),
    height: this.height()
  }));

  constructor(private el: ElementRef) {
    effect(() => {
      const size = this.currentSize();
      console.log(`Neue Größe: ${size.width}x${size.height}`);
    });
  }
}

Signal-basierte Inputs mit input(), interne Signals mit signal(), berechnete Werte mit computed() und Reaktionen mit effect(). Angular aktualisiert nur die tatsächlich betroffenen Teile der Anwendung, nicht den gesamten Komponentenbaum.

Die host-Eigenschaft im Dekorator bindet Direktiven-Properties direkt an Host-Attribute und -Styles ohne @HostBinding:

@Directive({
  selector: '[appTooltip]',
  standalone: true,
  host: {
    '(mouseenter)': 'show()',
    '(mouseleave)': 'hide()',
    '[class.has-tooltip]': 'true',
    '[attr.aria-describedby]': 'tooltipId()'
  }
})
export class TooltipDirective {
  tooltipId = signal('tooltip-' + Math.random().toString(36).substr(2, 9));
  
  show() {
    console.log('Tooltip anzeigen');
  }
  
  hide() {
    console.log('Tooltip verstecken');
  }
}

Diese deklarative Syntax ist kompakter als mehrere @HostListener und @HostBinding Dekoratoren.

10.8 Integration mit RxJS

Direktiven können RxJS-Streams für komplexe asynchrone Logik nutzen:

import { Directive, ElementRef, Output, EventEmitter, inject } from '@angular/core';
import { fromEvent } from 'rxjs';
import { throttleTime, map, pairwise, filter } from 'rxjs/operators';

interface ScrollInfo {
  direction: 'up' | 'down';
  position: number;
  delta: number;
}

@Directive({
  selector: '[appScrollObserve]',
  standalone: true
})
export class ScrollObserveDirective {
  private el = inject(ElementRef);
  
  @Output() scrollChange = new EventEmitter<ScrollInfo>();

  constructor() {
    fromEvent(this.el.nativeElement, 'scroll')
      .pipe(
        throttleTime(100),
        map(() => this.el.nativeElement.scrollTop),
        pairwise(),
        map(([prev, curr]) => ({
          direction: prev < curr ? 'down' as const : 'up' as const,
          position: curr,
          delta: Math.abs(curr - prev)
        })),
        filter(info => info.delta > 0)
      )
      .subscribe(info => this.scrollChange.emit(info));
  }
}

Der RxJS-Stream throttled Scroll-Events, berechnet Position und Richtung, filtert insignifikante Änderungen und emittiert strukturierte Daten. Die Verwendung:

<div class="content" appScrollObserve (scrollChange)="onScroll($event)">
  <!-- Scrollbarer Inhalt -->
</div>
onScroll(info: ScrollInfo) {
  if (info.direction === 'down' && info.position > 300) {
    this.hideHeader();
  } else if (info.direction === 'up') {
    this.showHeader();
  }
}

Diese Komposition aus Direktiven und RxJS ermöglicht deklarative, wiederverwendbare Event-Handling-Logik.