Blog-Headerbild
Angular Buch Team
 

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 und contentChild/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 neuen input().

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 von afterRenderEffect() 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

Zurück
Suggestions? Feedback? Bugs? Please fork/edit this page on Github.