Exkurs: Angular 8 → 18 für Rückkehrer:innen
Zielgruppe: Entwickler:in, die vor einigen Jahren mit Angular ~8 (2019) gearbeitet hat und jetzt an einem Projekt auf Angular 18 (Mai 2024) sitzt. Zwischen v8 und v18 liegen 10 Major-Releases. Dieses Dokument ordnet die Änderungen nach Konzept-Verschiebung, nicht nach Version, und sagt für jeden Punkt: was du bei v18 hast - und was erst danach kam (v19–v22), damit du dein nächstes Update planen kannst.
Verwandtes Dokument: Exkurs: Angular 8 → 22 - dasselbe für die aktuellste Version. Dieses Repo ist auf v22; es dient hier als „so ist es weitergegangen".
Wie dieser Exkurs aufgebaut ist
- Die drei großen Verschiebungen (v8 → v18) - wenn du nur einen Abschnitt liest, diesen.
- Was gleich geblieben ist - wie viel deines alten Wissens noch trägt.
- Übersetzungstabelle v8 → v18 - Idiom für Idiom.
- Die Verschiebungen im Detail - je Abschnitt: damals (v8) / bei v18 / warum / Ausblick seit v18 - plus eine „Selber machen"-Übung für dein v18-Projekt.
- Zeitleiste v8 → v18 (und ein Teaser „was danach kam").
- Der Wiedereinstieg - Reihenfolge, in der du dich einarbeitest.
- Was v18 schon hat / noch nicht hat - die scharfe Grenze.
Angular 18 in Zahlen: TypeScript >=5.4 <5.6, zone.js ~0.14, Node ^18.13 || >=20.9,
RxJS ^7.4. Build-System: esbuild/Vite (seit v17 Default). Neues Paket @angular/build (v18).
1. Die drei großen Verschiebungen
a) Von NgModule zu Standalone
Damals (v8): Alles in @NgModule - declarations, imports, exports, entryComponents,
SharedModule/CoreModule, Feature-Module mit RouterModule.forChild.
Bei v18: Standalone Components sind stabil (seit v15) und der Default aus ng new (seit
v17). @Component({ standalone: true, imports: [...] }), bootstrapApplication(App, appConfig),
ApplicationConfig mit provideX(). Wichtig für v18: standalone: true musst du noch
hinschreiben - dass es der Default wird und wegfallen kann, kommt erst mit v19.
Ausblick seit v18: v19 macht standalone zum Default (eine Migrations-Schematic entfernt
standalone: true überall); es gibt strictStandalone als tsconfig-Option. → In diesem Repo
(v22) steht in keinem @Component mehr ein standalone.
b) Von „RxJS für alles" zu Signals
Damals (v8): Zustand = BehaviorSubject, Template = value$ | async, überall das
takeUntil(destroy$) + ngOnDestroy-Ritual. Change Detection: bei jedem Event der ganze Baum.
Bei v18: Core-Signals sind stabil (seit v16): signal(), computed(), effect(). Im
Template sig() statt | async. toSignal() / toObservable() als Brücken (stabil).
takeUntilDestroyed() und DestroyRef (v16) ersetzen das destroy$-Muster. RxJS bleibt für
echte Ströme (HTTP, Debounce, switchMap, WebSockets).
Ausblick seit v18: v19 bringt linkedSignal() und resource() (experimentell, asynchrone
Signals); v20 stabilisiert effect/linkedSignal/toSignal weiter und entschärft die
effect()-Timing-Regeln. → In diesem Repo: catalog.service.ts (Signal-Trio),
auth.service.ts (Observable → Signal-Bridge).
c) Von Struktur-Direktiven zu eingebautem Control Flow
Damals (v8): *ngIf="x; else tpl", *ngFor="… trackBy: fn", <ng-template>, [ngSwitch] -
mit CommonModule im Modul.
Bei v18: @if / @for / @switch und @defer sind stabil (seit v17). track ist
Pflicht (direkt der Ausdruck, kein trackBy-Funktionsname). Es gibt eine Migrations-Schematic
(ng generate @angular/core:control-flow), die deinen *ngIf/*ngFor-Bestand umschreibt.
NgIf/NgFor funktionieren weiter, aber neue Templates brauchen kein CommonModule mehr.
Ausblick seit v18: v18 hat schon @let (lokale Template-Variable) noch nicht - das kam mit
v18.1/v19. Ansonsten ist Control Flow bei v18 im Wesentlichen fertig. → Im Repo überall:
app.component.html, catalog-search.component.html.
Unsichtbar, aber schon passiert: Der Compiler (View Engine → Ivy, ab v9/v13) und das Build-System (Webpack → esbuild/Vite, ab v17) sind bei v18 längst getauscht. Merkst du nur an Geschwindigkeit und besseren Fehlermeldungen.
2. Was gleich geblieben ist (Entwarnung)
| Konzept | v8 = v18 |
|---|---|
| Komponenten, Templates, Services, Pipes, Direktiven | dieselbe Idee, dieselben Decorators |
| Dependency Injection | hierarchische Injektoren, providedIn: 'root', Provider-Rezepte - unverändert |
| RxJS | weiterhin dabei und wichtig für Ströme |
| TypeScript | dieselbe Sprache (v8: ~3.4, v18: ~5.4/5.5) |
| Angular CLI | ng new / generate / build / serve / test / update |
| Angular Material / CDK | dieselbe Bibliothek; bei v18 auf „M3" (Material 3) umgestellt, API weitgehend gleich |
| Router-Grundidee | Routentabelle, <router-outlet>, Lazy Loading, Guards |
| Reactive Forms | jetzt typisiert (seit v14) |
| zone.js | bei v18 noch Default (zoneless ist experimentell) - dein ngZone-Wissen gilt weiter |
| Karma | bei v18 noch der Standard-Test-Runner und nicht deprecated |
3. Übersetzungstabelle v8 → v18
| Aufgabe | Angular 8 | Angular 18 |
|---|---|---|
| App starten | platformBrowserDynamic().bootstrapModule(AppModule) |
bootstrapApplication(AppComponent, appConfig) |
| App-weite Provider | @NgModule({ providers, imports }) |
ApplicationConfig mit provideRouter / provideHttpClient / … |
| Komponente „bekannt machen" | in declarations eines Moduls |
@Component({ standalone: true, imports: [...] }) |
| Abhängigkeit holen | constructor(private http: HttpClient) {} |
private http = inject(HttpClient) (Konstruktor geht weiter) |
| lokaler Zustand | x$ = new BehaviorSubject(0) + x$ | async |
x = signal(0) + x() |
| abgeleiteter Wert | combineLatest([...]).pipe(map(...)) |
computed(() => …) |
| Subscription aufräumen | takeUntil(this.destroy$) + ngOnDestroy |
takeUntilDestroyed() / DestroyRef |
| Bedingung im Template | *ngIf="x; else tpl" |
@if (x) { } @else { } |
| Schleife im Template | *ngFor="let i of items; trackBy: fn" |
@for (i of items(); track i.id) { } @empty { } |
| verzögert laden | manuell / gar nicht | @defer (on viewport) { } |
| Lazy-Route | loadChildren: './x/x.module#XModule' |
loadComponent: () => import('...').then(m => m.X) |
| Route Guard | @Injectable class G implements CanActivate |
const g: CanActivateFn = () => { … } (Funktion) |
| HTTP aktivieren | imports: [HttpClientModule] |
provideHttpClient(withInterceptors([...])) - XHR ist noch Default, withFetch() opt-in |
| HTTP-Interceptor | { provide: HTTP_INTERCEPTORS, useClass: …, multi: true } |
withInterceptors([fn]) (Funktion) |
| App-Init vor Bootstrap | APP_INITIALIZER-Multi-Provider mit deps |
weiterhin APP_INITIALIZER (provideAppInitializer() kommt erst v19) |
@Input() / @Output() |
@Input() x; @Output() y = new EventEmitter() |
x = input<T>(); y = output<T>() (Signal-basiert, seit v17.1–17.3) |
| Zwei-Wege-Bindung | @Input() value; @Output() valueChange |
value = model<T>() |
@ViewChild |
@ViewChild('r') r |
r = viewChild('r') (Signal, seit v17.2) |
| Change Detection | zone.js, OnPush als bewusste Optimierung |
zone.js Default, OnPush opt-in; zoneless experimentell |
| Unit-Test-Runner | Karma + Jasmine | Karma + Jasmine (unverändert; Jest experimentell seit v16) |
| E2E-Test | Protractor | Protractor ist tot → Playwright / Cypress / WebdriverIO |
| SSR | Angular Universal (renderModuleFactory) |
@angular/ssr, provideClientHydration(withEventReplay()) - Hydration + Event-Replay bei v18 |
4. Die Verschiebungen im Detail
NgModule → Standalone
- Damals (v8):
AppModulebündelte alles;entryComponentsfür dynamisch erzeugte Komponenten;ComponentFactoryResolver. - Bei v18:
@Component({ standalone: true, imports: [...] }). Dynamische Komponenten:ViewContainerRef.createComponent(Cmp)ohne Factory. „SharedModule" → eine Datei mit einem exportierten Array oder Einzel-Imports. - Warum: weniger Indirektion, besseres Tree-Shaking, präzisere „X is not a known element"-Fehler.
- Ausblick seit v18: v19 →
standaloneist Default,standalone: trueverschwindet per Migration. Bis dahin: hinschreiben. - Selber machen (v18-Projekt):
ng generate component features/demo --standalone(bei v17+ ist--standaloneschon der Default). Beobachte: kein Modul angefasst. In einem alten NgModule-Projekt kannst du mitng generate @angular/core:standaloneschrittweise migrieren (3 Modi: Komponenten umstellen → Bootstrap umstellen → ungenutzte NgModules löschen).
Dependency Injection: Konstruktor → inject()
- Damals (v8): nur Konstruktor-Parameter;
@Optional()/@Self()/@SkipSelf()/@Host(). - Bei v18:
inject(Token)als Feld-Initialisierer - und in Funktionen (funktionale Guards, Interceptors,computed). Modifier als Options:inject(X, { optional: true }). Das DI-System selbst ist unverändert. - Ausblick seit v18: unverändert bis v22 -
inject()ist stabil und der empfohlene Weg. - Selber machen (v18-Projekt): Nimm einen Service mit langer Konstruktor-Parameterliste und
stell ihn auf
inject()-Felder um - der Diff wird kürzer und die Abhängigkeiten sindreadonly. Die Migrations-Schematic dafür:ng generate @angular/core:inject.
Reaktivität: RxJS → Signals
- Was ein Signal ist: Wert-Container.
sig()liest (+ registriert Abhängigkeit),sig.set/sig.updateschreibt. Änderung → gezieltes Re-Rendering. - Bei v18 stabil:
signal,computed,effect,toSignal,toObservable,takeUntilDestroyed,DestroyRef, Signal-input()/output()/model()/viewChild(). - Wann noch RxJS: HTTP (
HttpClientgibtObservable), Debounce/Throttle,switchMapgegen Race-Conditions, WebSockets, NgRx-Effects. - Ausblick seit v18: v19
linkedSignal()+resource()(asynchrone Signals - „lade neu, wenn sich dieser Parameter-Signal ändert"); v20 stabilisiert die Signal-APIs weiter. - Selber machen (v18-Projekt): Such eine Komponente mit
data$ | async+ manuellemsubscribeimngOnInit+ngOnDestroy. Ersetz den Zustand durch einsignal()im Service, im Templateservice.data().ngOnDestroyund derasync-Pipe fallen weg. → Im Repo ist das bereits durchgezogen:catalog.service.ts.
Templates: Control Flow
- Damals (v8):
*ngIf/*ngFor/*ngSwitch+CommonModule. - Bei v18:
@if/@else if/@else,@for (…; track …; let i = $index)mit@empty,@switch.@defer(stabil seit v17): Template-Block lädt erst beion viewport/on interaction/on idle/on timer/when expr, mit@placeholder/@loading/@error. Migrations-Schematic:ng generate @angular/core:control-flow. - Ausblick seit v18:
@let(Template-Variable) kam mit v18.1/v19; ansonsten stabil. - Selber machen (v18-Projekt): Lauf die Control-Flow-Migration über ein Feature; prüf den
Diff. Dann wickle eine „schwere" Teilansicht (Chart, große Tabelle) in
@defer (on viewport) { … } @placeholder { … }und sieh im Netzwerk-Tab das nachgeladene Chunk.
Routing
- Damals (v8):
RouterModule.forRoot/forChild,loadChildren-Magic-String. - Bei v18:
provideRouter(routes, withComponentInputBinding(), withViewTransitions()).loadComponentfür eine einzelne Standalone-Komponente. Guards/Resolver sind Funktionen (CanActivateFn,ResolveFn).withComponentInputBinding()(v16) bindet Routenparameter aninput()-Felder. Neu in v18: Redirects als Funktion (redirectTo: () => …). - Ausblick seit v18: kaum Änderung bis v22 - Routing ist stabil.
- Selber machen (v18-Projekt): Schalt
withComponentInputBinding()ein und lies einen Routenparameter überid = input.required<string>()stattActivatedRoutezu injizieren undparamMapzu abonnieren. → Im Repo:app.routes.ts,role.guard.ts(Guard-Factory).
HTTP
- Damals (v8):
HttpClientModule+ Klassen-Interceptor überHTTP_INTERCEPTORS-Multi-Token. - Bei v18:
provideHttpClient(withInterceptors([fn])), funktionale Interceptors (v15). Das Standard-Backend ist nochXMLHttpRequest-withFetch()schaltet auffetch(opt-in, empfohlen für SSR). Neu in v18: Interceptor-Kontext, bessere Fehler-Typisierung. - Ausblick seit v18: v22 macht
fetchzum Default (withXhr()schaltet zurück). → Im Repo (v22) steht deshalbprovideHttpClient(withXhr(), withInterceptors([authInterceptor]))- die v22-Migration hatwithXhr()ergänzt, um beim alten Verhalten zu bleiben. - Selber machen (v18-Projekt): Schreib einen alten Klassen-Interceptor in die
HttpInterceptorFn-Form um ((req, next) => next(req)) und häng ihn inwithInterceptors([…])- die Array-Reihenfolge ist die Aufrufreihenfolge.
Component-API: Decorators → Signal-Primitive
Bei v18 alle stabil verfügbar:
| v8 | v18 |
|---|---|
@Input() value: string |
value = input<string>() / input.required<string>() |
@Output() change = new EventEmitter<T>() |
change = output<T>() |
Banana-in-a-box (value + valueChange) |
value = model<T>() |
@ViewChild('r') r / @ViewChildren |
r = viewChild('r') / viewChildren('r') (Signals) |
@HostBinding / @HostListener |
host: { '[class.x]': 'y()', '(click)': 'onClick()' } im Decorator |
ngOnChanges |
oft ein effect() auf den input()-Signal |
ngAfterViewInit für DOM |
afterNextRender(() => …) / afterRender(...) (seit v17) |
- Ausblick seit v18: v19+
linkedSignalfür abgeleitete, aber überschreibbare Zustände; Signal-basierte Formulare sind Richtung v21/v22 in Arbeit. - Selber machen (v18-Projekt): Bau eine kleine Präsentationskomponente mit
input.required<T>()+output<void>().input.required()erzwingt zur Compile-Zeit, dass der Wert gesetzt wird - das gab es mit@Input()nie.
Change Detection
- Damals (v8): zone.js patcht alle Browser-APIs;
OnPushwar die bewusste Optimierung mitmarkForCheck(). - Bei v18: zone.js noch Default.
OnPushopt-in.provideExperimentalZonelessChangeDetection()ist experimentell in v18 - erster ernsthafter zoneless-Anlauf. Signals treiben die CD bereits präzise, wenn du sie nutzt. - Ausblick seit v18: v19/v20 reifen zoneless; v22 macht
OnPushzumng generate-Default. → Im Repo hat die v22-MigrationchangeDetection: ChangeDetectionStrategy.Eageran die Komponenten geschrieben (= altes Verhalten), derOnPush-Umstieg ist dort ein vorgemerkter Folgeschritt. - Selber machen (v18-Projekt): Setz
changeDetection: ChangeDetectionStrategy.OnPushan eine Komponente, deren Zustand komplett über Signals läuft - alles reagiert weiter, weil Signals die CD anstoßen. Das ist die Vorstufe zu zoneless.
Build & Tooling
- Damals (v8): Webpack (
…:browser-Builder),environment.ts+fileReplacements, langsame Rebuilds. - Bei v18: esbuild + Vite (
@angular-devkit/build-angular:application, seit v17 Default). Rebuilds in Millisekunden, HMR, SSR eingebaut, Bundle-Budgets. Neu in v18: das Paket@angular/build(die Builder ziehen dorthin um). - Ausblick seit v18: v20/v21 machen
@angular/build:applicationzum kanonischen Builder;@angular-devkit/build-angularwird zur Kompatibilitätsschicht. - Selber machen (v18-Projekt):
ng update @angular/cliund prüf inangular.json, ob dein Projekt schon:applicationnutzt - wenn nicht:ng update @angular/cli --name use-application-builder.
Testing
- Damals (v8): Karma + Jasmine (Unit), Protractor (E2E).
- Bei v18: Karma ist noch Standard und nicht deprecated. Jest-Support ist experimentell
(seit v16). Protractor ist eingestellt (EOL Ende 2023, aus
ng newraus seit v16) - E2E jetzt Playwright / Cypress / WebdriverIO.TestBed.inject()stattTestBed.get(), Component Harnesses für Material. - Ausblick seit v18: v20 kündigt die Karma-Deprecation an; Nachfolger ist ein
Vitest-basierter Runner (
@angular/build:unit-test), ab ~v21 der empfohlene Weg. → Im Repo läuftapp.component.spec.tsbewusst noch unter Karma. - Selber machen (v18-Projekt): Wenn dein Projekt noch
e2e/mit Protractor hat: einen Golden-Path-Test in Playwright neu schreiben (siehelibrary-frontend/e2e/golden-path.spec.tsals Vorlage) und die Protractor-Config entfernen.
SSR / Hydration
- Damals (v8): Angular Universal, separat,
renderModuleFactory, „destructive" Hydration (DOM wird verworfen und neu gebaut). - Bei v18:
@angular/ssr, inng new --ssreingebaut.provideClientHydration()- Non-destructive Hydration ist GA (seit v17), Event-Replay kommt mit v18 (withEventReplay()- Klicks während des Ladens gehen nicht verloren). - Ausblick seit v18: v19 incremental Hydration (
@defer (hydrate on …)) - nur der Bereich wird aktiviert, mit dem der Nutzer interagiert. - Selber machen: in diesem Repo nicht anwendbar (reine SPA, kein SSR). Für dein v18-Projekt:
falls SSR aktiv,
withEventReplay()inprovideClientHydration()ergänzen.
5. Zeitleiste v8 → v18
Zum Einordnen. Maßgeblich im Zweifel: https://update.angular.dev und der CHANGELOG.
| Version (≈ Datum) | Für Rückkehrer:innen wichtig |
|---|---|
| 8 (Mai 2019) | dein Ausgangspunkt: NgModules, View Engine, Import-Funktion für loadChildren, differential loading |
| 9 (Feb 2020) | Ivy wird Default-Compiler |
| 10 (Jun 2020) | ng new --strict, CommonJS-Warnungen |
| 11 (Nov 2020) | schnellere Builds, TSLint → ESLint, HMR-Support |
| 12 (Mai 2021) | „Ivy Everywhere": View Engine deprecated, Prod-Build als Default, ?./?? in Templates |
| 13 (Nov 2021) | View Engine entfernt, IE11 raus, keine ComponentFactory mehr, Build-Cache |
| 14 (Jun 2022) | Standalone (Preview), typisierte Reactive Forms, inject(), provide*-Funktionen |
| 15 (Nov 2022) | Standalone stabil, funktionale Guards, NgOptimizedImage, Directive Composition, Material auf „MDC" |
| 16 (Mai 2023) | Signals (Preview), takeUntilDestroyed/DestroyRef, SSR-Hydration (Preview), esbuild-Dev-Server, required inputs, self-closing tags, Protractor aus ng new |
| 17 (Nov 2023) | @if/@for/@switch + @defer stabil, esbuild/Vite Application-Builder als Default, afterRender, neue Doku angular.dev, Standalone als ng new-Default, Hydration GA |
| 18 (Mai 2024) | ← du bist hier. Zoneless (experimentell), Material 3 stabil, SSR Event Replay, Redirects als Funktion, Paket @angular/build, Signal-Inputs/Outputs/Queries stabil |
Und danach - was auf deinem Update-Weg wartet (Details: Exkurs 8 → 22 und angular-update-migration.md):
- 19 (Nov 2024):
standaloneist Default (standalone: truefällt weg),provideAppInitializer(),linkedSignal,resource(), incremental Hydration, HMR für Templates/Styles - 20 (Mai 2025): Signal-APIs stabilisiert, Zoneless reift, Karma-Deprecation angekündigt
- 21 (Nov 2025): Vitest-Runner empfohlen, weitere Zoneless-Schritte
- 22 (Mai 2026): OnPush als Default,
fetchals Default, TypeScript 6.0
6. Der Wiedereinstieg
Dieses Repo ist auf v22, nicht v18 - aber die Struktur ist dieselbe, und die v18-Deltas stehen oben in Abschnitt 3/4. Lies die Dateien für die Form, denk die Deltas dazu:
library-frontend/src/main.ts-bootstrapApplicationstattbootstrapModule.src/app/app.config.ts- dieprovideX()-Liste. Delta v18: hier stünde noch{ provide: APP_INITIALIZER, useFactory: …, deps: […], multi: true }stattprovideAppInitializer(…).src/app/app.component.ts- Standalone-imports,inject(). Delta v18: mitstandalone: trueim Decorator.src/app/app.routes.ts-loadComponent, funktionale Guards.src/app/features/catalog/catalog.service.ts- Signal-Trio stattBehaviorSubject.src/app/features/catalog/catalog-search.component.html-@if/@for.src/app/core/auth/auth.interceptor.ts+role.guard.ts- funktionale Interceptors/Guards.
Danach: Frontend-Lernpfad (geführt, mit Übungen), angular-konzepte.md (Nachschlagewerk).
7. Was v18 schon hat - und was noch nicht
Hast du bei v18 (nutzt du es schon?):
inject(), Standalone, funktionale Guards/Interceptors, @if/@for/@switch/@defer,
Signals (signal/computed/effect/toSignal), input()/output()/model()/viewChild(),
takeUntilDestroyed/DestroyRef, afterNextRender, NgOptimizedImage, withComponentInputBinding,
esbuild/Vite-Build, Material 3, SSR-Hydration + Event-Replay, self-closing tags, host-Objekt.
Hast du bei v18 NOCH NICHT (kommt mit v19–v22):
standalone als Default · provideAppInitializer() · @let im Template · linkedSignal() ·
resource() · incremental Hydration · stabiles Zoneless · fetch als HTTP-Default · OnPush
als Default · TypeScript 6.0 · Vitest-Test-Runner.
Weiterführend
- Exkurs: Angular 8 → 22 - dieselbe Struktur bis zur aktuellsten Version, mit diesem Repo als lauffähigem Anschauungsmaterial.
- angular-update-migration.md - wie man Angular-Majors sicher aktualisiert (Regeln, Runbook) - relevant, wenn dein v18-Projekt weiterziehen soll.
- angular-konzepte.md · frontend-lernpfad.md
- https://angular.dev (Doku, komplett neu seit v17) · https://update.angular.dev