Exkurs: Angular 8 → 22 für Rückkehrer:innen

Zielgruppe: Entwickler:in, die vor einigen Jahren mit Angular ~8 (2019) an einem echten Projekt gearbeitet hat und jetzt auf Angular 22 (2026) trifft. Vieles ist verblasst, und zwischen v8 und v22 liegen 14 Major-Releases. Dieses Dokument liest sich nicht wie ein Changelog - es ordnet die Änderungen nach Konzept-Verschiebung und verankert jede an einer konkreten Datei in diesem Repository (das selbst eine moderne Angular-22-App ist).

Wie dieser Exkurs aufgebaut ist

  1. Die drei großen Verschiebungen - wenn du nur einen Abschnitt liest, diesen.
  2. Was gleich geblieben ist - damit klar ist, wie viel dein altes Wissen noch trägt.
  3. Übersetzungstabelle v8 → v22 - Idiom für Idiom, mit Repo-Fundstelle.
  4. Die Verschiebungen im Detail - je ein Abschnitt: damals / heute / warum / hier im Repo - und jeweils eine „Selber machen"-Übung am laufenden Code (mach es erst nach deinem v8-Reflex, dann den v22-Weg). Voraussetzung dafür: cd library-frontend && npm install, Backend siehe Frontend-Lernpfad.
  5. Zeitleiste - welche Version brachte was (zum Einordnen).
  6. Der 30-Minuten-Wiedereinstieg - Dateien in Lese-Reihenfolge.
  7. Was du wahrscheinlich noch nie gesehen hast - das echt Neue ohne v8-Entsprechung.

Die Übungen sind zum Ausprobieren gedacht, nicht zum Behalten - mach sie mit git stash / git checkout -- . rückgängig, wenn du fertig bist. Nichts davon ist im Repo committet.

Sitzt du an einem Projekt, das noch auf Angular 18 steht? Dann ist Exkurs: Angular 8 → 18 die passendere Variante - gleiche Struktur, aber auf den v18-Stand zugeschnitten (was bei v18 gilt, was erst v19–v22 brachten).


1. Die drei großen Verschiebungen

a) Von NgModule zu Standalone

Damals (v8): Alles lebte in @NgModule. Eine Komponente war erst benutzbar, wenn sie in declarations eines Moduls stand; ein Feature bekam ein eigenes FeatureModule; es gab SharedModule, CoreModule, declarations vs. imports vs. exports vs. entryComponents - und die ewige Frage „warum sieht meine Komponente die Direktive nicht?".

Heute (v22): Kein einziges NgModule. Eine Komponente deklariert ihre Abhängigkeiten selbst: @Component({ imports: [ReactiveFormsModule, MatButtonModule] }). Der App-Start ist bootstrapApplication(AppComponent, appConfig); app-weite Provider stehen in einer ApplicationConfig als provideRouter(...), provideHttpClient(...) usw. Seit v19 ist standalone der Default und muss nicht mehr hingeschrieben werden.

Warum: NgModules waren Angulars Antwort auf „wie finden sich Komponenten gegenseitig", wurden aber zur Boilerplate-Schicht mit verwirrender Semantik. Standalone macht Abhängigkeiten pro Komponente explizit und lokal.

→ Repo: jede *.component.ts, src/main.ts, src/app/app.config.ts.

b) Von „RxJS für alles" zu Signals

Damals (v8): Zustand = BehaviorSubject. Template = value$ | async. Und überall das takeUntil(this.destroy$) + ngOnDestroy-Ritual, um Subscriptions aufzuräumen. Change Detection lief über zone.js: bei jedem Event wurde der ganze Komponentenbaum geprüft.

Heute (v22): Signals. books = signal<Book[]>([]), im Template books(). Kein async-Pipe, kein manuelles Subscriben, kein OnDestroy für Zustand. computed() für abgeleitete Werte, effect() für Seiteneffekte. RxJS ist nicht weg - es bleibt für echte Ereignis-Ströme (HTTP-Antworten, WebSockets, Debounce-Suchen), und toSignal()/toObservable() überbrücken beide Welten.

Warum: Der async-Pipe-Boilerplate und das Aufräum-Ritual waren fehleranfällig, und die zonen-basierte „check everything"-Change-Detection skalierte schlecht. Signals geben dem Framework präzise Auskunft, was sich geändert hat.

→ Repo: catalog.service.ts (reine Signals), auth.service.ts (Observable → Signal übersetzt), features/catalog-admin/ (NgRx, liest via store.selectSignal(...)).

c) Von Struktur-Direktiven zu eingebautem Control Flow

Damals (v8): *ngIf="user; else loginTpl", *ngFor="let x of items; trackBy: trackFn", <ng-template #loginTpl>, [ngSwitch]. Brauchte CommonModule im Modul-imports.

Heute (v22): Eingebaute Blocksyntax, kein Import nötig:

@if (authService.isLoggedIn()) { <router-outlet /> } @else { <app-login-hint /> }

@for (book of books(); track book.id) { <tr>…</tr> } @empty { <p>Nichts gefunden.</p> }

track ist Pflicht (kein trackBy-Funktionsname mehr, direkt der Ausdruck). Neu dazu: @switch, @defer (verzögertes Nachladen von Template-Teilen), @let (lokale Template-Variable).

Warum: Die *-Direktiven waren ein ng-template-Trick mit sperriger else-Syntax, und der Compiler konnte sie schlecht optimieren. Die Blocksyntax ist lesbarer, schneller und tree-shakebar.

→ Repo: app.component.html, catalog-search.component.html, my-loans.component.html.

Unsichtbar, aber fundamental: Zwischen v8 und v13 wurde der Compiler komplett getauscht (View Engine → Ivy) und zwischen v13 und v17 das Build-System (Webpack → esbuild/Vite). Beides merkst du im Alltag nur an der Geschwindigkeit und an besseren Fehlermeldungen - es ändert nichts an deinem Code.


2. Was gleich geblieben ist (Entwarnung)

Konzept Stand v8 = Stand v22
Komponenten, Templates, Services, Pipes, Direktiven dieselbe Idee, dieselben Decorators @Component / @Injectable / @Pipe
Dependency Injection hierarchische Injektoren, providedIn: 'root', Provider-Konzept - unverändert, nur die Schreibweise am Abrufort (inject() statt Konstruktor)
RxJS weiterhin dabei und wichtig für Ströme - nur nicht mehr für jeden Zustand
TypeScript dieselbe Sprache (v8: ~3.4, v22: 6.0), striktere Defaults
Angular CLI ng new / ng generate / ng build / ng serve / ng test / ng update - alles noch da
Angular Material / CDK dieselbe Bibliothek (die Komponenten-Implementierung wurde intern auf „MDC" umgestellt, die API blieb weitgehend)
Router-Grundidee Routentabelle, <router-outlet>, Lazy Loading, Guards - Konzept identisch, Schreibweise modernisiert
Reactive Forms & Template-driven Forms beide noch da; Reactive Forms sind jetzt typisiert
HttpClient dieselbe API (get/post/…), nur die Registrierung und Interceptors änderten sich

Dein v8-Wissen ist also zu ~60 % direkt gültig. Was du neu lernst, ist vor allem Schreibweise und ein neues Konzept (Signals).


3. Übersetzungstabelle v8 → v22

Aufgabe Angular 8 Angular 22 Repo-Fundstelle
App starten platformBrowserDynamic().bootstrapModule(AppModule) bootstrapApplication(AppComponent, appConfig) src/main.ts
App-weite Provider @NgModule({ providers, imports }) ApplicationConfig mit provideX() app.config.ts
Komponente „bekannt machen" in declarations eines Moduls @Component({ imports: [...] }) bzw. gar nichts (Route-Component) jede Komponente
Abhängigkeit holen constructor(private http: HttpClient) {} private http = inject(HttpClient) jeder Service
lokaler Zustand x$ = new BehaviorSubject(0) + x$ | async x = signal(0) + x() catalog.service.ts
abgeleiteter Wert combineLatest([...]).pipe(map(...)) computed(() => …) (noch nicht genutzt; Kandidat)
Seiteneffekt auf Wertänderung manuelles subscribe effect(() => …) -
Subscription aufräumen takeUntil(this.destroy$) + ngOnDestroy takeUntilDestroyed() / DestroyRef (oder: kein Subscribe dank Signals) -
Bedingung im Template *ngIf="x; else tpl" @if (x) { } @else { } app.component.html
Schleife im Template *ngFor="let i of items; trackBy: fn" @for (i of items(); track i.id) { } catalog-search.component.html
verzögert laden manuell / gar nicht @defer (on viewport) { } (Übung im Lernpfad)
Lazy-Route loadChildren: './x/x.module#XModule' (später () => import()) loadComponent: () => import('...').then(m => m.X) app.routes.ts
Route Guard @Injectable class G implements CanActivate const g: CanActivateFn = () => { … } (Funktion, inject()-fähig) role.guard.ts
HTTP aktivieren imports: [HttpClientModule] provideHttpClient(withInterceptors([...])) (v22-Default: fetch; dieses Repo hält per withXhr() das alte Verhalten) app.config.ts
HTTP-Interceptor { provide: HTTP_INTERCEPTORS, useClass: …, multi: true } withInterceptors([authInterceptor]) (Funktion) auth.interceptor.ts
App-Init vor Bootstrap APP_INITIALIZER-Multi-Provider mit deps provideAppInitializer(() => inject(X).init()) app.config.ts
@Input() / @Output() @Input() x; @Output() y = new EventEmitter() x = input<T>(); y = output<T>() (Signal-basiert) (Route-Components haben keine; für dein nächstes Projekt)
@ViewChild @ViewChild('r') r: ElementRef r = viewChild<ElementRef>('r') (Signal) -
Umgebungskonfiguration environment.ts / environment.prod.ts + fileReplacements weiterhin möglich; oft --define / einfache Konstanten core/api/api-config.ts (bewusst simpel)
Unit-Test-Runner Karma + Jasmine Karma (deprecated) → Vitest-basierter Runner (neu) app.component.spec.ts
E2E-Test Protractor Playwright / Cypress / WebdriverIO e2e/golden-path.spec.ts
SSR Angular Universal (separat, renderModuleFactory) provideClientHydration() + @angular/ssr, in ng new --ssr eingebaut (nicht genutzt, reine SPA)

4. Die Verschiebungen im Detail

NgModule → Standalone

Dependency Injection: Konstruktor → inject()

Reaktivität: RxJS → Signals

Templates: Control Flow, und was mit CommonModule passierte

Routing

HTTP

Component-API: Decorators → Signal-Primitive

Für dein nächstes Projekt relevanter als für dieses Repo (die Route-Components hier haben keine Inputs/Outputs):

v8 v22
@Input() value: string value = input<string>() bzw. input.required<string>()
@Input() set value(v) {…} value = input<string>(); constructor() { effect(() => useValue(this.value())); }
@Output() change = new EventEmitter<T>() change = output<T>()
@Input() value; @Output() valueChange (Banana-in-a-box) value = model<T>()
@ViewChild('ref') ref / @ViewChildren ref = viewChild('ref') / viewChildren('ref') (Signals)
@HostBinding('class.active') isActive host: { '[class.active]': 'isActive()' } im @Component
ngOnChanges oft ein effect() auf den input()-Signal
ngAfterViewInit für DOM-Zugriff afterNextRender(() => …) / afterRenderEffect(...)

Lifecycle-Hooks (ngOnInit, ngOnDestroy, …) gibt es weiterhin, werden aber seltener gebraucht.

Selber machen (v8-Reflex → v22-Weg): Die Route-Components hier haben keine Inputs/Outputs - bau also eine kleine Präsentationskomponente. ng g c shared/status-chip, dann: readonly status = input.required<string>(); und readonly retry = output<void>();, im Template <mat-chip>{{ status() }}</mat-chip> <button mat-button (click)="retry.emit()">↻</button>. In my-loans.component.html einsetzen: <app-status-chip [status]="loan.status" (retry)="renew(loan.id)" />. Vergleiche mit deinem v8-Reflex (@Input() status: string; @Output() retry = new EventEmitter<void>();)

Change Detection

Build & Tooling

Testing


5. Zeitleiste - welche Version brachte was

Zum Einordnen. Die genaue Minor-Version ist weniger wichtig als der Bogen; maßgeblich im Zweifel: https://update.angular.dev und der angular/angular-CHANGELOG.

Version (≈ Datum) Für Rückkehrer:innen wichtig
8 (Mai 2019) dein Ausgangspunkt: NgModules, View Engine, loadChildren-String → Import-Funktion, differential loading
9 (Feb 2020) Ivy wird Default-Compiler (kleinere Bundles, bessere Fehler)
10 (Jun 2020) ng new --strict, Warnungen bei CommonJS-Abhängigkeiten
11 (Nov 2020) schnellere Builds, TSLint → ESLint-Migration beginnt, HMR-Support
12 (Mai 2021) „Ivy Everywhere": View Engine deprecated, Produktions-Build als Default, ?./?? in Templates
13 (Nov 2021) View Engine entfernt, IE11-Support raus, keine ComponentFactory mehr nötig, Persistent Build Cache
14 (Jun 2022) Standalone Components (Preview), typisierte Reactive Forms, inject(), provide*-Funktionen, Page-Title-Strategy
15 (Nov 2022) Standalone stabil, funktionale Guards, NgOptimizedImage stabil, Directive Composition API, Material auf „MDC"
16 (Mai 2023) Signals (Developer Preview), takeUntilDestroyed / DestroyRef, SSR-Hydration (Preview), esbuild-Dev-Server, required inputs, self-closing tags
17 (Nov 2023) neuer Control Flow @if/@for/@switch + @defer (stabil), esbuild/Vite Application-Builder als Default, afterRender/afterNextRender, neue Doku auf angular.dev, Standalone als ng new-Default
18 (Mai 2024) Zoneless (experimentell), Material 3 stabil, SSR Event Replay, @angular/build-Paket
19 (Nov 2024) standalone ist Default (standalone: true entfällt), linkedSignal, resource() (experimentell), incremental Hydration (Preview), HMR für Templates/Styles
20 (Mai 2025) Signal-APIs (effect, linkedSignal, toSignal) stabilisiert, incremental Hydration + Zoneless reifen, Karma-Deprecation angekündigt
21 (Nov 2025) weitere Zoneless-/Signal-Forms-Fortschritte, Vitest-Runner als empfohlener Weg, tsconfig-Aufräumungen
22 (Mai 2026) OnPush als ng generate-Default, fetch als HTTP-Backend-Default, TypeScript 6.0, @angular/animations-Modul deprecated zugunsten animate.enter/animate.leave

6. Der 30-Minuten-Wiedereinstieg

Öffne diese Dateien in dieser Reihenfolge - danach hast du ~80 % der Änderungen an echtem Code gesehen:

  1. src/main.ts (5 Zeilen) - bootstrapApplication statt bootstrapModule.
  2. src/app/app.config.ts - die provideX()-Liste = dein früheres @NgModule.
  3. src/app/app.component.ts + .html - Standalone-imports, inject(), @if/@else, self-closing <router-outlet />.
  4. src/app/app.routes.ts - loadComponent, funktionale Guards als Array.
  5. src/app/features/catalog/catalog.service.ts - das Signal-Trio statt BehaviorSubject + async.
  6. src/app/features/catalog/catalog-search.component.html - @if/@else if/@else, mat-table mit Signal-dataSource.
  7. src/app/core/auth/auth.interceptor.ts + role.guard.ts - funktionale Interceptors/Guards.
  8. src/app/core/auth/auth.service.ts - die RxJS-→-Signal-Bridge (zeigt, dass RxJS nicht weg ist).

Danach: der Frontend-Lernpfad geht dieselben Dateien mit Übungen durch, und angular-konzepte.md ist das Nachschlagewerk „Konzept → moderne vs. klassische Schreibweise".


7. Was du wahrscheinlich noch nie gesehen hast

Das genuin Neue - ohne Entsprechung in Angular 8:


Weiterführend

⌂ Cockpit