Mit Angular 22 verlassen mehrere zentrale Signal-APIs den experimentellen Status: Die Resource API und Signal Forms sind nun stabil und produktiv einsetzbar. Parallel wird OnPush zur neuen Standard-Change-Detection-Strategie, und das Framework bekommt mit @Service, injectAsync und debounced ergonomischere Bausteine für moderne, reaktive Anwendungen.
Dieser Artikel zeigt die wichtigsten Neuerungen von Angular 22 anhand konkreter Beispiele und greift dabei auch ausgewählte Highlights aus Angular 21.1 und 21.2 auf.
📂 Source Code (Branch ng22)
OnPush als neue Standard-Change-Detection-Strategie
Eine Entscheidung, die in der Angular-Community seit Anfang an diskutiert wurde, ist nun Realität: OnPush ist die neue Standard-Change-Detection-Strategie. Diese Umstellung macht mit dem Aufkommen von Signals und Zone-less Angular konsequent Sinn: Wer Signals verwendet, bekommt präzise Benachrichtigungen über Änderungen, und OnPush nutzt diese optimal aus. Das Ergebnis ist eine performante Change Detection, die sich zielgerichtet auf die von Änderungen betroffenen Komponenten konzentriert.
Wer das bisherige Verhalten benötigt, setzt die Strategie manuell auf Eager:
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'app-legacy',
changeDetection: ChangeDetectionStrategy.Eager,
template: `...`
})
export class LegacyCmp { [...] }Die Einstellung Eager ersetzt die ursprüngliche Einstellung Default, die nun deprecated ist. In diesem Fall prüft die Change Detection den gesamten Komponentenbaum auf Änderungen.
Um Breaking Changes zu vermeiden, aktiviert ng update bei der Aktualisierung der Angular-Version die Option Eager, falls OnPush nicht explizit gesetzt wurde.
Resource API wird stabil in Angular 22
Die Resource API war bislang das fehlende Bindeglied im Signal-Ökosystem: Sie ermöglicht das reaktive, asynchrone Ableiten von Daten, typischerweise HTTP-Anfragen, die nach der Änderung von Signals ausgelöst werden. Trotz ihrer zentralen Rolle blieb sie lange im experimentellen Status. Das ändert sich mit Angular 22 schlagartig: resource, rxResource und httpResource sind nun stabil und können bedenkenlos produktiv eingesetzt werden.
Den bequemsten Einstieg bietet die httpResource-Funktion. Sie erhält einen Lambda-Ausdruck, der eine Http-Anfrage zurückliefert. Dieser Ausdruck ist reaktiv: Ändert sich ein darin verwendetes Signal, startet die Anfrage automatisch neu.
import { httpResource } from '@angular/common/http';
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { Flight } from '../../data/flight';
@Component({
selector: 'app-flight-search',
changeDetection: ChangeDetectionStrategy.OnPush,
[...]
})
export class FlightSearch {
protected readonly filter = signal({ from: 'Hamburg', to: 'Graz' });
protected readonly flightsResource = httpResource<Flight[]>(
() => ({
url: 'https://demo.angulararchitects.io/api/flight',
params: {
from: this.filter().from,
to: this.filter().to,
},
}),
{ defaultValue: [] },
);
protected readonly flights = this.flightsResource.value;
protected readonly error = this.flightsResource.error;
protected readonly isLoading = this.flightsResource.isLoading;
protected search(): void {
this.flightsResource.reload();
}
}Der Typ-Parameter Flight[] legt den erwarteten Antworttyp fest. Das defaultValue-Argument stellt sicher, dass die Komponente beim Start nicht mit undefined konfrontiert wird. Soll die Resource keinen Request auslösen, genügt es, undefined zurückzugeben:
protected readonly flightsResource = httpResource<Flight[]>(
() => {
const filter = this.filter();
if (!filter.from || !filter.to) {
return undefined;
}
return {
url: 'https://demo.angulararchitects.io/api/flight',
params: { from: filter.from, to: filter.to },
};
},
{ defaultValue: [] },
);Die Resource verwaltet ihren Zustand über Signals: value enthält die geladenen Daten, error liefert Fehlerinformationen, und isLoading zeigt den Ladezustand an. Dazu kennt die Resource einen detaillierteren status mit den Werten idle, loading, reloading, error, resolved und local (für lokal überschriebene Werte).
Im Template lassen sich diese Signals direkt nutzen, etwa um den Ladezustand anzuzeigen und die geladenen Daten zu iterieren:
@if (flightsResource.isLoading()) {
<div>Loading ...</div>
}
@if (flightsResource.error()) {
<div>Error: {{ flightsResource.error() }}</div>
} @else {
<div class="row">
@for (flight of flightsResource.value(); track flight.id) {
<app-flight-card [item]="flight" />
}
</div>
}Race Conditions werden automatisch behandelt: Treffen mehrere Anfragen in schneller Folge ein, wird nur das Ergebnis der jeweils neuesten verwendet und ältere, sofern noch möglich, abgebrochen. Das entspricht der Funktionsweise von switchMap in RxJS.
Incremental Hydration jetzt standardmäßig aktiv
Mit Angular 22 aktiviert provideClientHydration() die Incremental Hydration automatisch. Wer sie nicht benötigt, deaktiviert sie explizit mit dem neuen withNoIncrementalHydration()-Feature. Eine mitgelieferte Schematic-Migration hilft beim Update.
Signal Forms wird stabil und produktiv einsetzbar
Auch Signal Forms kann nun produktiv eingesetzt werden. Der Weg von der experimentellen API zur stabilen Version verlief bemerkenswert schnell. Möglich wurde dies durch umfangreiche interne Fallstudien bei Google, in denen typische Formularanwendungen systematisch untersucht wurden.
Das Herzstück von Signal Forms ist die form-Funktion. Sie erhält ein Signal mit den Formulardaten sowie eine optionales Schema mit Validierungsregeln:
import { linkedSignal } from '@angular/core';
import { form, minLength, required } from '@angular/forms/signals';
@Component({ [...] })
export class FlightEdit {
private readonly store = inject(FlightDetailStore);
protected readonly flight = linkedSignal(() =>
normalizeFlight(this.store.flight()),
);
protected readonly flightForm = form(this.flight, (path) => {
required(path.from);
required(path.to);
required(path.date);
minLength(path.from, 3);
});
}Das Ergebnis ist ein FieldTree: eine tief verschachtelte Signal-Struktur, bei der jedes Property als Signal mit Formularstatus (value, dirty, invalid, errors) repräsentiert wird. Für die Template-Bindung kommt die FormField-Direktive zum Einsatz:
<input [formField]="flightForm.from" id="flight-from" />
<div>{{ flightForm.from().errors() | json }}</div>Neuer @Service-Dekorator: Services kürzer registrieren
Eine auffällige Neuerung ist der neue @Service-Dekorator. Er ersetzt die häufigen Fälle, in denen man bisher @Injectable() oder @Injectable({ providedIn: 'root' }) schrieb, und passt besser zur eigentlichen Intention, einen Service bereitzustellen:
import { Service } from '@angular/core';
@Service()
export class FlightClient { [...] }Standardmäßig wird der Service im Root-Scope bereitgestellt. Wer das nicht möchte, kann den Service stattdessen manuell bereitstellen, etwa in der app.config.ts, auf Komponenten- oder Routen-Ebene. In diesem Fall setzt man autoProvided auf false:
@Service({ autoProvided: false })
export class TabRegistry { [...] }injectAsync: Services lazy injizieren
Mit injectAsync lassen sich Abhängigkeiten lazy injizieren, also erst dann, wenn sie tatsächlich benötigt werden. Das ist besonders nützlich für Services, die umfangreiche Bibliotheken laden und erst bei einer konkreten Benutzeraktion gebraucht werden.
import { injectAsync } from '@angular/core';
@Component({ [...] })
export class CheckinPage {
private readonly upgradeService = injectAsync(() =>
import('./upgrade-service').then((m) => m.UpgradeService),
);
protected async upgrade(): Promise<void> {
const flightNumber = this.checkinFormModel().ticketId;
const upgradeService = await this.upgradeService();
upgradeService.upgrade(flightNumber);
}
}Die Funktion injectAsync erhält einen Lambda-Ausdruck, der ein Promise zurückgibt. Das Ergebnis ist eine Funktion, deren erster Aufruf sich um das Laden des Services kümmert. Der Import des UpgradeService und damit das Laden des zugehörigen Bundles findet erst beim ersten Aufruf von upgrade() statt.
Damit Lazy Loading funktioniert, muss der injizierte Service auto-provided sein, also entweder mit @Injectable({ providedIn: 'root' }) oder dem neuen @Service()-Dekorator dekoriert werden.
Prefetching mit injectAsync und onIdle
Lazy Loading bedeutet zwangsläufig eine Verzögerung beim ersten Aufruf. Um diese zu vermeiden, kann das Bundle bereits im Voraus geladen werden.
Genau dafür bietet injectAsync die Option prefetch: Sie verweist auf eine Funktion, die ein Promise liefert. Sobald der Promise resolved wird, beginnt Angular mit dem Laden des Services.
Dieser Mechanismus lässt sich gemeinsam mit der Hilfsfunktion onIdle nutzen. Sie liefert ein Promise und resolved es, sobald der Browser nichts zu tun hat:
import { injectAsync, onIdle } from '@angular/core';
private readonly upgradeService = injectAsync(
() => import('./upgrade-service').then((m) => m.UpgradeService),
{ prefetch: onIdle },
);Intern delegiert onIdle an requestIdleCallback und fällt auf setTimeout zurück, wenn der Browser diese API nicht unterstützt. Bei Bedarf lässt sich ein Timeout konfigurieren, nach dem das Prefetching spätestens angestoßen wird:
injectAsync(
() => import('./upgrade-service').then((m) => m.UpgradeService),
{ prefetch: () => onIdle({ timeout: 100 }) },
);Wer das Verhalten projektweit anpassen will, kann den zugrundeliegenden IdleService über provideIdleServiceWith, typischerweise in der app.config.ts, austauschen.
Resource-Komposition über Snapshots
Ein Grundprinzip der reaktiven Programmierung ist es, Werte voneinander abzuleiten. Ein Computed entsteht aus einem oder mehreren Signals und aktualisiert sich, sobald sich seine Quellen ändern. Bei Resources war dieses Ableiten bislang nur indirekt möglich: Man konnte zwar deren einzelnen Signals (value, error, isLoading) projizieren, aber nicht die Resource als Ganzes. Seit Angular 21.2 steht dafür ein eigenes Konzept bereit: Über sogenannte Snapshots lässt sich eine Resource vollständig in eine neue Resource transformieren, ohne die ursprüngliche Ladelogik zu berühren.
Der Ausgangspunkt ist das snapshot-Signal einer Resource, das den vollständigen aktuellen Zustand inklusive Status und Wert enthält. Ein Snapshot liefert also den gesamten Zustand als Objekt mit Signals. Dieses Objekt lässt sich auf ein neues Objekt mit davon abgeleiteten Signals abbilden. Aus diesem abgeleiteten Snapshot baut resourceFromSnapshots wiederum eine neue Resource.
Zur Veranschaulichung dient ein Beispiel, das die geladenen Daten filtert, etwa um nur Gepäckstücke ab einem bestimmten Mindestgewicht anzuzeigen:
import {
linkedSignal,
Resource,
resourceFromSnapshots,
ResourceSnapshot,
Signal,
} from '@angular/core';
export function withMinWeight(
input: Resource<Luggage[]>,
minWeight: Signal<number>,
): Resource<Luggage[]> {
const derived = linkedSignal<
{ snap: ResourceSnapshot<Luggage[]>; min: number },
ResourceSnapshot<Luggage[]>
>({
source: () => ({ snap: input.snapshot(), min: minWeight() }),
computation: ({ snap, min }) => {
if (snap.status === 'resolved') {
return { ...snap, value: snap.value.filter((item) => item.weight >= min) };
}
return snap;
},
});
return resourceFromSnapshots(derived);
}Über das computation-Callback des linkedSignal entscheidet man, wie der neue Snapshot aus den Quellsignals zusammengesetzt wird. Ändert sich entweder die Quell-Resource oder das minWeight-Signal, wird die Berechnung automatisch neu ausgeführt. Die abgeleitete Resource bleibt also stets konsistent mit ihren Eingaben.
Das Muster lässt sich generisch für beliebige Transformationen nutzen. Ein interessanter Anwendungsfall, den auch das Angular-Team gemeinsam mit Snapshots beschreibt, ist es, beim Neuladen den zuletzt geladenen Wert beizubehalten, anstatt undefined anzuzeigen:
export function withPreviousValue<T>(input: Resource<T>): Resource<T> {
const derived = linkedSignal<ResourceSnapshot<T>, ResourceSnapshot<T>>({
source: input.snapshot,
computation: (snap, previous) => {
if (snap.status === 'loading' && previous?.value?.status === 'resolved') {
return { ...snap, value: previous.value.value };
}
return snap;
},
});
return resourceFromSnapshots(derived);
}Das previous-Argument des computation-Callbacks enthält dabei den zuletzt produzierten Snapshot der abgeleiteten Resource. So lässt sich der vorherige Wert gezielt in den neuen Snapshot übernehmen.
debounced: Debouncing für Signals und Resources
Signals wissen von Natur aus nichts von Zeit: Im Gegensatz zu Observables kennen sie deswegen auch kein Konzept für Verzögerung oder Throttling. Das macht Debouncing in reinen Signal-Chains bisher unmöglich, zumindest dann, wenn man dieses mentale Modell nicht brechen will.
Für Formulare hat das Angular-Team deswegen Debouncing direkt in Signal Forms eingebaut, was einen Großteil der Anwendungsfälle abdeckt. Für alles darüber hinaus gibt es nun die Funktion debounced.
Im Gegensatz zu Signals kennen Resources sehr wohl Zeit. Genau hier setzt debounced an. Die Funktion erzeugt eine Resource, deren Wert um den angegebenen Zeitraum verzögert aktualisiert wird. Der status der Resource zeigt dabei an, ob der Wert noch im Pending-Fenster liegt:
import { debounced } from '@angular/core';
const filter = signal('');
const debouncedFilter = debounced(filter, 300); // 300ms
effect(() => console.log(debouncedFilter.value()));FormRoot und Submission API in Signal Forms
Bereits seit Angular 21.2 steht die Submission API für Signal Forms bereit. Sie erlaubt es, die gesamte Logik zum Absenden eines Formulars direkt beim Aufruf von form zu definieren:
import { FormRoot, submit } from '@angular/forms/signals';
@Component({
imports: [ [...], FormRoot ],
[...]
})
export class FlightEdit {
protected readonly flightForm = form(this.flight, flightSchema, {
submission: {
action: async (form) => this.save(form),
ignoreValidators: 'none',
onInvalid: (form) => this.reportValidationError(form),
},
});
}Die Eigenschaft action enthält die asynchrone Speicherlogik und kann serverseitige Validierungsfehler als Rückgabewert zurückliefern, welche Signal Forms dann direkt in den Formularstatus übernimmt. Die Option ignoreValidators steuert, ob fehlschlagende oder noch ausstehende Validatoren das Absenden blockieren (none | pending | all). onInvalid wird aufgerufen, wenn die Validierung das Absenden verhindert.
Im Template wird der FieldTree, der das gesamte Formular repräsentiert, über die neue FormRoot-Direktive an das form-Tag gebunden. Diese Direktive übernimmt drei Aufgaben: Sie unterdrückt das standardmäßige Validierungsverhalten des Browsers (etwa die nativen Tooltips bei required-Feldern), verbindet die action aus der Submission-Konfiguration mit dem submit-Event des Formulars und verhindert doppelte Validierungsmeldungen. Zum Auslösen des submit-Events genügt ein gewöhnlicher button ohne explizites type-Attribut. Er wirkt innerhalb eines Formulars standardmäßig als Submit-Button:
<form [formRoot]="flightForm">
[...]
<button>Save</button>
</form>Benötigt man weitere Absende-Aktionen, z. B. für einen Genehmigungsworkflow, hilft die submit-Hilfsfunktion, die die Übermittlung nur dann ausführt, wenn das Formular gültig ist:
import { submit } from '@angular/forms/signals';
protected async requestApproval(): Promise<void> {
await submit(this.flightForm, {
action: async (form) => {
await this.store.requestApproval(form().value());
},
ignoreValidators: 'none',
onInvalid: (form) => this.reportValidationError(form),
});
}Der onInvalid-Handler eignet sich darüber hinaus dazu, nach einer fehlgeschlagenen Validierung den Fokus automatisch auf das erste ungültige Eingabefeld zu setzen. Signal Forms stellt dafür die Methode focusBoundControl() bereit:
private reportValidationError(form: FieldTree<Flight>): void {
this.snackBar.open('Please correct the validation errors', 'OK');
const errors = form().errorSummary();
if (errors.length > 0) {
errors[0].fieldTree().focusBoundControl();
}
}Modern Angular
✓ Bereits auf Angular 22 aktualisiert!
Mehr zu Signal Forms und moderner Angular-Architektur findest du in meinem neuen eBook Modern Angular. Es behandelt Signals, Architektur, Testing, KI-Assistenten und praxistaugliche Lösungen für moderne Business-Anwendungen.
CSS-Klassen für Signal Forms
Signal Forms unterstützt seit Angular 21.2 bedingte CSS-Formatierung, wie man sie von Template-driven und Reactive Forms kennt. Über provideSignalFormsConfig definiert man zunächst eine Zuordnung von CSS-Klassennamen zu Formularstatus-Predicaten:
import { provideSignalFormsConfig } from '@angular/forms/signals';
export const appConfig: ApplicationConfig = {
providers: [
[...],
provideSignalFormsConfig({
classes: {
'ng-invalid': field => field.state().invalid(),
'ng-valid': field => field.state().valid(),
'ng-dirty': field => field.state().dirty(),
'ng-pristine': field => !field.state().dirty(),
'ng-pending': field => field.state().pending(),
}
}),
],
};Die zugehörigen CSS-Regeln sehen dann beispielsweise so aus:
input.ng-valid {
border-left: 3px solid darkseagreen;
}
input.ng-invalid.ng-dirty {
border-left: 3px solid var(--color-error);
}
input.ng-pending {
border-left: 3px solid var(--color-border);
}Signal Forms wendet die konfigurierten Klassen automatisch auf die gebundenen Eingabefelder an:

Wer exakt dieselben Klassen wie bei Reactive oder Template-driven Forms verwenden möchte, kann das vorgefertigte Konfigurationsobjekt NG_STATUS_CLASSES aus dem Compat-Namespace nutzen:
import { NG_STATUS_CLASSES } from '@angular/forms/signals/compat';
provideSignalFormsConfig({
classes: NG_STATUS_CLASSES
}),Interop von Signal Forms mit Reactive Forms
Signal Forms lässt sich nahtlos mit bestehenden Reactive Forms integrieren. Die Brücke compatForm aus @angular/forms/signals/compat erlaubt es, ein Signal-Form-Modell mit reaktiven Form-Controls zu verbinden:
import { compatForm, SignalFormControl } from '@angular/forms/signals/compat';
@Component({ [...] })
export class CheckinPage {
protected readonly address = new SignalFormControl(
this.addressFormModel(),
(path) => {
required(path.street);
required(path.zipCode);
required(path.country);
},
);
protected readonly checkinForm = compatForm(this.checkinFormModel, (path) => {
required(path.ticketId);
});
}SignalFormControl verhält sich dabei wie ein regulärer AbstractControl und kann direkt in bestehende Reactive-Forms-Strukturen eingebettet werden. Umgekehrt versteht Signal Forms auch Legacy Form Controls, CVA-basierte (Control Value Accessor) Inputs und klassische Validatoren. Signal Forms kann damit auch mit bestehenden Legacy Form Controls arbeiten. Die Interop-Brücke zur CVA kann bei Bedarf pro Feld deaktiviert werden:
<input ngNoCva [field]="myField">validateStandardSchema mit dynamischen Regeln
Standard Schema ist ein community-getriebenes Interface, das Validierungsbibliotheken wie Zod und Valibot implementieren. Da Signal Forms mit validateStandardSchema eine Funktion mitbringt, die dieses Interface direkt versteht, lassen sich Schemata aus all diesen Bibliotheken für die Formularvalidierung nutzen, ohne dass dafür ein eigener Adapter geschrieben werden muss.
Seit Angular 21.2 kann sich das Schema zudem dynamisch anpassen. Statt eines fixen Schema-Objekts übergibt man validateStandardSchema einen Lambda-Ausdruck, der intern in ein computed umgewandelt wird. Ändert sich ein darin verwendetes Signal, wird das Computed automatisch neu ausgewertet und die aktualisierten Validierungsregeln gelten sofort:
import { Signal } from '@angular/core';
import { SchemaPathTree, validateStandardSchema } from '@angular/forms/signals';
import { z } from 'zod';
import { Flight } from './flight';
const FlightZodSchema = z.object({
id: z.number().int(),
from: z.string().min(3).max(20),
to: z.string().min(3).max(20),
date: z.string(),
delayed: z.boolean(),
});
const StrictFlightZodSchema = z.object({
id: z.number().int(),
from: z.string().min(10).max(30),
to: z.string().min(10).max(30),
[...]
});
export function validateWithSchema(
path: SchemaPathTree<Flight>,
strict: Signal<boolean>,
) {
validateStandardSchema(
path,
() => strict() ? StrictFlightZodSchema : FlightZodSchema,
);
}Damit lassen sich z. B. kontextabhängige Validierungsstrategien umsetzen. Das Formular wechselt automatisch zwischen lockerem und striktem Schema, sobald sich das strict-Signal ändert.
disabled, readonly und hidden mit when-Eigenschaft
Signal Forms kennt die Hilfsfunktionen disabled, readonly und hidden, um Eingabefelder abhängig vom Formularstatus zu steuern. Die Neuerung: Die Bedingung wird nun über eine when-Eigenschaft im Parameterobjekt übergeben. Das macht die API konsistenter und ermöglicht es, im Fehlerfall auch eine erklärende Zeichenkette statt true zurückzugeben:
import { disabled, hidden, readonly } from '@angular/forms/signals';
disabled(path.delay, {
when: (ctx) => (ctx.valueOf(path.delayed) ? false : 'not delayed'),
});
readonly(path.delay, {
when: (ctx) => ctx.valueOf(path.delayed),
});
hidden(path.delay, {
when: (ctx) => ctx.valueOf(path.delayed),
});Der ctx-Parameter stellt dabei kontextuellen Zugriff auf andere Feldwerte bereit, sodass komplexe, feldübergreifende Bedingungen sauber ausgedrückt werden können.
Route Auto Cleanup für Environment Injectors
Services lassen sich in Angular auch auf Routen-Ebene konfigurieren, und zwar über die providers-Eigenschaft einer Route-Konfiguration. Bisher gab es dabei allerdings einen Haken: Die so registrierten Services wurden beim Verlassen der Route nicht aufgeräumt, sondern lebten bis zum Schließen der Anwendung weiter. Der Grund liegt im historischen Verhalten der zugrundeliegenden Environment Injectors. Sie sind das Gegenstück zu jenen Providern, die früher auf NgModule-Ebene eingerichtet wurden, und ihr ursprüngliches Lebenszyklus-Verhalten sollte beibehalten werden.
Seit Angular 21.1 lässt sich das ändern. Mit withExperimentalAutoCleanupInjectors werden Environment Injectors einer Route inklusive aller dort registrierten Service-Instanzen beim Verlassen der Route automatisch zerstört:
import {
provideRouter,
withComponentInputBinding,
withExperimentalAutoCleanupInjectors,
} from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withComponentInputBinding(),
withExperimentalAutoCleanupInjectors(),
),
[...]
],
};Das Feature bleibt vorläufig experimentell.
Aktive Routen mit isActive als Signal ermitteln
Mit isActive bringt Angular 22 eine neue Möglichkeit, programmatisch zu ermitteln, ob eine Route aktiv ist. Die Funktion liefert ein Signal und wird im Template automatisch neu ausgewertet, sobald sich der Routenzustand ändert:
import { isActive, Router } from '@angular/router';
@Component({ [...] })
export class BookingNavigation {
private readonly router = inject(Router);
protected readonly flightSearchActive = isActive(
'/ticketing/booking/flight-search', this.router
);
protected readonly passengerSearchActive = isActive(
'/ticketing/booking/passenger-search', this.router
);
protected readonly summaryActive = isActive(
'/ticketing/booking/summary', this.router,
{ paths: 'exact' }
);
}Der optionale dritte Parameter nimmt IsActiveMatchOptions entgegen und steuert, wie genau der Vergleich ausfallen soll:
paths('exact'|'subset'): Müssen alle Segmente übereinstimmen, oder reicht ein Subset?matrixParams('exact'|'subset'|'ignored'): Vergleich der Matrix-Parameter der übereinstimmenden Segmente.queryParams('exact'|'subset'|'ignored'): Vergleich der Query-Parameter.fragment('exact'|'ignored'): Vergleich des URL-Fragments.
Standardmäßig gilt für paths Subset-Matching: Eine Route gilt als aktiv, wenn sie ein Subset der aktuellen URL ist. Im Beispiel wurde für summaryActive paths: 'exact' gesetzt, damit die Route exakt übereinstimmen muss. Im Template nutzt man das Signal direkt für die aktive CSS-Klasse:
<a [routerLink]="['./flight-search']" [class.active]="flightSearchActive()">
Flight
</a>HttpClient in Angular 22: FetchBackend als Standard
Der HttpClient setzt ab Angular 22 standardmäßig auf das FetchBackend. Das explizite withFetch() ist damit deprecated und kann entfernt werden.
Der Hintergrund: Die Fetch API ist heute in allen relevanten Browsern und modernen JavaScript-Runtimes verfügbar. Gegenüber XMLHttpRequest (XHR) bietet sie eine modernere, Promise-basierte API, bessere Unterstützung für Streaming-Szenarien und eignet sich besser als gemeinsame HTTP-Abstraktion für Browser und SSR. Allerdings unterstützt Fetch keine Upload-Progress-Events.
Passend dazu ersetzt Angular 22 die bisherige pauschale reportProgress-Option (deprecated) durch zwei dedizierte Varianten, die Upload- und Download-Fortschritt getrennt schalten:
// Download-Fortschritt (funktioniert mit Fetch)
http.get('/large-file', { reportDownloadProgress: true, observe: 'events' });
// Upload-Fortschritt (erfordert withXhr())
http.post('/upload', file, { reportUploadProgress: true, observe: 'events' });Wird reportUploadProgress zusammen mit dem FetchBackend verwendet, wirft Angular eine Exception. Das ist ein bewusst harter Hinweis, dass in diesem Fall withXhr() erforderlich ist.
Wer das ursprüngliche Verhalten benötigt, zum Beispiel für eine Fortschrittsanzeige beim Upload, wechselt zurück zu XHR:
provideHttpClient(withXhr());Bei der Versions-Aktualisierung ergänzt ng update automatisch withXhr(). Das verhindert unerwünschte Verhaltensänderungen in bestehenden Anwendungen.
Neuerungen an der Template-Syntax in Angular 22
Angular 21.1, 21.2 und die Vorabversionen von Angular 22 haben eine Reihe nützlicher Erweiterungen für Template-Ausdrücke mitgebracht, die hier zusammengefasst werden. Zur Veranschaulichung dienen Beispiele aus den jeweiligen Pull Requests.
Seit Angular 21.1 unterstützen Templates Object-Spread, Array-Spread und Rest-Argumente in Funktionsaufrufen, also Syntax, die bislang nur in TypeScript-Klassen erlaubt war:
<div [class]="{ ...baseClasses, active: isActive() }"></div>
<ul>
@for (item of [...preferred, ...rest]; track $index) {
<li>{{ item }}</li>
}
</ul>
{{ sum(...numbers()) }}Ebenfalls seit 21.1 unterstützt @switch mehrere aufeinanderfolgende @case-Marker für denselben Block, analog zum Fall-Through in anderen Sprachen:
@switch (state) {
@case ('a')
@case ('b') { <p>A oder B</p> }
@case ('c') { <p>C</p> }
@default { <p>Sonst</p> }
}Angular 21.2 hat Arrow Functions mit implizitem Rückgabewert in Template-Ausdrücken ergänzt. Sie sind besonders praktisch in Kombination mit @for und Event-Bindings:
@for (item of items(); track item.id) {
<button (click)="select((x) => x.id === item.id)">…</button>
}Pfeilfunktionen mit Block-Body ({ … }) und Pipes im Body sind nicht zulässig. Funktionen, die ausschließlich eigene Parameter nutzen, hebt der Compiler auf Modul-Ebene; Template-kontext-bezogene Funktionen werden auf der View gespeichert, um Identitätsstabilität zu gewährleisten.
Ebenfalls seit 21.2 sind Typprüfungen mit instanceof direkt im Template möglich:
@if (event instanceof MouseEvent) {
<p>ClientX: {{ event.clientX }}</p>
}Auch erschöpfende Fallunterscheidungen für @switch kamen in 21.2: Mit @default never; am Ende eines @switch-Blocks prüft TypeScript zur Compile-Zeit, ob alle Varianten eines Union-Typs abgedeckt sind. Wird die Union später erweitert und ein neuer Wert nicht behandelt, schlägt die Übersetzung fehl:
state: 'loggedOut' | 'loading' | 'loggedIn' = 'loggedOut';@switch (state) {
@case ('loggedOut') { <button>Login</button> }
@case ('loading') { <p>Loading ...</p> }
@case ('loggedIn') { <p>Welcome back!</p> }
@default never;
}Hier ist der @switch-Ausdruck (state) selbst die Union. Im @default-Zweig erkennt TypeScript, dass state nach Abdeckung aller Fälle den Typ never hat. Wird die Union später z. B. um 'banned' erweitert ohne entsprechendes @case, schlägt der Compiler an.
Häufig switcht man jedoch nicht auf die Union selbst, sondern auf eine ihrer Properties, etwa den Discriminator einer diskriminierten Union. TypeScript kann dann zwar die abgefragte Property einengen, aber nicht erkennen, ob die übergeordnete Union dadurch vollständig abgedeckt ist. Angular 22 schließt diese Lücke. Mit never(<ausdruck>) lässt sich gezielt angeben, welcher Ausdruck auf vollständige Abdeckung geprüft werden soll:
state!: { mode: 'show'; menu: number } | { mode: 'hide' };@switch (state.mode) {
@case ('show') { {{ state.menu }} }
@case ('hide') {}
@default never(state);
}Die Angabe never(state) sagt dem Compiler explizit, dass er die vollständige Abdeckung gegen die übergeordnete state-Union prüfen soll. Wird sie später um ein weiteres mode ergänzt, ohne dass ein entsprechendes @case existiert, meldet der Template-Compiler den Fehler.
Angular 22 bringt zwei weitere Präzisierungen beim Umgang mit null und undefined. Das Verhalten von ?. im Template entspricht nun exakt JavaScript-Semantik: Bricht die Kette an einer null- oder undefined-Stelle ab, ist das Ergebnis undefined. Wer das alte Angular-spezifische Verhalten benötigt, kann den Ausdruck mit $null(...) umschließen:
{{ user?.profile?.name }} <!-- jetzt: undefined wenn Kette bricht -->Darüber hinaus erlaubt der Typprüf-Block nun echtes TypeScript-Narrowing über ?.. Nach einem Truthiness-Check wird der Typ wie in regulärem TS-Code eingeengt, sodass der Folgezugriff ohne ?. typsicher ist:
@Component({
template: `
@if (user?.isMember) {
{{ user.isMember }}
}
`,
})
export class UserComponent {
user?: { isMember: boolean };
}Schließlich sind in Angular 22 auch //- und /* … */-Kommentare innerhalb von HTML-Element-Definitionen erlaubt. Das ist praktisch, um lange Attributlisten zu strukturieren:
<div
// primary button
class="btn btn-primary"
/*
Achtung: greift nur, wenn `loading` false ist
*/
[disabled]="loading()"
></div>@defer: Optionaler Timeout für on idle
@defer (on idle) kann jetzt einen Timeout in Millisekunden erhalten, analog zu IdleRequestOptions.timeout. So lässt sich verhindern, dass ein Defer-Block endlos auf eine Idle-Phase wartet, die nie eintritt:
@defer (on idle(2000)) {
<heavy-widget />
}Weiteres im Überblick
httpResourceund Transfer State: Ressourcen, die auf dem Server vorgeladen werden, spielen nun nahtlos mit dem HTTP Transfer State zusammen. Das verhindert redundante HTTP-Anfragen beim ersten Rendern im Browser. Der Client verwendet einfach die bereits serverseitig geholten Daten.Web MCP Tools (
provideExperimentalWebMcpTools,declareExperimentalWebMcpTool): Angular-Anwendungen können KI-Tools direkt in den Injector registrieren. Beim Zerstören des Injectors werden die Tools automatisch wieder ausgetragen, ideal in Kombination mit Route-Providern undwithExperimentalAutoCleanupInjectors.ApplicationRef.bootstrapmit Config:bootstrap()auf derApplicationRefakzeptiert nun analog zucreateComponentein Konfigurationsobjekt. Das ist besonders relevant für Micro-Frontends, die bedarfsgesteuert in bestimmte Bereiche der Seite geladen und gebootstrappt werden:appRef.bootstrap(MyComponent, { hostElement: document.querySelector('#root')! }).Bootstrap unter Shadow Roots: Angular kann jetzt direkt unter einem Shadow Root gestartet werden. Styles werden im
SharedStylesHostkorrekt am übergeordneten Shadow Root registriert. Das ist ein weiterer Schritt Richtung sauberer Web-Component- und Micro-Frontend-Integration.Wildcard-Routen mit Trailing-Segmenten (seit 21.1): Das Wildcard-Segment
**darf nun von führenden und nachfolgenden Segmenten umgeben sein, z. B.'foo/**/bar'. Bisher war das nur mit einem eigenen Path-Matcher möglich. Das ist besonders für Shell-Anwendungen nützlich, die anhand eines Musters das passende Micro-Frontend laden sollen.AI-Runtime-Debugging: Im Dev-Mode registriert Angular KI-Debugging-Tools in der Seite. Dazu gehört
angular:di-graph, das den vollständigen Dependency-Injection-Graph (Element- und Environment-Injectors) für in-page KI-Assistenten bereitstellt. Das ermöglicht zukünftig KI-gestützte Analysen des DI-Graphen direkt im Browser.Trailing-Slash-Location-Strategien (seit 21.2): Mit
TrailingSlashPathLocationStrategyundNoTrailingSlashPathLocationStrategygibt es zwei neue Subklassen, die steuern, ob URLs in der Adressleiste mit oder ohne abschließendem/enden sollen.heightinImageLoaderConfig(seit 21.2): Der Image-Loader akzeptiert nun zusätzlich zurwidthauch eineheight-Angabe.Custom-Transformations für Image-Loader (seit 21.1): Die eingebauten Loader für Cloudflare, Cloudinary, ImageKit und Imgix akzeptieren ein
transform-Property für anbieterspezifische URL-Optionen:provideCloudflareLoader('https://cdn.example/', { transform: { format: 'webp', sharpen: 50 } }).TypeScript 6 (seit 21.2): Angular unterstützt nun TypeScript 6. TypeScript 5.9 wird nicht mehr unterstützt.
Node.js 26: Angular kompiliert und läuft nun offiziell auf Node.js 26.
Mehr dazu: Angular Architecture Workshop (Remote, Interaktiv, Advanced)
Werde zum Experten für unternehmensweite und langlebige Angular-Anwendungen mit unserem Angular Architecture Workshop!

Deutsche Version | English Version
FAQ zu Angular 22
Was sind die wichtigsten neuen Features in Angular 22?
Die Resource API (resource, rxResource, httpResource) und Signal Forms sind stabil. OnPush ist die neue Standard-Change-Detection-Strategie, Incremental Hydration ist standardmäßig aktiv. Dazu kommen @Service, injectAsync mit onIdle-Prefetching, die debounced-Funktion sowie zahlreiche Erweiterungen an Template-Syntax und Router.
Ist Signal Forms in Angular 22 produktiv einsetzbar?
Ja. Signal Forms hat den experimentellen Status verlassen. Inklusive Submission API, dynamischer Schemata über validateStandardSchema, bedingter CSS-Klassen und Interop mit Reactive Forms steht ein produktionsreifer Formular-Stack auf Signal-Basis bereit.
Muss ich nach dem Update auf Angular 22 meine Change-Detection-Strategie anpassen?
Im Regelfall nicht. ng update setzt für bestehende Komponenten ohne explizite Strategie automatisch ChangeDetectionStrategy.Eager, um das bisherige Verhalten zu erhalten. Wer aktiv von OnPush profitieren möchte, kann die Migration schrittweise pro Komponente vornehmen.
Ersetzt der @Service-Dekorator @Injectable() komplett?
Nein. @Service() ist eine ergonomischere Kurzform für den häufigsten Fall (providedIn: 'root'). @Injectable() bleibt weiterhin verfügbar und ist überall dort sinnvoll, wo abweichende Provider-Konfigurationen genutzt werden.
Fazit
Angular 22 markiert einen wichtigen Reifepunkt in der Signal-Ära: Mit der stabilen Resource API und Signal Forms stehen nun zwei der zentralen Bausteine für moderne, reaktive Angular-Anwendungen bereit für den Produktiveinsatz. Der neue @Service-Dekorator, die Debouncing-Funktion debounced, dynamische Schemata und die Submission API runden das Bild ab und zeigen, wie konsequent das Angular-Team an einem kohärenten, ergonomischen API-Design arbeitet.
Die Erweiterungen der Template-Syntax und die Neuerungen beim Router, wie das Auto Cleanup für Route-Injectors und die neue reaktive Funktion isActive, vervollständigen eine Version, die in ihrer Breite beeindruckt. Wer in den letzten Monaten mit dem Update gewartet hat, bekommt jetzt ein stabiles Fundament, das den Umstieg auf signalbasierte Entwicklung auch ohne experimentelle Features erlaubt.

