diff --git a/blog/2025-11-angular21/README.md b/blog/2025-11-angular21/README.md index a24272f8..a8c4b03a 100644 --- a/blog/2025-11-angular21/README.md +++ b/blog/2025-11-angular21/README.md @@ -15,7 +15,7 @@ keywords: - Karma language: de header: angular21.jpg -sticky: true +sticky: false isUpdatePost: true --- diff --git a/blog/2025-11-zu-vitest-migrieren/README.md b/blog/2025-11-zu-vitest-migrieren/README.md index cfbc2fbc..a5ae7de9 100644 --- a/blog/2025-11-zu-vitest-migrieren/README.md +++ b/blog/2025-11-zu-vitest-migrieren/README.md @@ -3,13 +3,15 @@ title: 'Vitest in Angular 21: Was ist neu und wie kann man migrieren?' author: Johannes Hoppe mail: johannes.hoppe@haushoppe-its.de published: 2025-11-18 -lastModified: 2025-11-20 +lastModified: 2026-06-01 keywords: - Angular - Angular 21 + - Angular 22 - Vitest - Karma - Jasmine + - fakeAsync language: de header: angular-vitest.jpg --- @@ -29,7 +31,7 @@ In diesem Artikel zeigen wir, was Vitest für dich bedeutet, wie du bestehende A ## Warum Angular Karma und Jasmine ersetzt _Karma und Jasmine_ haben für Angular lange Jahre gute Dienste geleistet, vor allem wegen der Ausführung in einem echten Browser. -Es gab aber Nachteile: die Ausführungsgeschwindigkeit war nie optimal und das Ökosystem ist veraltet ([Karma ist seit 2023 deprecated](https://github.com/karma-runner/karma#karma-is-deprecated-and-is-not-accepting-new-features-or-general-bug-fixes)). +Es gab aber Nachteile: die Ausführungsgeschwindigkeit war nie optimal und das Ökosystem ist veraltet ([Karma ist seit 2023 deprecated](https://github.com/karma-runner/karma#karma-is-deprecated-and-is-not-accepting-new-features-or-general-bug-fixes)). Über mehrere Jahre prüfte das Angular-Team Alternativen (Jest, Web Test Runner usw.), ohne einen klaren Gewinner zu finden. [Vitest](https://vitest.dev/) wurde inzwischen äußerst populär und erwies sich als passende Lösung. @@ -48,7 +50,7 @@ Kurz gesagt: Der Wechsel sorgt für Tempo, eine deutlich bessere Developer Exper Wenn du ein **neues Projekt** mit Angular 21 erzeugen möchtest, nutzt die Angular CLI standardmäßig den neuen Test-Runner Vitest. Die Wahl kannst du über die Option `--test-runner` beeinflussen: -Mit `--test-runner=vitest` erhältst du die neue, schnellere und modernere Standardlösung. +Mit `--test-runner=vitest` erhältst du die neue, schnellere und modernere Standardlösung. Möchtest du dagegen weiterhin bei der bewährten Karma/Jasmine-Kombination bleiben, verwende die Option `--test-runner=karma`. Ohne explizite Angabe der Option wird automatisch Vitest verwendet. @@ -64,9 +66,9 @@ Bevor du das automatische Refactoring‑Schematic verwendest, musst du dein Proj #### 1. Abhängigkeiten installieren -Installiere `vitest` sowie eine DOM‑Emulationsbibliothek. -Obwohl Tests weiterhin im Browser ausgeführt werden können (siehe Schritt 5), verwendet Vitest standardmäßig eine DOM‑Emulation, um eine Browserumgebung in Node.js zu simulieren und Tests schneller auszuführen. -Die CLI erkennt automatisch `happy-dom`, falls es installiert ist; ansonsten greift sie auf `jsdom` zurück. +Installiere `vitest` sowie eine DOM‑Emulationsbibliothek. +Obwohl Tests weiterhin im Browser ausgeführt werden können (siehe Schritt 5), verwendet Vitest standardmäßig eine DOM‑Emulation, um eine Browserumgebung in Node.js zu simulieren und Tests schneller auszuführen. +Die CLI erkennt automatisch `happy-dom`, falls es installiert ist; ansonsten greift sie auf `jsdom` zurück. Eines der beiden Pakete muss vorhanden sein. ```bash @@ -91,20 +93,20 @@ Suche in der Datei `angular.json` das `test`-Target deines Projekts und setze de } ``` -Der `unit-test`‑Builder verwendet standardmäßig `"tsConfig": "tsconfig.spec.json"` und `"buildTarget": "::development"`. +Der `unit-test`‑Builder verwendet standardmäßig `"tsConfig": "tsconfig.spec.json"` und `"buildTarget": "::development"`. Falls dein Projekt andere Werte benötigt, etwa weil die `development`-Konfiguration fehlt oder spezielle Test‑Einstellungen nötig sind, kannst du eine eigene Build-Konfiguration anlegen und zuweisen, z. B. `testing`. -Der vorherige Builder `@angular/build:karma` erlaubte es, Build‑Optionen (wie `polyfills`, `assets`, `styles`) direkt im `test`-Target zu definieren. Der neue Builder `@angular/build:unit-test` unterstützt das nicht. -Falls sich deine Test‑Build‑Optionen von der `development`-Konfiguration unterscheiden, musst du diese Optionen in eine eigene Build-Konfiguration verschieben. +Der vorherige Builder `@angular/build:karma` erlaubte es, Build‑Optionen (wie `polyfills`, `assets`, `styles`) direkt im `test`-Target zu definieren. Der neue Builder `@angular/build:unit-test` unterstützt das nicht. +Falls sich deine Test‑Build‑Optionen von der `development`-Konfiguration unterscheiden, musst du diese Optionen in eine eigene Build-Konfiguration verschieben. Stimmen sie bereits mit `development` überein, ist kein weiterer Schritt notwendig. -> **Tipp:** Alternativ kannst du einfach ein neues Projekt mittels `ng new` erzeugen und die relevanten Abschnitte aus der neu generierten `angular.json` in dein bestehendes Projekt übernehmen. +> **Tipp:** Alternativ kannst du einfach ein neues Projekt mittels `ng new` erzeugen und die relevanten Abschnitte aus der neu generierten `angular.json` in dein bestehendes Projekt übernehmen. > So erhältst du automatisch eine saubere Vorlage für die Vitest-Konfiguration. #### 3. Eigene `karma.conf.js`‑Konfiguration berücksichtigen -Eigene Einstellungen aus der Datei `karma.conf.js` werden nicht automatisch migriert. +Eigene Einstellungen aus der Datei `karma.conf.js` werden nicht automatisch migriert. Prüfe diese Datei, bevor du sie löschst, und übertrage relevante Optionen manuell. Viele Karma‑Optionen besitzen Vitest‑Entsprechungen, die du in einer `vitest.config.ts` definieren kannst und dann über `runnerConfig` in der `angular.json` einbindest. @@ -118,7 +120,7 @@ Weitere Einstellungen findest du in der offiziellen [Vitest‑Dokumentation](htt #### 4. Karma- und `test.ts`‑Dateien entfernen -Du kannst nun die Dateien `karma.conf.js` sowie `src/test.ts` löschen und alle Karma‑bezogenen Pakete deinstallieren. +Du kannst nun die Dateien `karma.conf.js` sowie `src/test.ts` löschen und alle Karma‑bezogenen Pakete deinstallieren. Die folgenden Befehle entsprechen einem Standard‑Angular‑Projekt. In deinem Projekt können weitere Pakete vorhanden sein. @@ -160,7 +162,7 @@ Der Browsername hängt vom verwendeten Provider ab (z. B. `chromium` bei Playw } ``` -Der Headless‑Modus wird automatisch aktiviert, wenn die Umgebungsvariable `CI` gesetzt ist oder der Browsername "Headless" enthält (z. B. `ChromeHeadless`). +Der Headless‑Modus wird automatisch aktiviert, wenn die Umgebungsvariable `CI` gesetzt ist oder der Browsername "Headless" enthält (z. B. `ChromeHeadless`). Andernfalls läuft der Browser sichtbar. ### Automatisches Test‑Refactoring per Schematic @@ -250,8 +252,8 @@ Dabei gibt es zwei mögliche Wege: Entweder verweist du direkt auf eine bestimmt ``` Alternativ kannst du die Angular CLI automatisch suchen lassen. -Bei automatischer Suche setzt du `"runnerConfig": true` in der `angular.json`. -Der Builder sucht dann selbstständig nach einer Datei namens `vitest-base.config.*`, zunächst im Projektverzeichnis und anschließend im Workspace-Root. +Bei automatischer Suche setzt du `"runnerConfig": true` in der `angular.json`. +Der Builder sucht dann selbstständig nach einer Datei namens `vitest-base.config.*`, zunächst im Projektverzeichnis und anschließend im Workspace-Root. So kannst du beispielsweise gemeinsame Einstellungen zentral definieren und bequem wiederverwenden. @@ -264,8 +266,8 @@ Neu lernen musst du vor allem Jasmine‑spezifische Stellen. ### Globale Funktionen -Die bekannten globalen Testfunktionen wie `describe`, `it` bzw. `test`, `beforeEach`, `afterEach` und `expect` bleiben in Vitest unverändert erhalten. -Sie stehen ohne weitere Importe zur Verfügung, sofern in deiner `tsconfig.spec.json` der Eintrag `types: ["vitest/globals"]` gesetzt ist. +Die bekannten globalen Testfunktionen wie `describe`, `it` bzw. `test`, `beforeEach`, `afterEach` und `expect` bleiben in Vitest unverändert erhalten. +Sie stehen ohne weitere Importe zur Verfügung, sofern in deiner `tsconfig.spec.json` der Eintrag `types: ["vitest/globals"]` gesetzt ist. Trotzdem empfehlen wir, diese Funktionen explizit zu importieren. Dadurch vermeidest du mögliche Namenskollisionen, etwa mit gleichnamigen Funktionen aus Cypress, was in der Vergangenheit regelmäßig zu verwirrenden Problemen geführt hat. @@ -295,7 +297,7 @@ expect(flag).toBe(false); #### 2) `toHaveBeenCalledOnceWith()` gibt es in Jest/Vitest nicht -Jasmine hat einen praktischen Matcher für einen Spy mit der Prüfung auf "genau einmal und genau mit diesen Argumenten". +Jasmine hat einen praktischen Matcher für einen Spy mit der Prüfung auf "genau einmal und genau mit diesen Argumenten". Als Ersatz verwendest du einfach [`toHaveBeenCalledExactlyOnceWith()`](https://vitest.dev/api/expect.html#tohavebeencalledexactlyoncewith): ```ts @@ -310,8 +312,8 @@ expect(spy).toHaveBeenCalledExactlyOnceWith(book); #### 3) Asynchrone Matchers: `expectAsync(...)` (Jasmine) vs. `.resolves/.rejects` (Jest/Vitest) -Jasmine hat eine [eigene Async-API](https://jasmine.github.io/api/5.12/async-matchers): `await expectAsync(promise).toBeResolved() / toBeRejectedWith(...)`. -Jest/Vitest nutzen stattdessen das Muster [`await expect(promise).resolves/...`](https://vitest.dev/api/expect.html#resolves) bzw. [`.rejects/...`](https://vitest.dev/api/expect.html#rejects). +Jasmine hat eine [eigene Async-API](https://jasmine.github.io/api/5.12/async-matchers): `await expectAsync(promise).toBeResolved() / toBeRejectedWith(...)`. +Jest/Vitest nutzen stattdessen das Muster [`await expect(promise).resolves/...`](https://vitest.dev/api/expect.html#resolves) bzw. [`.rejects/...`](https://vitest.dev/api/expect.html#rejects). Beim Umstieg müssen diese Expectations umgeschrieben werden. ```ts @@ -326,10 +328,10 @@ await expect(doWork()).resolves.toBe('OK'); await expect(doWork()).rejects.toThrow('Boom'); ``` -Vitest zielt also bei den Matchern auf Jest‑Kompatibilität ab. -Kompatibilität mit Jasmine steht hingegen überhaupt nicht im Fokus. -In der Praxis ist der Anpassungsaufwand meist gering (vor allem bei `toBeTrue`/`toBeFalse` und `toHaveBeenCalledOnceWith`), aber er existiert. -Bei asynchronen Erwartungen unterscheidet sich das Pattern sogar deutlich. +Vitest zielt also bei den Matchern auf Jest‑Kompatibilität ab. +Kompatibilität mit Jasmine steht hingegen überhaupt nicht im Fokus. +In der Praxis ist der Anpassungsaufwand meist gering (vor allem bei `toBeTrue`/`toBeFalse` und `toHaveBeenCalledOnceWith`), aber er existiert. +Bei asynchronen Erwartungen unterscheidet sich das Pattern sogar deutlich. Aber keine Sorge: Die Wahrscheinlichkeit, dass dein Projekt `expectAsync` verwendet, ist sehr gering, da in der Angular-Dokumentation stattdessen immer Angular-spezifische Hilfsfunktionen gezeigt wurden. Daher dürfte in den meisten Projekten hier wahrscheinlich gar keine zusätzliche Arbeit anfallen. @@ -420,8 +422,8 @@ Das Angular-Team hat sich [bewusst für das Standard-Vitest-Verhalten entschiede ### Asynchronität ohne Zone.js mit Vitest Timer -Seit Angular 21 laufen Unit-Tests standardmäßig zoneless. -Das bedeutet: Die früheren Angular-Hilfsfunktionen `waitForAsync()` und `fakeAsync()`/`tick()` funktionieren nicht mehr automatisch, weil sie auf Zone.js basieren. +Seit Angular 21 laufen Unit-Tests standardmäßig zoneless. +Das bedeutet: Die früheren Angular-Hilfsfunktionen `waitForAsync()` und `fakeAsync()`/`tick()` funktionieren nicht mehr automatisch, weil sie auf Zone.js basieren. Entscheidend ist: Das hat nichts mit Vitest zu tun. Auch unter Jasmine hätte man in einer zonenlosen Umgebung auf diese Utilitys verzichten müssen. @@ -450,8 +452,8 @@ Modern ist nur die Schreibweise, bei der es zwischen Jasmine und Vitest keinen U Der zweite Angular-Klassiker [`fakeAsync()`](https://angular.dev/api/core/testing/fakeAsync) und [`tick()`](https://angular.dev/api/core/testing/tick) braucht hingegen einen echten Ersatz. (Hinweis: Diese beiden Helfer sind nicht Bestandteil von Jasmine, sondern kommen aus `@angular/core/testing`.) Vitest bringt ein eigenes [Fake-Timer-System](https://vitest.dev/api/vi.html#fake-timers) mit. -Die Nutzung erfordert etwas Einarbeitung, denn nicht alle Timer funktionieren gleich und nicht jeder Test braucht dieselben Werkzeuge. -Beginnen wir mit einem einfachen zeitbasierten Beispiel. +Die Nutzung erfordert etwas Einarbeitung, denn nicht alle Timer funktionieren gleich und nicht jeder Test braucht dieselben Werkzeuge. +Beginnen wir mit einem einfachen zeitbasierten Beispiel. Die folgende Funktion erhöht einen Counter nach genau fünf Sekunden: ```ts @@ -489,7 +491,7 @@ describe('startFiveSecondTimer', () => { Es eignet sich besonders gut, wenn du eine ganz bestimmte Zeitspanne simulieren oder mehrere Timer in korrekt getakteter Reihenfolge ablaufen lassen möchtest. -Doch nicht alle Timer sind so einfach. +Doch nicht alle Timer sind so einfach. Manchmal besteht der Code nur aus timerbasierten Aktionen, aber ohne zusätzliche Promises. Das folgende Beispiel inkrementiert einen Counter mehrfach, indem es ausschließlich Timeouts und Intervals nutzt: ```ts @@ -565,12 +567,25 @@ describe('startAsyncJob', () => { }); ``` -`runAllTimersAsync()` ist damit ein guter Ersatz für Tests, bei denen bisher `fakeAsync()` und `tick()` in Kombination mit Microtask-Flushing verwendet wurden. +`runAllTimersAsync()` ist damit ein guter Ersatz für Tests, bei denen bisher `fakeAsync()` und `tick()` in Kombination mit Microtask-Flushing verwendet wurden. + +#### Migration mit der Angular CLI + +Mit Angular 22 stellt die Angular CLI ein eigenes Schematic bereit, das diese Umstellung weitgehend automatisch erledigt: + +```bash +ng generate @schematics/angular:fake-async-to-vitest-fake-timers +``` + +Das Schematic ersetzt `fakeAsync(...)`-Wrapper durch entsprechende Aufrufe von `vi.useFakeTimers()` bzw. `vi.useRealTimers()`, übersetzt `tick(ms)` in `vi.advanceTimersByTime(ms)` und kümmert sich auch um die nötigen Imports aus `vitest`. +Wo eine eindeutige Übersetzung nicht möglich ist (etwa bei komplexen Microtask-Flows), hinterlässt das Schematic einen TODO-Kommentar – diese Stellen müssen wir dann manuell auf `runAllTimers()` oder `runAllTimersAsync()` umstellen. + +So lässt sich die Migration auch in größeren Projekten gut vorantreiben, ohne jeden einzelnen Test von Hand anzufassen. ### TestBed und ComponentFixture -Nach all den kleinen, aber subtilen Unterschieden zwischen Jasmine und Vitest gibt es hier gute Nachrichten: -Die Verwendung von `TestBed` und `ComponentFixture` bleibt vollständig unverändert, da dies kein Thema ist, das Vitest berührt. +Nach all den kleinen, aber subtilen Unterschieden zwischen Jasmine und Vitest gibt es hier gute Nachrichten: +Die Verwendung von `TestBed` und `ComponentFixture` bleibt vollständig unverändert, da dies kein Thema ist, das Vitest berührt. Du erzeugst weiterhin deine Komponenten oder Services mithilfe von `TestBed`. Auch der explizite Aufruf von `fixture.detectChanges()` ist unverändert notwendig, um die Change Detection manuell anzustoßen. @@ -580,15 +595,15 @@ Auch der explizite Aufruf von `fixture.detectChanges()` ist unverändert notwend Spezielle Karma-Anwendungsfälle wie eigene Karma-Plugins oder individuelle Browser‑Launcher lassen sich erwartungsgemäß nicht direkt auf Vitest übertragen. Du wirst im Vitest-Ökosystem nach Alternativen suchen müssen. -Bei der Umstellung auf Vitest kann eine kurze Gewöhnungsphase im Team nötig sein, da bestimmte neue API-Konzepte wie `vi.spyOn`, `vi.fn` oder Strategien zum Zurücksetzen von Mocks zwar leicht zu erlernen sind, sich aber dennoch von Jasmine unterscheiden. +Bei der Umstellung auf Vitest kann eine kurze Gewöhnungsphase im Team nötig sein, da bestimmte neue API-Konzepte wie `vi.spyOn`, `vi.fn` oder Strategien zum Zurücksetzen von Mocks zwar leicht zu erlernen sind, sich aber dennoch von Jasmine unterscheiden. Achte deshalb darauf, dass deine Tests mögliche Manipulationen an globalen Objekten vollständig aufräumen und verwende dafür idealerweise Methoden wie [`afterEach`](https://vitest.dev/api/#aftereach) mit [`vi.restoreAllMocks()`](https://vitest.dev/api/vi.html#vi-restoreallmocks). ## Fazit -Mit Vitest als Standard in Angular 21 wird das Testen deutlich moderner und schneller. -Die Umstellung ist meist unkompliziert, die Migrations‑Schematics helfen beim Einstieg. -Wo früher `fakeAsync` und Zone.js‑Magie nötig waren, reichen heute `async/await` und flexible Fake‑Timer. +Mit Vitest als Standard in Angular 21 wird das Testen deutlich moderner und schneller. +Die Umstellung ist meist unkompliziert, die Migrations‑Schematics helfen beim Einstieg. +Wo früher `fakeAsync` und Zone.js‑Magie nötig waren, reichen heute `async/await` und flexible Fake‑Timer. Und wenn es realistisch sein muss, steht dir der Browser‑Modus zur Verfügung. Insgesamt bedeutet das: kürzere Feedback‑Schleifen, robustere Tests und weniger Reibung im Alltag. Viel Spaß beim Testen! diff --git a/blog/2026-06-angular22/README.md b/blog/2026-06-angular22/README.md new file mode 100644 index 00000000..49d94af3 --- /dev/null +++ b/blog/2026-06-angular22/README.md @@ -0,0 +1,437 @@ +--- +title: 'Angular 22 ist da!' +author: Angular Buch Team +mail: team@angular-buch.com +published: 2026-06-01 +lastModified: 2026-06-01 +keywords: + - Angular + - Angular 22 + - Signal Forms + - Resource API + - httpResource + - rxResource + - Fetch API + - OnPush + - Debounced Signals + - Service Decorator + - injectAsync + - WebMCP + - Angular ARIA + - Vitest + - Webpack +language: de +header: angular22.jpg +sticky: true +hidden: true +isUpdatePost: true +--- + +Mit Beginn des Monats Juni gibt es Neuigkeiten aus der Angular-Welt: **Angular 22** ist da! +Dieses Release zieht viele Konzepte, die in den letzten Versionen reifen durften, über die Ziellinie: +**Signal Forms**, die **Resource API** und das Paket **`@angular/aria`** sind stable, der `HttpClient` setzt nun standardmäßig auf die moderne Fetch-API und ein neuer `@Service()`-Decorator stellt eine vereinfachte Version vom bisherigen `@Injectable()` dar. +Hinzu kommen einige spannende neue Bausteine wie `injectAsync()`, `debounced()` und eine erste Integration für **WebMCP**. +Auf der Werkzeug-Seite werden die alten Webpack-basierten Builder als veraltet markiert, und die Angular CLI bringt einen neuen Migrationspfad von `fakeAsync` auf die Fake Timers von Vitest mit. + +Im [Angular-Blog](TODO) findest du die offiziellen Informationen zum neuen Release. +Um ein bestehendes Projekt auf Angular 22 zu migrieren, kannst du den Befehl `ng update` verwenden, siehe [Angular Update Guide](https://angular.dev/update-guide). + + + +## Versionen von TypeScript und Node.js + +Die folgenden Versionen von TypeScript und Node.js sind für Angular 22 notwendig: + +- TypeScript: >=6.0.0 <6.1.0 +- Node.js: ^22.22.0 || ^24.13.1 || >=26.0.0 + +Ausführliche Infos zu den unterstützten Versionen findest du in der [Angular-Dokumentation](https://angular.dev/reference/versions). + + +## Signal Forms sind stable + +Mit Angular 21 wurden die *Signal Forms* als experimentelles Feature eingeführt – jetzt, ein halbes Jahr später, sind sie offiziell **stabil**. +Damit hat Angular einen ganz neuen Ansatz für die Verarbeitung von Formularen im Werkzeugkasten, der konsequent auf Signals setzt. + +Die Grundidee ist denkbar einfach: Die Formulardaten werden als ganz normales Signal geführt. +Aus dieser Datenstruktur leitet Angular automatisch die Form-Struktur ab. +Validierungsregeln werden über eine schemabasierte API mit Funktionen wie `schema()`, `required()` oder `minLength()` deklariert. +Im Template kommt nur noch eine einzige Direktive zum Einsatz: `[formField]`. + +```ts +import { schema, form, FormField, required, minLength } from '@angular/forms/signals'; + +const bookFormSchema = schema(fieldPath => { + required(fieldPath.title); + minLength(fieldPath.isbn, 10); +}); + +@Component({ + imports: [FormField], + template: ` + + + `, +}) +export class BookForm { + protected readonly bookData = signal({ title: '', isbn: '' }); + protected readonly bookForm = form(this.bookData, bookFormSchema); +} +``` + +Mit der Stabilisierung sind die APIs und Konzepte nun verlässlich – der Einsatz in Produktion ist offiziell empfohlen. +Wir gehen davon aus, dass *Reactive Forms* und *Template-Driven Forms* perspektivisch durch Signal Forms abgelöst werden. +Bestehende Reactive Forms müssen aber nicht über Bord geworfen werden: +Über die Compat-Schicht `@angular/forms/signals/compat` lassen sich beide Welten miteinander verzahnen. +Eine ausführliche Anleitung mit Top-down- und Bottom-up-Strategien gibt es im [Migration Guide](https://angular.dev/guide/forms/signals/migration). + +In den letzten Monaten haben wir uns intensiv mit Signal Forms beschäftigt und eine vierteilige Blogpost-Serie veröffentlicht: + +- [**Part 1: Getting Started with the Basics**](/blog/2025-10-signal-forms-part1) +- [**Part 2: Advanced Validation and Schema Patterns**](/blog/2025-10-signal-forms-part2) +- [**Part 3: Child Forms and Custom UI Controls**](/blog/2025-10-signal-forms-part3) +- [**Part 4: Metadata and Accessibility Handling**](/blog/2025-12-signal-forms-part4) + + +## Resource API ist stable + +Die zweite große Neuerung betrifft das Laden asynchroner Daten: +Die **Resource API** ist mit Angular 22 stabil. +Konkret betrifft das die Funktionen `resource()` und `rxResource()` aus `@angular/core` sowie `httpResource()` aus `@angular/common/http`. + +Eine Resource repräsentiert einen asynchron geladenen Datensatz. +Sie liefert nicht nur den geladenen Wert, sondern auch reaktive Statusinformationen wie `isLoading`, `error` und `value` – jeweils als Signal. +Damit lässt sich der gesamte Datenladeprozess elegant in Komponenten abbilden, ohne sich um Subscriptions oder manuelles State-Management kümmern zu müssen. + +Die drei Varianten unterscheiden sich in ihrem Loader: + +- `resource()` arbeitet mit Promise-basierten Loadern. +- `rxResource()` ist die Brücke zur RxJS-Welt: Hier liefert der Loader einen Observable. +- `httpResource()` ist die HTTP-spezifische Variante. Sie nutzt unter der Haube den `HttpClient` und unterstützt damit auch alle HTTP-Interceptors. + +```ts +import { httpResource } from '@angular/common/http'; + +@Service() +export class BookStore { + selectedIsbn = signal(null); + + book = httpResource(() => { + const isbn = this.selectedIsbn(); + return isbn ? `/api/books/${isbn}` : undefined; + }); +} +``` + +Wir haben die Idee der Resource API bereits in einem ausführlichen Blogpost vorgestellt: +[**Reactive Angular: Daten laden mit der Resource API**](/blog/2024-10-resource-api). +Mit der Stabilisierung in Angular 22 ist das dort beschriebene Vorgehen offiziell der empfohlene Weg, um in Komponenten signal-basiert Daten zu laden. + + +## Angular ARIA ist stable + +Auch das mit Angular 21 eingeführte Paket [`@angular/aria`](https://angular.dev/guide/aria/overview) hat den Schritt aus der Developer Preview heraus geschafft und ist mit Angular 22 **stable**. +Das Paket bietet eine Sammlung von Direktiven, die gängige [WAI-ARIA-Patterns](https://www.w3.org/WAI/ARIA/apg/patterns/) umsetzen – von Accordion über Combobox bis hin zu Tabs und Tree. +Tastaturinteraktionen, ARIA-Attribute, Fokus-Management und Screen-Reader-Unterstützung sind dabei bereits eingebaut. +Wir liefern lediglich die HTML-Struktur, das Styling und die fachliche Logik. + +Mit dem Sprung zu stable können wir die Direktiven nun bedenkenlos in produktiven Anwendungen einsetzen. +Die Installation erfolgt wie gewohnt über die Angular CLI: + +```bash +ng add @angular/aria +``` + + +## HttpClient: Fetch-API ist jetzt der Default + +Der `HttpClient` hat eine kleine, aber wirkungsvolle Veränderung erfahren: +Mit Angular 22 ist die **Fetch-API** der neue Standard. +Bisher musste die Fetch-Variante explizit über `withFetch()` aktiviert werden – andernfalls verwendete der `HttpClient` das ältere `XMLHttpRequest`. +Nun wird `FetchBackend` automatisch verwendet, ganz ohne zusätzliche Konfiguration. + +Da seit Angular 21 die Providers für den `HttpClient` automatisch eingebunden werden, reicht es, den `HttpClient` per `inject()` in unseren Komponenten und Services zu nutzen. +Ein expliziter Aufruf von `provideHttpClient()` in der `app.config.ts` ist nicht mehr nötig – Fetch funktioniert ab Angular 22 ganz von allein. + +```ts +@Service() +export class BookStore { + // HttpClient ist out of the box verfügbar – mit Fetch als Default + private http = inject(HttpClient); +} +``` + +Die Vorteile: bessere Kompatibilität mit Server-Side Rendering, eine moderne Browser-API und ein etwas schlankeres Bundle, weil der XHR-Pfad nicht mehr standardmäßig benötigt wird. + +Allerdings ist diese Umstellung ein Breaking Change, der eine wichtige Einschränkung mit sich bringt: +Das `FetchBackend` unterstützt **keine Upload-Progress-Events**. +Wer in seiner Anwendung mit `reportProgress: true` den Fortschritt von Datei-Uploads tracken möchte, muss bei den betroffenen Requests explizit auf das XHR-Backend zurückwechseln. +Dafür rufen wir `provideHttpClient()` weiterhin manuell auf und konfigurieren das XHR-Backend: + +```ts +export const appConfig: ApplicationConfig = { + providers: [ + provideHttpClient(withXhr()) + ] +}; +``` + + +## ChangeDetection.OnPush ist jetzt Default + +Mit Angular 22 wird ein weiterer großer Schritt in Richtung Performance gegangen: +**`ChangeDetectionStrategy.OnPush` ist nun die Standard-Strategie** für alle Komponenten. +Dies basiert auf dem [RFC zum Thema](https://github.com/angular/angular/discussions/66779), an dem die Community lange mitdiskutiert hat. + +Komponenten, in denen die `changeDetection`-Property nicht explizit gesetzt wurde, verhalten sich also ab sofort wie zuvor `OnPush`. +Damit setzt das Angular-Team konsequent den eingeschlagenen Weg fort: +Mit Angular 21 wurde Zoneless Change Detection zum Standard, Signals sind seit Längerem das zentrale Reaktivitätsprimitiv – und nun ist auch die granulare Change Detection per Default aktiv. +Das Ergebnis ist eine bessere Performance "out of the box", weil unnötige Change-Detection-Durchläufe vermieden werden. + +Bei der Migration gibt es allerdings eine Stolperfalle: +Komponenten, die ihren View-Status über direkte Property-Zuweisungen aus einer Subscription heraus aktualisieren, ohne zusätzlich `markForCheck()` aufzurufen, können stillschweigend "einfrieren". +Die Daten kommen an, aber die Anzeige im Template aktualisiert sich nicht – weil Angular nicht mehr automatisch erkennt, dass eine Aktualisierung nötig ist. + +Die saubere Lösung ist, Subscriptions auf Signals umzustellen, beispielsweise mit `toSignal()`. +Alternativ kann man explizit `markForCheck()` aufrufen oder den Wert über die `async`-Pipe in das Template binden. +Wer schon konsequent auf Signals setzt, muss in seinen eigenen Komponenten in der Regel gar nichts anpassen. + +Besondere Vorsicht ist bei eigenen Bibliotheken gefragt: +Library-Autor:innen sollten ihre Komponenten überprüfen und – falls die Komponenten sich auf das alte Verhalten verlassen – die `changeDetection`-Property explizit auf `ChangeDetectionStrategy.Default` setzen, damit nichts unerwartet bricht. + + +## HTML-Kommentare in Angular-Templates + +Eine kleine, aber im Alltag sehr nützliche Verbesserung betrifft die Templates: +Angular 22 erlaubt nun **Kommentare innerhalb von Template-Elementen** – zusätzlich zu den klassischen HTML-Kommentaren ``. + +Bisher konnte man Attribute, Inputs oder Event-Bindings in einem mehrzeiligen Element-Tag nicht einfach auskommentieren oder mit einer kurzen Notiz versehen. +Jetzt akzeptiert der Template-Parser auch JavaScript-typische Kommentare im Stil `// ...` für einzelne Zeilen sowie `/* ... */` für mehrzeilige Kommentare – direkt zwischen den Attributen. + +```html + + +``` + +## Debounced Signals + +Mit Angular 22 zieht eine neue experimentelle Funktion in `@angular/core` ein: **`debounced()`**. +Sie ermöglicht es, ein Signal zu *entprellen*, also seinen Wert erst nach einer kurzen Wartezeit weiterzureichen. +Das ist ein Klassiker bei Such-Eingabefeldern: Während die Nutzer:innen tippen, soll nicht nach jedem Tastendruck eine Anfrage abgeschickt werden – sondern erst, wenn die Eingabe zur Ruhe gekommen ist. + +Bisher war dieses Muster fest in der Welt von RxJS verankert: Man musste das Signal mit `toObservable()` in einen Observable umwandeln, `debounceTime()` verwenden und das Ergebnis mit `toSignal()` zurückkonvertieren. +Mit `debounced()` geht das nun ohne Umwege direkt in der Signal-Welt. + +```ts +import { debounced, resource, signal } from '@angular/core'; + +@Component({/* ... */}) +export class Search { + query = signal(''); + debouncedQuery = debounced(this.query, 300); + + results = resource({ + params: () => this.debouncedQuery.value(), + loader: ({ params }) => fetchResults(params), + }); +} +``` + +Die Funktion `debounced()` liefert eine `Resource` zurück, deren Wert erst nach Ablauf der angegebenen Wartezeit (in Millisekunden) aktualisiert wird. +Während des Wartens hat die Resource den Status `loading`, danach `resolved`. +Statt einer festen Millisekundenzahl kann auch eine eigene Wait-Funktion übergeben werden, die ein `Promise` zurückgibt – damit lassen sich z. B. unterschiedliche Wartezeiten je nach Eingabelänge realisieren. + +Wichtig: `debounced()` muss in einem Injection Context aufgerufen werden, damit Angular die zugehörigen Timer beim Zerstören des Injectors automatisch aufräumen kann. + +In den Signal Forms gibt es zusätzlich die verwandte Funktion `debounce()`, mit der sich asynchrone Validatoren entprellen lassen – etwa, um nicht bei jedem Tastendruck eine Server-seitige Eindeutigkeitsprüfung anzustoßen. + + +## Der neue Decorator `@Service()` + +Mit Angular 22 wurde der neue Decorator `@Service()` eingeführt. +Er ist die moderne und ergonomische Alternative zum etablierten Decorator `@Injectable()` mit der Einstellung `providedIn: 'root'`. + +Da das Klassennamen-Suffix `Service` [mit Angular 20 weggefallen ist](/blog/2025-05-angular20), ist der neue Decorator aus unserer Sicht eine sinnvolle Ergänzung. +So ist auf den ersten Blick erkennbar, dass es sich bei einer Klasse um einen Service handelt. + +Der Decorator kann in den meisten Fällen direkt ersetzt werden: + +```ts +// VORHER +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class BookStore {} +``` + +```ts +// NACHHER +import { Service } from '@angular/core'; + +@Service() +export class BookStore {} +``` + +Die Angular CLI generiert Services mit `ng generate service` nun ebenfalls standardmäßig mit dem neuen Decorator. +Um beim Generieren den älteren Decorator `@Injectable()` zu erhalten, können wir das Flag `--injectable` verwenden. + +```bash +# mit Decorator `@Service()` +ng g service book-store + +# mit Decorator `@Injectable()` +ng g service book-store --injectable +``` + +Im Vergleich zu `@Injectable()` sieht `@Service()` keine Konfigurationsmöglichkeiten vor und ist damit bewusst schlank gehalten. +Eine wichtige Eigenschaft sollte man dabei kennen: **Constructor Injection ist mit `@Service()` nicht erlaubt**. +Dependencies müssen über die Funktion `inject()` aufgelöst werden – andernfalls wirft Angular einen Fehler. +Diese Einschränkung schiebt uns sanft, aber bestimmt in Richtung des modernen, funktionalen DI-Stils. + +Für spezielle Fälle wie `providedIn: 'platform'` benötigen wir weiterhin den Decorator `@Injectable()`. +Wir müssen also nicht befürchten, dass `@Injectable()` in naher Zukunft "deprecated" wird. +Wir empfehlen dennoch, neue Services mit dem neuen Decorator auszustatten – die Syntax ist kürzer und es sieht auch ein wenig schicker aus. + +> Übrigens: Das Konzept eines `@Service()`-Decorators für Angular wurde von Johannes als Gedankenexperiment in einem [eigenen Blogpost](/blog/2025-09-service-decorator) durchgespielt – jetzt gibt es ihn wirklich! + + +## `injectAsync()`: Services lazy laden + +Ein weiteres neues Werkzeug im Bereich Dependency Injection ist die Funktion **`injectAsync()`** aus `@angular/core`. +Damit lassen sich Services und ihre Abhängigkeiten **lazy laden**, ohne dass sie im initialen Bundle der Anwendung landen. + +Bisher war das Pattern für lazy geladene Services umständlich: +Man musste den `Injector` per `inject()` holen, den Service dynamisch importieren und das Ergebnis selbst über `Injector.get()` auflösen und cachen. +Mit `injectAsync()` übernimmt Angular all diese Schritte automatisch. +Die Funktion bekommt einen Loader übergeben, der die Service-Klasse über einen dynamischen `import()` zurückgibt. +Beim ersten Aufruf des zurückgegebenen Promise wird der Service geladen, durch die DI aufgelöst und für nachfolgende Aufrufe gecacht. + +```ts +import { Component, injectAsync, onIdle, signal } from '@angular/core'; + +@Component({/* ... */}) +export class PostEditor { + protected readonly content = signal(''); + + private markdownService = injectAsync( + () => import('../markdown.service').then(m => m.MarkdownService), + { prefetch: onIdle } + ); + + async preview() { + const svc = await this.markdownService(); + // ... + } +} +``` + +Der Vorteil ist greifbar: Schwere Abhängigkeiten – etwa Markdown-Parser, Charting-Bibliotheken oder PDF-Renderer – tauchen nicht mehr im Initial-Bundle auf. +Sie werden erst dann nachgeladen, wenn die jeweilige Funktion aufgerufen wird. + +Optional kann eine **Prefetch-Strategie** angegeben werden. +Mit `prefetch: onIdle` lädt Angular die Abhängigkeit ruhig im Hintergrund, sobald der Browser idle ist. +So bleibt das Initial-Bundle schlank, und die Nutzer:innen müssen trotzdem nicht warten, wenn sie das Feature später aufrufen – die Datei ist dann bereits im Cache. + + +## WebMCP: KI-Agenten in Web-Apps integrieren + +Ein Thema, das uns in den nächsten Jahren noch intensiv begleiten wird, ist die Integration von KI-Agenten in unsere Anwendungen. +Mit dem aufkommenden Webstandard **[WebMCP](https://github.com/MiguelsPizza/WebMCP)** lässt sich aus einer Webseite heraus dem Browser – und damit angeschlossenen KI-Agenten – ein Set von **Tools** zur Verfügung stellen. + +Tools werden über die neue Browser-API `navigator.modelContext.registerTool()` registriert. +Externe Agenten wie Claude, ChatGPT-Erweiterungen oder Gemini können diese Tools entdecken und auf Wunsch der Nutzer:innen aufrufen. +Damit verschmelzen Web-App und Agent: Aktionen, die sonst über die UI ausgeführt werden, können auch direkt aus einem Chat-Kontext heraus ausgelöst werden. + +Angular 22 bringt dafür erste Bausteine mit, um diese Welt sauber an die Komponenten- und Service-Architektur anzudocken: + +- Tools können direkt in Services oder Komponenten definiert und über die Dependency Injection bereitgestellt werden. +- Das Lifecycle-Handling übernimmt Angular: Tools werden automatisch wieder abgemeldet, wenn die zugehörige Komponente zerstört wird. + +```ts +// TODO: konkretes Codebeispiel und Quelle prüfen (Doku/PR-Link), sobald die offizielle Doku verfügbar ist +``` + +### Signal Forms: `experimentalWebMcpTool` + +Besonders charmant ist die Brücke zwischen Signal Forms und WebMCP: +Mit der Funktion **`experimentalWebMcpTool`** lässt sich ein Formular deklarativ als WebMCP-Tool registrieren. +Angular leitet das JSON-Schema automatisch aus der Form-Definition ab – inklusive aller Validierungsregeln. + +Damit kann ein KI-Agent ein Formular stellvertretend "ausfüllen", ohne dass wir per Hand ein eigenes Tool definieren müssen. +Praktische Anwendungsfälle gibt es viele: Eine Buchsuche, eine Buchung oder eine strukturierte Datenerfassung, die der Agent im Auftrag der Nutzer:innen vornimmt. + +```ts +// TODO: Beispielcode aus offizieller Doku prüfen / einfügen +import { form, schema, experimentalWebMcpTool } from '@angular/forms/signals'; + +protected readonly bookForm = form(this.bookData, bookFormSchema, { + webMcpTool: experimentalWebMcpTool({ + name: 'create-book', + description: 'Legt ein neues Buch im System an.', + }), +}); +``` + +Wie der Name verrät, ist diese Schnittstelle noch experimentell. +Die genaue Form der API kann sich also noch ändern – wir behalten das Thema im Auge und werden hier in Kürze einen ausführlichen Blogpost dazu veröffentlichen. + + +## Deprecation der Webpack-basierten Builder + +Auf der Werkzeug-Seite zieht das Angular-Team einen weiteren Schlussstrich: +Die alten **Webpack-basierten Builder** (`@angular-devkit/build-angular:browser` und `@angular-devkit/build-angular:dev-server`) sind mit Angular 22 offiziell als **deprecated** markiert. + +Schon seit einigen Versionen ist der esbuild-basierte `application`-Builder der Standard für neue Projekte – er ist deutlich schneller, unterstützt SSR direkt und integriert sich nahtlos in den Vitest-Test-Runner. +Wer noch auf einer Webpack-Konfiguration unterwegs ist, sollte spätestens jetzt die Migration zum neuen Builder einplanen. +Die Angular CLI stellt dafür ein passendes Migrationsskript bereit, das die `angular.json` automatisch umstellt: + +```bash +ng update @angular/cli --name use-application-builder +``` + +Eine Entfernung der Webpack-Builder ist in einem der kommenden Major-Releases geplant. + + +## fakeAsync zu Vitest Fake Timers migrieren + +Mit Angular 21 wurde Vitest zum neuen Standard-Test-Runner. +Wer bestehende Tests migriert, stößt früher oder später auf eine Stolperfalle: +Die altbekannten Helfer `fakeAsync()` und `tick()` aus `@angular/core/testing` basieren auf Zone.js und passen nicht mehr ohne Weiteres zum neuen, zonenlosen Setup. +Vitest bringt mit den **Fake Timers** ein eigenes, modernes Konzept zur Steuerung von Zeit in Tests mit. + +Mit Angular 22 stellt die Angular CLI ein Schematic bereit, das Tests automatisch von `fakeAsync`/`tick` auf die Fake Timers von Vitest umstellt: + +```bash +ng generate @schematics/angular:fake-async-to-vitest-fake-timers +``` + +Das Schematic ersetzt die `fakeAsync`-Wrapper durch `vi.useFakeTimers()`, übersetzt `tick(...)` in `vi.advanceTimersByTime(...)` und kümmert sich um die zugehörigen Imports. +In unserem [Vitest-Migrationsleitfaden](/blog/2025-11-zu-vitest-migrieren#asynchronit%C3%A4t-ohne-zonejs-mit-vitest-timer) haben wir die verschiedenen Vitest-Timer-APIs ausführlich erklärt und zeigen auch, in welchen Fällen das Schematic an seine Grenzen stößt. + + +## Sonstiges + +Im Changelog von [Angular](https://github.com/angular/angular/releases) und der [Angular CLI](https://github.com/angular/angular-cli/releases) findest du stets alle Detailinformationen zur aktuellen Entwicklung des Frameworks. +Einige interessante Aspekte haben wir hier zusammengetragen: + +- **TODO:** TODO (siehe [PR](https://github.com/angular/angular/pull/TODO)). +- **TODO:** TODO (siehe [Commit](https://github.com/angular/angular/commit/TODO)). + +
+ +Wir wünschen dir viel Spaß beim Entwickeln mit Angular 22! +Hast du Fragen zur neuen Version von Angular oder zu unserem Buch? Schreibe uns! + +**Viel Spaß wünschen +Ferdinand, Danny und Johannes** + +
+ +**Titelbild:** TODO. Foto von Ferdinand Malcher