Angular 19 ist da!
19.11.2024
Neben grauen Herbsttagen hat der November in Sachen Angular einiges zu bieten: Am 19. November 2024 wurde die neue Major-Version Angular 19 releaset! Angular bringt mit der Resource API und dem Linked Signal einige neue Features mit. Standalone Components müssen außerdem nicht mehr explizit als solche markiert werden. Wir stellen in diesem Blogpost alle wichtigen Neuerungen vor!
Im offiziellen Angular-Blog finden Sie alle offiziellen Informationen direkt vom Angular-Team.
Für die Migration auf Angular 19 empfehlen wir, den Befehl ng update
zu nutzen.
Detaillierte Infos zu den Schritten liefert der Angular Update Guide.
Standalone Components: der neue Standard
Standalone Components wurden mit Angular 15 eingeführt und haben sich seitdem zum Standard bei der Komponentenentwicklung mit Angular etabliert.
NgModules sind damit vollständig optional.
Ab Angular 19 ist es nicht mehr notwendig, eine Standalone Component explizit als solche zu markieren. Das Flag standalone: true
in den Metadaten der Komponente entfällt, denn der neue Standardwert ist true
:
// vor Angular 19
@Component({
selector: 'app-my',
templateUrl: './my.component.html',
standalone: true,
imports: []
})
export class MyComponent {}
// ⭐️ ab Angular 19
@Component({
selector: 'app-my',
templateUrl: './my.component.html',
imports: []
})
export class MyComponent {}
Modulbasierte Komponenten müssen nun explizit mit standalone: false
markiert werden.
Eine automatische Migration beim Update mit ng update
sorgt dafür, dass das Feld standalone
korrekt gesetzt wird.
Wir empfehlen unbedingt, durchgehend auf Standalone Components zu setzen und NgModules nur noch in Ausnahmefällen zu verwenden, wenn es für die Kompatibilität notwendig ist.
Übrigens: Mit Angular 19 wurde eine neue Compiler-Option eingeführt, die Standalone Components erzwingt. Setzen wir strictStandalone
in der Datei tsconfig.json
, müssen alle Komponenten im Projekt standalone sein.
{
"compilerOptions": { /* ... */ },
"angularCompilerOptions": {
"strictStandalone": true,
// ...
}
}
In einem ausführlichen Blogpost erklärt Alex Rickabaugh vom Angular-Team die Zukunft von NgModules. Auch wenn "die Zukunft standalone ist", bleiben NgModules weiterhin erhalten und werden nicht deprecated. Projekte können modulbasierte Komponenten weiterhin verwenden, ein Zwang zum Update besteht nicht.
Lokale Variablen mit @let
Mit dem neuen Schlüsselwort @let
können wir lokale Variablen direkt im Template definieren.
Diese Syntax funktioniert schon seit Angular 18.1, wird mit Angular 19 aber als stable markiert.
@let name = expression;
Die Variablen können im Template flexibel eingesetzt werden, aber:
Geschäftslogik sollte grundsätzlich in der TypeScript-Klasse untergebracht werden und nicht im Template (Trennung von Logik und Darstellung).
Die @let
-Syntax sollte deshalb sparsam eingesetzt werden.
@for (book of books(); track book.isbn) {
@let preisBrutto = book.price * 1.19;
<h2>{{ book.title }} – {{ preisBrutto | currency }}</h2>
}
Um Daten aus Observables aufzulösen, können wir im Template die AsyncPipe verwenden.
Mithilfe von @let
können wir das Ergebnis der AsyncPipe elegant in eine Variable schreiben und anschließend einsetzen:
@let book = book$ | async;
<h2>{{ book.title }}</h2>
<p>{{ book.description }}</p>
Der Conditional Control Flow (@if
) und die Direktive *ngIf
bieten seit jeher die Möglichkeit, das Ergebnis der Bedingung in eine lokale Variable zu schreiben:
@if (book$ | async; as book) {
<h2>{{ book.title }}</h2>
}
<ng-container *ngIf="book$ | async as book">
<h2>{{ book.title }}</h2>
</ng-container>
Falls die Daten aus dem Observable book$
zeitverzögert eintreffen oder als optional typisiert sind (Book | undefined
), ist die Kombination mit @if
der richtige Weg: Das Template soll nur angezeigt werden, wenn tatsächlich Daten vorliegen.
Häufig wird dieses Muster aber nur eingesetzt, um die Daten in eine Variable zu schreiben, ohne dass die if-Bedingung tatsächlich benötigt wird.
In diesem Fall sollte @let
verwendet werden.
@if
+as
: wenn Bedingung geprüft werden muss, bevor die Variable verwendet wird@let
: wenn nur eine Variable benötigt wird
Resource API
Mit Angular 19 wurde die experimentelle Resource API vorgestellt. Damit können wir intuitiv Daten laden und in Komponenten verarbeiten. Eine Resource repräsentiert einen Datensatz, der asynchron geladen wird, in der Regel per HTTP. Die Resource bietet eine Schnittstelle an, um die Daten zu verarbeiten, neuzuladen und sogar manuell zu überschreiben.
Eine Resource wird mit der Funktion resource
und einer Loader-Funktion initialisiert.
Dieser Loader ist dafür verantwortlich, die Daten asynchron zu laden. Die Funktion muss immer eine Promise zurückgeben: Wir können hier also entweder direkt die native Fetch API verwenden, oder wir wandeln das Observable aus dem HttpClient
von Angular mithilfe von firstValueFrom()
in eine Promise um:
import { resource } from '@angular/core';
// ...
booksResource = resource({
loader: () => fetch(this.apiUrl + '/books').then(res => res.json()) as Promise<Book[]>
});
// bs.getAll() gibt ein Observable<Book[]> zurück
booksResource = resource({
loader: () => firstValueFrom(this.bs.getAll())
});
Der Loader wird automatisch ausgeführt, sobald die Resource initialisiert wird.
Um mit den Daten zu arbeiten, stehen in der Resource drei Signals zur Verfügung: value
enthält die Daten, status
informiert über den Zustand der Resource, und error
liefert ein Fehler-Objekt:
{{ booksResource.value() | json }}
@for(book of booksResource.value(); track book.isbn) {
<p>{{ book.title }}</p>
}
Die Resource bietet uns die Möglichkeit, ohne großen Aufwand einen Ladeindikator anzuzeigen.
Dafür besitzt das Objekt sogar ein eigenes Signal isLoading()
:
@if (booksResource.isLoading()) {
<div>LOADING</div>
}
Eine Resource kann jederzeit neugeladen werden. Der Loader wird beim Aufruf der Methode reload()
erneut ausgeführt, und die geladenen Daten stehen anschließend in value
zur Verfügung:
@Component({ /* ... */ })
export class BookListComponent {
booksResource = resource({ /* ... */ });
reloadList() {
this.booksResource.reload();
}
}
Außerdem kann der Wert einer Resource jederzeit manuell überschrieben werden.
Dafür besitzt das Signal value
die bekannten Methoden set()
und update()
.
Mit einem Observable oder einem Signal, das durch toSignal()
aus einem Observable erstellt wurde, wäre das nicht so einfach möglich.
clearBookList() {
this.booksResource.value.set([]);
}
Die Loader-Funktion kann Parameter verarbeiten. Das ist sinnvoll, wenn der HTTP-Request weitere Informationen benötigt, z. B. die ID des zu ladenden Datensatzes.
Dafür können wir optional einen request
definieren: Dieses Signal dient als Trigger für die Loader-Funktion.
Immer wenn sich der Wert ändert, wird der Loader neu ausgeführt.
Der Wert des request
-Signals steht dann als Argument für den Loader zur Verfügung.
Immer wenn sich im folgenden Beispiel die ISBN ändert, wird der HTTP-Request für das dazugehörige Buch neu ausgeführt.
@Component({ /* ... */ })
export class BookDetailsComponent {
private bs = inject(BookStoreService);
isbn = input.required<string>();
bookResource = resource({
request: this.isbn,
loader: ({ request }) => this.bs.getSingle(request)
});
}
Für die Kompatibilität mit Observables aus der Bibliothek RxJS bietet Angular die sogenannte rxResource
an.
Sie funktioniert wie resource
, aber die Loader-Funktion gibt ein Observable zurück.
Auf diese Weise können wir Observables aus dem HttpClient
direkt verwenden, ohne Umweg über eine Promise:
import { rxResource } from '@angular/core/rxjs-interop';
// ...
booksResource = rxResource({
loader: () => this.bs.getAll()
});
Bitte beachten Sie, dass die Resource API experimentell ist und sich die Schnittstelle vor dem finalen Release noch ändern könnte.
📝 Wir erläutern die Resource API ausführlich in einem separaten Blogpost:
Neu in Angular 19: Daten laden mit der Resource API
Linked Signals
Das Linked Signal wurde mit Angular 19 als Developer Preview vorgestellt.
Es handelt sich um ein Signal, das seinen Wert automatisch auf Basis anderer Signals berechnet – ähnlich wie ein Computed Signal mit computed()
.
Der Unterschied: Der Wert eines Linked Signals kann jederzeit mit den Methoden set()
und update()
von außen überschrieben werden, so wie wir es von signal()
kennen.
Ein Linked Signal vereint also das Beste aus beiden Welten, wie der folgende Vergleich zeigt.
import { linkedSignal } from '@angular/core';
timestampMs = signal(Date.now());
// Wert des Signals kann überschrieben werden
timestampMs.set(Date.now());
timestampMs.update(ms => ms + 1000);
// computed(): Signal (nicht schreibbar)
const timestampSeconds = computed(() => timestampMs() / 1000);
timestampSeconds.set(0); // ❌ Compilation Error
// linkedSignal(): WritableSignal (schreibbar)
const timestampSecondsLinked = linkedSignal(() => timestampMs() / 1000);
timestampSecondsLinked.set(0); // ✅ funktioniert
Wir können alternativ eine ausführlichere Schreibweise wählen: In einem Optionsobjekt übergeben wir dazu source
und computation
.
Der aktuelle Wert des Signals in source
wird als Argument an die Computation Function übergeben.
Welche Schreibweise zu wählen ist, hängt vom Anwendungsfall und Geschmack ab, beide Implementierungen von timestampSecondsLinked
führen zum gleichen Ergebnis.
const timestampMs = signal(Date.now());
const timestampSecondsLinked = linkedSignal({
source: timestampMs,
computation: ms => ms / 1000
});
Ein Linked Signal ist besonders nützlich, wenn lokaler State mit dynamisch geladenen Daten synchronisiert werden soll. Das Signal berechnet seinen Wert aus einer Quelle, z. B. ein Component Input oder ein HTTP-Request, die Komponente kann das Signal aber weiterhin selbst mit Werten überschreiben.
📝 Wir stellen das Linked Signal ausführlich in einem separaten Blogpost vor. Dort finden Sie mehrere praktische Anwendungsfälle für
linkedSignal()
:
Neu in Angular 19: LinkedSignal für reaktive Zustandsverwaltung
Migrationen für Signal-based APIs
Angular hat in den vergangenen Versionen mehrere Komponenten-Schnittstellen auf funktionale Varianten umgestellt.
- Component Inputs mit der Funktion
input()
liefern die Daten als Signal. - Querys für ViewChildren und ContentChildren können mit den Funktionen
viewChild
/viewChildren
undcontentChild
/contentChildren
als Signals erfasst werden. - Component Outputs können mit der Funktion
output()
definiert werden. Das Ergebnis ist zwar kein Signal, aber die Schnittstelle steht in einer Reihe mit dem neueninput()
.
Bisher wurden dafür Dekoratoren verwendet (z. B. @Input()
), die nun nicht mehr notwendig sind.
Angular bietet Migrationsskripte an, um die Propertys in den Komponenten korrekt auf die neuen Schnittstellen zu migrieren:
# Updates `@Input` declarations to signal inputs
ng generate @angular/core:signal-input-migration
# Updates query declarations to signal queries
ng generate @angular/core:signal-queries-migration
# Updates @output declarations to the functional equivalent
ng generate @angular/core:output-migration
Für Inputs und Querys (ViewChildren und ContentChildren) gibt es eine kombinierte Migration:
# Combines all signals-related migrations into a single migration
ng generate @angular/core:signals
Signals schreiben in Effects
Ein Effect ist eine Funktion, die automatisch ausgeführt wird, wenn eins der darin verwendeten Signals seinen Wert ändert. Damit können wir Code ausführen, sobald veränderte Daten vorliegen.
counter = signal(0);
counter100 = signal(0);
constructor() {
effect(() => {
console.log('Aktueller Counter-Wert:', this.counter());
// funktionierte bisher nur mit `allowSignalWrites: true`
this.counter100.set(this.counter() * 100);
});
}
Dabei galt bisher die Empfehlung, in Effects keine Werte von Signals zu verändern.
Sollte das doch möglich sein, musste dafür die Option allowSignalWrites
gesetzt werden – dann konnte der Effects auch in Signals schreiben.
Mit Angular 19 entfällt diese Option. In Effects können wir nun ohne zusätzliche Konfiguration die Werte von Signals ändern. Diese Richtungsänderung hat das Angular-Team in einem eigenen Blogpost vorgestellt. Es gilt nun nicht mehr als schlechte Praxis, mit Effects weitere Signals zu setzen oder andere Seiteneffekte auszulösen.
Bitte verwenden Sie Effects grundsätzlich dennoch sparsam! Häufig ist ein Computed Signal oder Linked Signal das bessere Mittel:
counter = signal(0);
counter100 = computed(() => this.counter() * 100);
afterRenderEffect
: Effects für DOM-Interaktionen
Angular hat bereits vor einiger Zeit die neuen Lifecycle-Funktionen afterRender
und afterNextRender
vorgestellt.
Mit Angular 19 kommt nun das signalbasierte Pendant afterRenderEffect
hinzu.
Das Besondere an afterRenderEffect
: Die Daten zwischen den Render-Phasen werden als Signals ausgetauscht.
Die Phasen werden nur erneut ausgeführt, wenn sich gebundene Signals geändert haben.
DOM-Manipulationen werden so auf das nötige Minimum reduziert.
Alle drei Hilfsmittel sind dafür gedacht, sicher mit dem DOM einer Komponente zu interagieren. In der Regel ist das für normale Geschäftslogik nicht notwendig, weshalb die drei Funktionen eher für Spezialfälle gedacht sind.
📝 Wenn Sie mehr über das geänderte Verhalten von
effect()
und die neuen Effects vonafterRenderEffect()
erfahren möchten, empfehlen wir unseren ausführlichen Blogpost dazu:
Angular 19: Mastering effect and afterRenderEffect
Sonstiges
Wir empfehlen, regelmäßig einen Blick in den Changelog von Angular und der Angular CLI zu werfen. Neben den großen neuen Features gibt es auch einige kleinere interessante Neuerungen:
- Zoneless Application generieren: Mit der Funktion
provideExperimentalZonelessChangeDetection()
können wir den älteren Mechanismus für die Change Detection auf Basis von Zone.js deaktivieren. Die Change Detection funktioniert dann vollständig mit Signals. Ab Angular 19 können wir diesen Modus bereits bei der Erstellung eines Projekts wählen:ng new --experimental-zoneless
. (siehe Commit) - Default Export für Komponenten: Komponenten werden standardmäßig als Named Export generiert:
export class FooComponent {}
. In manchen Fällen kann es sinnvoll sein, stattdessen einen Default Export zu verwenden (export default class FooComponent {}
), z. B. für eine verkürzte Schreibweise beim Lazy Loading von Komponenten. Beim Anlegen einer Komponente mit der Angular CLI können wir nun auch einen Default Export generieren lassen:ng g c foo --export-default
. (siehe Commit) - typeof im Template: In Template Expressions wird jetzt auch das Schlüsselwort
typeof
unterstützt. Damit kann der Typ einer Variable direkt geprüft werden, ohne den Umweg über eine Methode der Komponente zu gehen:@if (typeof foo === 'string') {}
. (siehe Commit) - Ungenutzte Standalone Imports: Der Angular Language Service (in Visual Studio Code) erkennt ungenutzte Imports in Komponenten. Ein Hinweis erscheint, wenn eine Komponente/Pipe/Direktive importiert wird, aber nicht im Template genutzt wird.
Wir wünschen Ihnen viel Spaß mit Angular 19! Haben Sie Fragen zur neuen Version zu Angular oder zu unserem Buch? Schreiben Sie uns!
Viel Spaß wünschen Ferdinand, Danny und Johannes
Titelbild: Cala Paquita, Mallorca, Spanien. Foto von Tom Torgau
Suggestions? Feedback? Bugs? Please fork/edit this page on Github.