Angular-Konzepte in diesem Projekt (Nachschlagewerk)
Zielgruppe: Entwickler:in, die ein bestimmtes Angular-Konzept schnell an diesem Code nachschlagen will. Der Frontend-Lernpfad geht dieselben Themen in Lese-Reihenfolge mit Übungen durch - dieses Dokument ist die alphabetisch/thematisch sortierte Referenz. Jeder Abschnitt: was es ist, wo im Projekt, klassischer Angular-Weg zum Vergleich. Wenn dein „klassischer Angular-Weg" noch aus v8-Zeiten stammt: der Exkurs: Angular 8 → 22 erzählt die Entwicklung narrativ und mit dem Warum.
Auf einen Blick
| Konzept | In diesem Projekt | Klassischer / älterer Weg |
|---|---|---|
| App-Struktur | Standalone Components, kein NgModule |
AppModule + Feature-NgModules |
| Bootstrap | bootstrapApplication(AppComponent, appConfig) |
platformBrowserDynamic().bootstrapModule(AppModule) |
| Provider-Konfiguration | ApplicationConfig mit provideXxx()-Funktionen |
@NgModule({ providers, imports }) |
| App-Initialisierung | provideAppInitializer(() => inject(...)...) |
APP_INITIALIZER-Multi-Provider mit deps |
| Dependency Injection | inject(Service) als Feld-Initialisierer |
Konstruktor-Parameter mit private |
| Zustandsverwaltung | Angular Signals (Standard) / NgRx (nur catalog-admin) |
RxJS BehaviorSubject + async-Pipe |
| Routing | flache Routes mit loadComponent |
loadChildren auf Feature-Module |
| Route Guards | funktionale CanActivateFn (+ Guard-Factory) |
@Injectable implements CanActivate |
| HTTP-Interceptor | funktionale HttpInterceptorFn |
@Injectable implements HttpInterceptor |
| Templates - Bedingungen/Schleifen | @if / @else if / @for |
*ngIf / *ngFor (+ CommonModule) |
| Formulare | Reactive Forms, nonNullable |
Template-driven ([(ngModel)]) |
| Change Detection | zonen-basiert, eventCoalescing: true |
zonen-basiert (Default), Zoneless (neuer) |
| UI-Komponenten | Angular Material 22 | - |
| Auth | angular-oauth2-oidc, Code Flow + PKCE |
- |
Standalone Components
Was: Eine Komponente deklariert ihre Template-Abhängigkeiten selbst im imports-Array des
@Component-Decorators. Es gibt kein NgModule, das Komponenten "bündelt und bereitstellt".
Wo: Jede Komponente. Beispiel catalog-search.component.ts:
@Component({
selector: 'app-catalog-search',
imports: [ReactiveFormsModule, MatFormFieldModule, MatInputModule, /* ... */],
templateUrl: './catalog-search.component.html',
})
export class CatalogSearchComponent { }
Klassischer Weg: Ein CatalogModule mit declarations: [CatalogSearchComponent],
imports: [CommonModule, ReactiveFormsModule, ...], exports: [...]. Die Standalone-Variante
spart diese Indirektion. Bis Angular 18 musste standalone: true im Decorator stehen; ab
Angular 19 ist standalone der Default - beim Update auf v19 hat eine Migrations-Schematic die
Angabe aus allen Komponenten dieses Projekts entfernt (siehe
angular-update-migration.md).
Bootstrap und ApplicationConfig
Was: main.ts ruft bootstrapApplication(RootComponent, config). Die config ist ein
Objekt { providers: [...] } - die einzige Stelle für app-weite Provider.
Wo: src/main.ts (5 Zeilen), src/app/app.config.ts (die Provider-Liste).
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideAnimations(),
provideHttpClient(withInterceptors([authInterceptor])),
importProvidersFrom(OAuthModule.forRoot()),
provideStore({ catalogAdmin: catalogAdminReducer }),
provideEffects([CatalogAdminEffects]),
provideStoreDevtools({ maxAge: 25, logOnly: !isDevMode() }),
provideAppInitializer(() => inject(AuthService).initialize()),
],
};
Detail importProvidersFrom(OAuthModule.forRoot()): angular-oauth2-oidc bietet (noch) nur
eine NgModule-API. importProvidersFrom ist die Brücke, um deren Provider in eine
Standalone-ApplicationConfig zu ziehen.
Detail provideAppInitializer: Nimmt eine Funktion, die (optional) ein Promise zurückgibt.
Angular verzögert den Bootstrap (und damit die erste Router-Navigation), bis das Promise auflöst.
Die Funktion läuft im Injection-Kontext - daher inject(AuthService) direkt darin, ohne
deps-Liste. Hier: der Router darf keine Route aktivieren, bevor AuthService.initialize() den
Login-Zustand aus Keycloak
geladen hat.
Dependency Injection mit inject()
Was: inject(Token) holt eine Abhängigkeit aus dem DI-Container. Funktioniert überall in
einem "Injection Context": Feld-Initialisierer, Konstruktor, provideX-Factories, funktionale
Guards/Interceptors, provideAppInitializer-Callbacks, runInInjectionContext.
Wo: Durchgängig. catalog.service.ts: private readonly http = inject(HttpClient);.
auth.interceptor.ts: const authService = inject(AuthService); - hier geht der
Konstruktor-Weg gar nicht, weil der Interceptor eine Funktion ist, keine Klasse. Ebenso
app.config.ts: provideAppInitializer(() => inject(AuthService).initialize()).
Klassischer Weg: constructor(private http: HttpClient) {}. Weiterhin gültig; inject() ist
kompakter und die einzige Option in funktionalen Bausteinen. Vergleiche mit der
Backend-Coding-Guideline "Konstruktor-Injection statt Feld-Injection" - im Frontend ist
inject() als Feld-Initialisierer der Normalfall, weil es dort keine Reflection-basierte
Feld-Injection wie bei Spring gibt (die Abhängigkeit ist sichtbar und readonly).
Signals
Was: Ein signal(initialValue) ist ein Container für einen Wert, den Leser automatisch
"abonnieren". sig() liest, sig.set(v) / sig.update(fn) schreibt. Ein Lesezugriff im
Template registriert die Komponente als abhängig - ändert sich das Signal, rendert Angular neu.
Wo: Jeder Feature-Service außer catalog-admin. Muster:
readonly books = signal<BookResponse[]>([]);
readonly loading = signal(false);
readonly error = signal<string | null>(null);
auth.service.ts gibt seinen ganzen Zustand als Signals nach außen (isLoggedIn, roles,
username), obwohl die Quelle darunter ein RxJS-Observable (oauthService.events) ist - der
Service übersetzt Observable → Signal.
Verwandt:
computed(() => ...)- abgeleiteter, gecachter Signal. Wird hier noch nicht genutzt; ein guter Kandidat wäreoverdueCount = computed(() => this.loans().filter(l => l.overdue).length).NgRxgibt dir überstore.selectSignal(selector)ebenfalls einen Signal zurück - siehecatalog-admin.component.ts. Signals sind also nicht das Gegenteil von NgRx, sondern die gemeinsame Lese-Schnittstelle.
Klassischer Weg: readonly books$ = new BehaviorSubject<BookResponse[]>([]) + im Template
books$ | async. Nachteil: async-Pipe-Boilerplate, Nullable-Werte beim ersten Tick,
Verschachtelung bei mehreren Streams.
Templates: @if, @for, Control Flow
Was: Seit Angular 17 gibt es eingebaute Block-Syntax für Kontrollfluss - keine
Struktur-Direktiven, kein CommonModule-Import.
Wo: app.component.html:
@if (authService.isLoggedIn()) {
<router-outlet />
} @else {
<div class="login-hint"> ... </div>
}
catalog-search.component.html nutzt die dreistufige Kette
@if (loading()) { ... } @else if (books().length === 0) { ... } @else { <table> ... }.
Klassischer Weg: *ngIf="isLoggedIn(); else loginHint" mit <ng-template #loginHint>, und
*ngFor="let x of items()". Funktioniert weiter, braucht aber CommonModule in imports und
ist bei else/elseif deutlich umständlicher.
Noch nicht genutzt, aber verwandt: @for (item of items(); track item.id) { } (das track
ist Pflicht), @switch, @defer (verzögertes Laden von Template-Teilen).
Routing und Lazy Loading
Was: Routes ist ein flaches Array. loadComponent lädt pro Route ein eigenes JS-Chunk.
Wo: app.routes.ts:
{
path: 'admin/books',
canActivate: [authGuard, roleGuard(['LIBRARY_ADMIN', 'LIBRARIAN'])],
loadComponent: () => import('./features/catalog-admin/catalog-admin.component')
.then((m) => m.CatalogAdminComponent),
title: 'Bestand verwalten',
}
title wird von Angulars TitleStrategy automatisch in den document.title geschrieben.
Fallback-Routen am Ende: { path: '', redirectTo: 'catalog' }, { path: '**', redirectTo: 'catalog' }.
Klassischer Weg: loadChildren: () => import('./catalog/catalog.module').then(m => m.CatalogModule) - das Feature-Modul brachte seine eigene RouterModule.forChild([...])-Tabelle
mit. Mit Standalone Components entfällt diese zweite Ebene.
Route Guards (funktional + Guard-Factory)
Was: Ein Guard ist eine Funktion CanActivateFn, die true, false oder ein UrlTree
(Redirect) zurückgibt.
Wo: role.guard.ts:
export function roleGuard(requiredRoles: string[]): CanActivateFn {
return () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.hasAnyRole(...requiredRoles)) return true;
return router.createUrlTree(['/']);
};
}
export const authGuard: CanActivateFn = () => inject(AuthService).isLoggedIn();
roleGuard(...) ist eine Factory - sie erzeugt eine passende CanActivateFn pro
Rollen-Liste, statt eine Guard-Klasse je Kombination zu brauchen.
Klassischer Weg: @Injectable({providedIn:'root'}) class RoleGuard implements CanActivate { canActivate(route) {...} } - eine parametrisierte Guard-Klasse ging nur über route.data und war
sperriger.
Wichtig: Beide Guards sind UX, keine Sicherheit - siehe das Javadoc in der Datei und den
Frontend-Lernpfad, Etappe 6.
Autorisierung erzwingt das Backend (@PreAuthorize).
HTTP-Client und Interceptor
Was: provideHttpClient(withInterceptors([fn])) registriert funktionale Interceptors. Ein
HttpInterceptorFn bekommt (req, next) und gibt next(req) zurück - HttpRequest ist
immutable, Änderungen über req.clone(...).
Wo: auth.interceptor.ts:
export const authInterceptor: HttpInterceptorFn = (request, next) => {
const authService = inject(AuthService);
if (!request.url.startsWith(API_BASE_URL)) return next(request);
const token = authService.getAccessToken();
if (!token) return next(request);
return next(request.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};
Spiegelbild zum Backend: lending-service hat einen TokenRelayFeignInterceptor, der das Token
von Service zu Service weiterreicht - hier wird es ursprünglich angehängt.
Klassischer Weg: @Injectable() class AuthInterceptor implements HttpInterceptor, registriert
über { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }.
HTTP-Aufrufe selbst: catalog.service.ts zeigt HttpParams (Query-String bauen) und
getippte Antworten this.http.get<PageResponse<BookResponse>>(url, { params }). Die
Signal-Services rufen .subscribe(...) direkt; die NgRx-Effects nutzen den Stream mit
map/catchError-Operatoren.
Reactive Forms
Was: Der Formularzustand lebt als FormGroup/FormControl-Objekt im TypeScript-Code. Das
Template bindet nur über [formGroup] und formControlName.
Wo: members-admin.component.ts (das reichhaltigste Beispiel):
protected readonly registerForm = this.formBuilder.nonNullable.group({
firstName: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
tier: ['STANDARD' as (typeof this.tiers)[number], Validators.required],
});
formBuilder.nonNullable→reset()setzt auf die Initialwerte, Typen sind nicht... | null.form.getRawValue()→ getipptes Wert-Objekt (auch deaktivierte Controls).- Template:
[disabled]="form.invalid"am Submit-Button,(ngSubmit)="save()"am<form>.
Klassischer Weg: Template-driven mit [(ngModel)] und #form="ngForm". Kürzer zu tippen,
aber der Zustand ist implizit im DOM und schwerer isoliert zu testen. Dieses Projekt nutzt
Template-driven nie.
Change Detection
Was: provideZoneChangeDetection({ eventCoalescing: true }) in app.config.ts. Angular
nutzt hier weiterhin zone.js (package.json: zone.js ~0.16), um DOM-Events abzufangen und
danach neu zu rendern. eventCoalescing fasst mehrere Events im selben Tick zu einem
Render-Durchlauf zusammen.
Relevanz für dich: Weil der Zustand über Signals läuft, ist Change Detection meist kein
Thema - ein signal.set() markiert die abhängigen Komponenten präzise. ChangeDetectionStrategy. OnPush wird hier nicht explizit gesetzt (mit reiner Signal-Nutzung wäre es der logische nächste
Schritt und eine sinnvolle Übung).
Neuer Weg (nicht in diesem Projekt): Zoneless Change Detection - provideZonelessChangeDetection()
(in Angular 18 noch provideExperimentalZonelessChangeDetection, ab v20 stabilisiert und
umbenannt). Kommt ohne zone.js aus und verlässt sich ganz auf Signals. Dieses Projekt bleibt
bewusst beim etablierten zonen-basierten Modell - die Umstellung ist ein eigenes Vorhaben, kein
Update-Nebeneffekt.
Angular Material
Was: @angular/material + @angular/cdk (beide ^22.1). Fertige UI-Komponenten:
MatToolbar, MatTable, MatFormField, MatInput, MatButton, MatChips, MatSelect,
MatProgressSpinner, MatCheckbox.
Wo: Jede Komponente importiert die Mat*Module, die ihr Template braucht. Die Tabelle in
catalog-search.component.html zeigt das mat-table-Muster mit matColumnDef/matHeaderCellDef/
matCellDef und displayedColumns (als Feld in der Komponente).
provideAnimations() in app.config.ts ist Voraussetzung für Material-Übergänge
(Ripple-Effekte, Spinner, Menü-Animationen).
Auth: OIDC Authorization Code Flow + PKCE
Was: angular-oauth2-oidc (^19.0.0). Der Browser ist ein "public client" ohne
Client-Secret; PKCE sichert den Code-Austausch ab.
Wo:
auth.config.ts-AuthConfig:issuer,clientId: 'library-frontend',responseType: 'code',scope: 'openid profile email',requireHttps: false(nur lokal!).auth.service.ts-initialize()ruftloadDiscoveryDocumentAndTryLogin()+setupAutomaticSilentRefresh().login()→initCodeFlow(). Rollen aus dem Access-Token, Claimrealm_access.roles(identisch zum BackendKeycloakRealmRoleConverter).app.config.ts-provideAppInitializer(...)hält den Bootstrap bisinitialize()fertig ist.auth.interceptor.ts- hängt das Token an Requests ansapi-gateway.
Siehe auch: Glossar ("Authorization Code Flow + PKCE"), ADR-0004 (Token-Lebensdauer), ADR-0005 (Resource-Server-Setup Backend).
Was dieses Projekt bewusst NICHT nutzt
| Nicht genutzt | Stattdessen | Grund |
|---|---|---|
NgModule |
Standalone Components | Weniger Indirektion, ab v19 Angular-Default |
| Template-driven Forms | Reactive Forms | Testbarer, expliziter Zustand |
*ngIf / *ngFor |
@if / @for |
Kein CommonModule, bessere else-Syntax |
| Klassenbasierte Guards/Interceptors | funktionale Variante | Kürzer, inject()-fähig |
APP_INITIALIZER-Multi-Provider |
provideAppInitializer() |
Kürzer, inject()-fähig (ab v19) |
| NgRx überall | Signals als Standard | NgRx nur in catalog-admin als Lern-Kontrast |
| Zoneless CD | zonen-basierte CD | bewusst konservativ, eigene Umstellung wert |
| Eigenes State-Framework | Signals / NgRx | Kein "Framework im Framework" |
| Frontend-eigenes Datenmodell | *.model.ts spiegelt Backend-DTOs 1:1 |
Keine Übersetzungsschicht, eine Quelle der Wahrheit |