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..31661219 --- /dev/null +++ b/blog/2026-06-angular22/README.md @@ -0,0 +1,426 @@ +--- +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 +isUpdatePost: true +--- + +Es gibt wieder Neuigkeiten aus der Angular-Welt: **Angular 22** ist da! +Dieses Release zieht viele Konzepte über die Ziellinie: +**Signal Forms**, **Resource API** und **`@angular/aria`** sind stable. +Der `HttpClient` setzt nun standardmäßig auf die moderne Fetch API, und es wurde ein neuer `@Service()`-Decorator eingeführt. +Diese und einige weitere Neuerungen stellen wir in diesem Blogpost vor. + + +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). + + +## Unser neues Angular-Buch + +Ende Mai 2026 erschien unser neues Angular-Buch im Handel! In der neuen 1. Auflage vermitteln wir einen fundierten praktischen Einstieg in Angular. +Das Buch basiert auf der neuen Major-Version Angular 22 und ist auch für folgende Versionen geeignet. +Unter anderem behandeln wir ausführlich die neuen Signal Forms und die Resource API. + +Das [Beispielprojekt "BookManager"](https://bm1.angular-buch.com) aus dem Buch läuft ebenfalls aktuell auf Angular 22. + + +## 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: Die Formulardaten werden in einem Signal gespeichert, das von uns verwaltet wird. +Aus dieser Datenstruktur leitet Angular automatisch die Formularstruktur ab. +Validierungsregeln werden über eine schemabasierte API mit Funktionen wie `required()`, `minLength()` oder `validate()` deklariert. +Für die Datenbindung kommt im Template 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); +} +``` + +Die Schnittstellen und Konzepte sind stabil, und der Einsatz in Produktion wird 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) + +Auch in unserem neuen Angular-Buch findest du drei ausführliche Kapitel zu Signal Forms. + + +## Resource API ist stable + +Die Resource API wird mit Angular 22 ebenfalls als *stable* markiert! +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 Prozess zum Laden von Daten elegant 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 einem Promise-basierten Loader. +- `rxResource()` ist die Brücke zur RxJS-Welt: Die Resource verarbeitet ein 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 { + readonly selectedIsbn = signal(null); + + readonly book = httpResource(() => { + const isbn = this.selectedIsbn(); + return isbn ? `/api/books/${isbn}` : undefined; + }); +} +``` + +Wir haben die 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. + +Für schreibende Operationen wird allerdings weiterhin der `HttpClient` eingesetzt. Eine Resource eignet sich nur, um Daten zu laden, die als Signals bereitgestellt werden. + + +## Angular ARIA ist stable + +Das Paket [`@angular/aria`](https://angular.dev/guide/aria/overview) 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. + + +Das neue Paket gilt ab Angular 22 ebenfalls als **stable**. +Wir können die Direktiven nun also bedenkenlos in produktiven Anwendungen einsetzen. +Die Installation erfolgt wie gewohnt über die Angular CLI: + +```bash +ng add @angular/aria +``` + + + + +## 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! + + + + + +## 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 das Property `changeDetection` nicht explizit gesetzt wurde, verwenden jetzt automatisch die Strategie `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 Durchläufe der Change Detection vermieden werden. + +Wenn deine Anwendung schon durchgehend auf Signals basiert, sollte die Umstellung kein Problem sein. +Schon seit einigen Jahren wird empfohlen, `OnPush` einzusetzen, sodass viele Projekte bereits gut darauf abgestimmt sind. + +Für ältere Anwendungen hat die Migration allerdings Stolperfallen: +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 AsyncPipe 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.Eager` setzen, damit nichts unerwartet bricht. +`Eager` ist der neue Name für die alte Strategie `Default`. + + +## HttpClient: Fetch API ist jetzt der Default + +Der `HttpClient` nutzt nun unter der Haube standardmäßig die moderne Fetch API des Browsers. +Bisher musste Fetch explizit über `withFetch()` aktiviert werden, ansonsten verwendete der `HttpClient` das ältere `XMLHttpRequest`. + +Da seit Angular 21 die Providers für den `HttpClient` automatisch eingebunden werden, müssen wir für die Einrichtung nichts weiter tun: Wir können den `HttpClient` direkt per `inject()` in der Anwendung nutzen. +Fetch funktioniert ab Angular 22 ganz von allein, und ein expliziter Aufruf von `provideHttpClient()` in der `app.config.ts` ist nicht mehr nötig. + +```ts +@Service() +export class BookStore { + // HttpClient ist out of the box verfügbar – mit Fetch als Default + #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()) + ] +}; +``` + + + +## 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 + +Im neuen Release wurde die experimentelle Funktion **`debounced()`** vorgestellt. +Damit können wir ein Signal *entprellen*, sodass es seinen Wert erst nach einer kurzen Wartezeit ausgibt. +Das ist ein Klassiker bei Such-Eingabefeldern: Während der Eingabe 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 ein 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 { + protected readonly query = signal(''); + protected readonly debouncedQuery = debounced(this.query, 300); + + protected readonly 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 Signal Forms gibt es zusätzlich die verwandte Schema-Funktion `debounce()`, mit der sich asynchrone Validatoren entprellen lassen. +Dieses Hilfsmittel können wir z. B. einsetzen, um nicht bei jedem Tastendruck eine serverseitige Eindeutigkeitsprüfung anzustoßen. + + +## `injectAsync()`: Services lazy laden + +Ein weiteres neues Werkzeug im Bereich Dependency Injection ist die Funktion **`injectAsync()`**. +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. +Um den Service zu verwenden, müssen wir selbst die Promise verarbeiten, die von der Funktion ausgegeben wird, z. B. mit `async`/`await`. +Beim Aufruf wird die Klasse durch die Dependency Injection aufgelöst und für nachfolgende Aufrufe gecacht. + +```ts +import { Component, injectAsync, onIdle, signal } from '@angular/core'; + +@Component({ /* ... */ }) +export class PostEditor { + #markdownParser = injectAsync( + () => import('../markdown-parser').then(m => m.MarkdownParser), + { prefetch: onIdle } + ); + + async preview() { + const svc = await this.#markdownParser(); + // ... + } +} +``` + +Schwere Abhängigkeiten wie Markdown-Parser, Charting-Bibliotheken oder PDF-Renderer tauchen so 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 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 + +Angular 22 bringt experimentelle Unterstützung für **[WebMCP](https://github.com/webmachinelearning/webmcp)** (Web Model Context Protocol) mit. +Dieser aufkommende Webstandard ermöglicht es, aus einer Web-App heraus strukturierte Tools für KI-Agenten im Browser bereitzustellen. +Statt DOM-Scraping und simulierten Klicks können Agenten wie Claude oder Gemini die deklarierten Tools direkt aufrufen, z. B. um ein Formular auszufüllen oder eine Suche auszulösen. + +Angular dockt WebMCP sauber an die bestehende Architektur an: Tools lassen sich global, pro Route oder in Services und Komponenten registrieren. +Besonders elegant ist die Brücke zu Signal Forms: Mit der Option `experimentalWebMcpTool` in der Funktion `form()` wird ein Formular automatisch als WebMCP-Tool exponiert, inklusive JSON-Schema und Validierung. + +Wir haben dem Thema einen eigenen ausführlichen Artikel gewidmet: +[**WebMCP: KI-Agenten in Angular-Apps integrieren**](/blog/2026-05-webmcp) + + +## Webpack-basierte Builder sind deprecated + +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. + + +## Testing: `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:** Joshua Tree National Park, Kalifornien, USA, 2019. Foto von Ferdinand Malcher diff --git a/blog/2026-06-angular22/angular22.jpg b/blog/2026-06-angular22/angular22.jpg new file mode 100644 index 00000000..4eaf8980 Binary files /dev/null and b/blog/2026-06-angular22/angular22.jpg differ