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
- Die drei großen Verschiebungen - wenn du nur einen Abschnitt liest, diesen.
- Was gleich geblieben ist - damit klar ist, wie viel dein altes Wissen noch trägt.
- Übersetzungstabelle v8 → v22 - Idiom für Idiom, mit Repo-Fundstelle.
- 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. - Zeitleiste - welche Version brachte was (zum Einordnen).
- Der 30-Minuten-Wiedereinstieg - Dateien in Lese-Reihenfolge.
- 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
- Damals:
AppModulebündelte alles; Feature-Module +RouterModule.forChild;SharedModulere-exportierteCommonModule+ Material;entryComponentsfür dynamisch erzeugte Komponenten. - Heute:
@Component({ imports: [...] }). Dynamisch erzeugte Komponenten brauchen keinentryComponentsund keineComponentFactorymehr (ViewContainerRef.createComponent(Cmp)). Ein „SharedModule" wird zu einer schlichten Datei, die ein Array von Direktiven/Pipes exportiert, oder man importiert sie einzeln. - Warum: weniger Indirektion, bessere Tree-Shaking-Fähigkeit, klarere Fehlermeldungen
(„X is not a known element" zeigt jetzt genau, welcher
imports-Eintrag fehlt). - In diesem Repo:
app.config.tsist die gesamte „Modul-Konfiguration" der App.OAuthModule.forRoot()(eine Fremdbibliothek, die noch NgModule-API hat) wird überimportProvidersFrom(...)eingebunden - die Brücke von der alten in die neue Welt. - Selber machen (v8-Reflex → v22-Weg):
cd library-frontend && npx ng generate component features/demo. Beobachte: es entsteht kein Modul, und keine bestehende Datei muss angefasst werden (in v8 hätteng g czusätzlichdeclarationsim nächstgelegenen*.module.tsergänzt). Öffne die erzeugtedemo.ts-@Componentmitimports: [], sonst nichts. Danachrm -rf src/app/features/demo.
Dependency Injection: Konstruktor → inject()
- Damals: ausschließlich Konstruktor-Parameter.
@Optional(),@Self(),@SkipSelf(),@Host()als Parameter-Decorators. - Heute:
inject(Token)als Feld-Initialisierer - und, entscheidend, auch in Funktionen (funktionale Guards, Interceptors,provideAppInitializer-Callbacks,computed). Die alten Modifier gibt es als Options:inject(X, { optional: true }). - Das DI-System selbst ist unverändert - hierarchische Injektoren,
providedIn: 'root'(seit v6), Provider-Rezepte (useClass/useValue/useFactory/useExisting). - In diesem Repo:
auth.interceptor.ts-const authService = inject(AuthService)in einer reinen Funktion; über den Konstruktor ginge das gar nicht. - Selber machen (v8-Reflex → v22-Weg):
catalog-search.component.tshat schon einenconstructor() { this.search(); }. Zieh dieinject()-Felder als Parameter dort hinein -constructor(protected catalogService: CatalogService, private formBuilder: FormBuilder) { this.search(); }- und lösch die Feld-Zeilen.npx ng buildbleibt grün: das DI-System ist dasselbe. Dann zurück aufinject()und überlege, warum es inauth.interceptor.tsundrole.guard.tskeine Konstruktor-Alternative gibt (dort ist der Baustein eine Funktion, keine Klasse).
Reaktivität: RxJS → Signals
- Was ein Signal ist: ein Container für einen Wert.
sig()liest (und registriert den Leser als abhängig),sig.set(v)/sig.update(fn)schreibt. Ändert sich der Wert, rendert Angular genau die abhängigen Stellen neu. computed(() => a() + b())- abgeleiteter, automatisch gecachter Signal.effect(() => console.log(sig()))- läuft bei jeder Änderung; für Logging, LocalStorage, Nicht-Angular-Integration. Kein Ersatz für Datenfluss zwischen Komponenten.- RxJS-Bridges:
toSignal(obs$)(Observable → Signal, räumt selbst auf),toObservable(sig)(andersrum). - Wann noch RxJS: HTTP (
HttpClientgibt weiterhinObservablezurück), Debounce/Throttle, Race-Conditions (switchMap), WebSockets, komplexe Event-Orchestrierung. NgRx-Effects. takeUntilDestroyed()(seit ~v16) ersetzt das altetakeUntil(destroy$)-Muster;DestroyRefgibt dir einen Aufräum-Hook ohneimplements OnDestroy.- In diesem Repo:
catalog.service.tszeigt das Signal-Trio (books/loading/error);auth.service.tsabonniert intern ein RxJS-events-Observable vonOAuthServiceund übersetzt es in Signals nach außen - genau die Bridge-Situation. - Selber machen (v8-Reflex → v22-Weg): Dein v8-Reflex wäre ein zweites
BehaviorSubjectpluscombineLatest. Der v22-Weg: infines.service.tseinreadonly openCount = computed(() => this.fines().filter((f) => f.status === 'OPEN').length);ergänzen (computedaus@angular/coreimportieren). Imfines.component.htmlneben der Tabelle{{ finesService.openCount() }}ausgeben. Kein Subscribe, keinasync-Pipe - der Wert aktualisiert sich automatisch, sobaldpay()die Liste neu lädt.
Templates: Control Flow, und was mit CommonModule passierte
@if/@else if/@else,@for (…; track …; let i = $index)mit@empty,@switch/@case/@default.@defer(stabil seit v17): ein Template-Block wird erst geladen, wenn eine Bedingung eintritt (on viewport,on interaction,on idle,on timer,when expr), mit@placeholder,@loading,@error. Das ist Lazy Loading auf Template-Ebene - gab es in v8 überhaupt nicht.@let name = expr;- lokale, read-only Template-Variable.NgIf,NgForOf,NgSwitchexistieren noch (für Bestandscode), aber neue Projekte brauchen sie nicht - und damit oft gar keinCommonModulemehr.NgClass/NgStylesind meist durch[class.x]="…"/[class]="objekt()"/[style.width.px]="…"ersetzbar.- Self-closing tags:
<app-catalog-search />statt<app-catalog-search></app-catalog-search>. - Selber machen (v8-Reflex → v22-Weg):
catalog-search.component.htmlhat einen@else if (catalogService.books().length === 0) { <p class="empty-state">Keine Bücher gefunden.</p> }-Zweig - schreib ihn mal in die v8-Form um (*ngIf+<ng-template #empty>), spür den Unterschied, und mach ihn rückgängig. Dann wickle die<table mat-table …>in einen@defer (on viewport) { … } @placeholder { <p>Lade Tabelle…</p> }-Block.npx ng buildund imdist/siehst du ein zusätzliches Chunk - der Tabellen-Code (inkl.MatTableModule) wird erst bei Sichtbarkeit geladen. Das gab es in Angular 8 nicht.
Routing
- Damals:
RouterModule.forRoot(routes)im AppModule,forChildje Feature-Modul.loadChildrenwar anfangs ein magischer String ('./x/x.module#XModule'), später eine Import-Funktion. - Heute:
provideRouter(routes, withComponentInputBinding(), withViewTransitions())in derApplicationConfig.loadComponentlädt eine einzelne Standalone-Komponente. Routen können eigeneprovidershaben (Scoped Services pro Route-Teilbaum). Guards und Resolver sind Funktionen (CanActivateFn,ResolveFn). MitwithComponentInputBinding()landen Routenparameter direkt ininput()-Feldern der Komponente. - In diesem Repo:
app.routes.ts- flaches Array,loadComponent,canActivate: [authGuard, roleGuard([...])].role.guard.tszeigt eine Guard-Factory (roleGuard(['ADMIN'])gibt eineCanActivateFnzurück) - mit Klassen-Guards war das umständlich. - Selber machen (v8-Reflex → v22-Weg): Häng in
app.config.tswithComponentInputBinding()anprovideRouter(routes). Ergänze incatalog-search.component.tseinreadonly q = input<string>();und ruf im Konstruktorthis.searchForm.patchValue({ title: this.q() ?? '' }). Jetzt füllthttp://localhost:4200/catalog?q=Cleandas Suchfeld vor - ohneActivatedRoutezu injizieren undqueryParamMapzu abonnieren (der v8-Weg). Danach rückgängig.
HTTP
- Damals:
HttpClientModuleinimports. Interceptor =@Injectable-Klasseimplements HttpInterceptormitintercept(req, next), registriert über denHTTP_INTERCEPTORS-Multi-Token. - Heute:
provideHttpClient(withInterceptors([fn])). Interceptor =HttpInterceptorFn- eine Funktion(req, next) => next(req). Ab v22 istfetchdas Standard-Backend stattXMLHttpRequest. - In diesem Repo:
auth.interceptor.ts(funktional, hängtAuthorization: Beareran), registriert inapp.config.tsalsprovideHttpClient(withXhr(), withInterceptors([authInterceptor])). DaswithXhr()hat die v22-Migration ergänzt, um beim bisherigen XHR-Backend zu bleiben - der Rückbau auf denfetch-Default ist als Folgeschritt vorgemerkt (siehe angular-update-migration.md). - Selber machen (v8-Reflex → v22-Weg): Dein v8-Reflex: eine
@Injectable-Klasseimplements HttpInterceptor+{ provide: HTTP_INTERCEPTORS, useClass: …, multi: true }. Der v22-Weg: eine Dateicore/api/logging.interceptor.tsmitexport const loggingInterceptor: HttpInterceptorFn = (req, next) => { console.log(req.method, req.url); return next(req); };und inapp.config.tswithInterceptors([authInterceptor, loggingInterceptor]). Öffne die Konsole: die Reihenfolge im Array ist die Aufrufreihenfolge. Danach wieder entfernen.
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>();)
input.required()erzwingt jetzt zur Compile-Zeit, dassstatusgesetzt wird.
Change Detection
- Damals: zone.js patcht alle Browser-APIs; nach jedem Event wird der ganze Baum geprüft.
ChangeDetectionStrategy.OnPushwar eine Optimierung, die man bewusst einschaltete und dann mitChangeDetectorRef.markForCheck()bediente. - Heute: Immer noch zone.js by default - aber
OnPushist ab v22 der Default fürng generate, Signals treiben die CD punktgenau, und zoneless (provideZonelessChangeDetection()) ist verfügbar (kommt ganz ohne zone.js aus, verlässt sich auf Signals + explizite Benachrichtigung). - In diesem Repo:
app.config.tsnutzt bewusst weiterprovideZoneChangeDetection({ eventCoalescing: true })(konservativ). Die v22-Migration hatchangeDetection: ChangeDetectionStrategy.Eageran die Komponenten geschrieben, um das bisherige Verhalten einzufrieren - der Umstieg aufOnPushist als eigener Schritt vorgemerkt (siehe angular-update-migration.md). - Selber machen (v8-Reflex → v22-Weg): In v8 war
OnPushein Wagnis - man musste überall anmarkForCheck()denken. Setz infines.component.tschangeDetection: ChangeDetectionStrategy.OnPush(stattEager), starteng serve, geh auf/fines, lade Mahngebühren und bezahl eine. Alles reagiert weiter - weil der gesamte Zustand über Signals (finesService.fines()etc.) läuft und Signals die CD von sich aus anstoßen. Genau deshalb istOnPushin v22 der Default.
Build & Tooling
- Damals: Webpack unter der Haube (
@angular-devkit/build-angular:browser),ng eject(früh entfernt), langsame Rebuilds,environment.ts-Dateien mitfileReplacements. - Heute: esbuild + Vite (
@angular/build:applicationbzw. noch@angular-devkit/build-angular:application). Rebuilds in Millisekunden, HMR für Templates und Styles, SSR eingebaut, Bundle-Budgets, automatisches Font-/Asset-Inlining.angular.jsonbenutzt denapplication-Builder (ein Artefakt für Browser + optional Server). - In diesem Repo:
library-frontend/angular.json-builder: "@angular-devkit/build-angular:application". - Selber machen (v8-Reflex → v22-Weg): Starte
npx ng serve, warte auf den ersten Build, dann ändere inapp.component.htmlden Marken-Text und speichere. Der Rebuild ist im Terminal in Millisekunden durch (v8/Webpack: mehrere Sekunden), und der Browser tauscht Template/Styles per HMR ohne Full-Reload.npx ng build --configuration productionund schau indist/library-frontend/- pro Lazy-Route ein eigeneschunk-*.js.
Testing
- Damals: Karma + Jasmine (Unit), Protractor (E2E).
- Heute: Protractor ist eingestellt (Ende 2023 EOL, aus
ng newentfernt). Karma gilt seit ~v20 als deprecated; Nachfolger ist ein Vitest-basierter Runner (@angular/build:unit-test, noch experimentell) - Jest wird von der Community genutzt. E2E: Playwright, Cypress oder WebdriverIO.TestBedbekam Verbesserungen (TestBed.inject()stattTestBed.get()), Component Harnesses für Material. - In diesem Repo:
app.component.spec.tsläuft noch unter Karma (bewusst, ein Wechsel ist vorgemerkt);e2e/golden-path.spec.tsist Playwright gegen den echten Stack. - Selber machen (v8-Reflex → v22-Weg): Lies
app.component.spec.ts. Zwei Dinge, die es in deinem v8-Gedächtnis anders gab:TestBed.inject(AuthService)(früherTestBed.get(...), seit v9 deprecated) und der Provider-Override{ provide: AuthService, useValue: new FakeAuthService() }mit frei setzbaren Signals im Fake. Skizziere nach demselben Muster einen Spec fürcatalog-search.component.ts(Fake-CatalogServicemitbooks/loadingalssignal(...), prüfe „Spinner beiloading()", „Keine Bücher gefunden bei leerer Liste").
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:
src/main.ts(5 Zeilen) -bootstrapApplicationstattbootstrapModule.src/app/app.config.ts- dieprovideX()-Liste = dein früheres@NgModule.src/app/app.component.ts+.html- Standalone-imports,inject(),@if/@else, self-closing<router-outlet />.src/app/app.routes.ts-loadComponent, funktionale Guards als Array.src/app/features/catalog/catalog.service.ts- das Signal-Trio stattBehaviorSubject+async.src/app/features/catalog/catalog-search.component.html-@if/@else if/@else,mat-tablemit Signal-dataSource.src/app/core/auth/auth.interceptor.ts+role.guard.ts- funktionale Interceptors/Guards.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:
@defer- Lazy Loading auf Template-Ebene (@defer (on viewport) { <schwere-komponente /> }).- Signal-Inputs/Outputs -
input(),input.required(),output(),model()(Zwei-Wege). resource()/linkedSignal()- asynchrone Signals: „lade Daten, wenn sich dieser Parameter-Signal ändert", deklarativ, mitloading/error/value- der moderne Ersatz für „Parameter beobachten →switchMap→ Zustand setzen".NgOptimizedImage(ngSrcstattsrc) - automatischesrcset, Lazy Loading, LCP-Priorisierung, Größen-Warnungen.- Incremental Hydration - SSR-Seiten werden nicht komplett „aktiviert", sondern erst der
Bereich, mit dem der Nutzer interagiert (
@defer (hydrate on interaction)). - Zoneless Change Detection - Angular ohne
zone.js. afterRenderEffect/afterNextRender- definierter Hook für DOM-Messungen nach dem Rendern (früher der Wilde Westen ausngAfterViewInit+setTimeout).@letim Template, Self-closing Component-Tags,host-Objekt im Decorator statt@HostBinding/@HostListener.- Control-Flow-,
inject()- und Standalone-Migrations-Schematics (ng generate @angular/core:control-flowusw.) - automatische Codemods für den Bestandscode.
Weiterführend
- angular-konzepte.md - jedes Konzept mit „dieses Projekt vs. klassischer Angular-Weg" als Tabelle.
- frontend-lernpfad.md - geführter Pfad durch den Code mit Übungen.
- angular-update-migration.md - wie dieses Projekt selbst von 18 auf 22 kam, majorweise, mit allen Stolpersteinen.
- Offizielle Doku (komplett neu seit v17): https://angular.dev - besonders „Signals", „Control flow", „Standalone", „Migrations".
- https://update.angular.dev - der interaktive Migrations-Guide „von Version X auf Y".