Alles rund um Angular im Blog https://javascript-days.de/blog/angular/ JavaScript & Angular Days Mon, 23 Feb 2026 13:02:37 +0000 en-US hourly 1 https://wordpress.org/?v=6.7.2 Angular 21: Die Evolution zum AI-First-Framework https://javascript-days.de/blog/angular/angular-21-zoneless-change-detection-ai-integration/ Mon, 23 Feb 2026 12:28:50 +0000 https://javascript-days.de/?p=107958 Mit Angular 21 vollzieht das Framework einen strategischen Wandel zum AI-First-Framework. Die Einführung von Zoneless Change Detection als Standard, die Ablösung von Zone.js, der Einsatz von Vitest im Testing sowie experimentelle Signal Forms markieren eine neue Phase in der Angular-Architektur.

The post Angular 21: Die Evolution zum AI-First-Framework appeared first on JavaScript & Angular Days.

]]>
Mit Angular 21 verschiebt sich der Schwerpunkt des Frameworks sichtbar – architektonisch, organisatorisch und mit Blick auf den Einfluss KI-gestützter Entwicklung.

Karsten Sitterberg, einer der ausgewiesenen Angular-Experten im deutschsprachigen Raum, hat die Neuerungen detailliert analysiert. Sein Fachartikel liefert den technischen Deep Dive, inklusive Migrationspfaden, API-Details und praktischer Beispiele:

 

Zoneless als neuer Standard

Mit Angular 21 wird „Zoneless Change Detection” zum Default für neue Anwendungen. Damit verabschiedet sich Angular schrittweise von Zone.js – jener Bibliothek, die bislang im Hintergrund Browser-APIs patchte, um Change Detection automatisch auszulösen.

Der Schritt ist technisch konsequent. Explizite Steuerung ersetzt implizite „Magie”. Anwendungen werden transparenter, Debugging nachvollziehbarer, Performance kalkulierbarer. Gleichzeitig entsteht Reibung dort, wo Drittbibliotheken noch auf Zone.js setzen. Wer größere Ökosysteme betreibt, wird diese Übergangsphase aktiv gestalten müssen.

NEU! Generative AI für Angular-Entwickler:innen

Erlebe das neue Bootcamp vom 19. – 20. März 2026

Testing: Vitest statt Karma

Auch im Testing vollzieht Angular einen deutlichen Schnitt. Vitest wird für neue Projekte zum Standard. Das passt zur esbuild-basierten Toolchain und reduziert Ausführungszeiten spürbar.

Für bestehende Projekte bleibt die Migration optional und vorerst experimentell. Dennoch ist die Richtung klar: Angular modernisiert seine Infrastruktur beherzt, statt historische Entscheidungen dauerhaft mitzuschleppen. Die Details zu Syntax-Anpassungen und Migration beschreibt Sitterberg in seinem Beitrag.

 

Signal Forms: Architektur in Bewegung

Signals prägen Angular bereits seit einigen Versionen. Mit den experimentellen Signal Forms wird dieses Architekturprinzip nun auf ein traditionell komplexes Feld übertragen: Formulare.

Ziel ist eine klarere, typsichere und deklarative Modellierung, die perspektivisch die Trennung zwischen template-driven und reactive forms überwindet. Noch trägt die API den Status „experimentell”, doch die Stoßrichtung ist eindeutig. Wer Angular strategisch einsetzt, sollte diese Entwicklung aufmerksam verfolgen.

 

AI-Integration: Infrastruktur statt Schlagwort

Angular 21 versteht AI nicht als Marketing-Add-on, sondern als Werkzeug in der Entwicklungsumgebung.

Dazu gehört die Integration eines MCP-Servers in die CLI, der es ermöglicht, Angular-Befehle aus AI-gestützten Editoren heraus auszuführen. Hinzu kommen LLM-optimierte Dokumentationsdateien sowie ein Codegen-Scorer, der automatisch generierten Code bewertet.

Das sind keine visionären Ankündigungen, sondern konkrete Werkzeuge. Wer mit AI-gestütztem Coding arbeitet, erhält damit eine strukturierte Anbindung an bestehende Entwicklungsprozesse.

Router und weitere Anpassungen

Der Router kehrt zu einem konsistent asynchronen Verhalten auf Basis von async/await zurück. Das vereinfacht Server-Side-Rendering und verändert insbesondere das Verhalten in Tests.

Daneben bringt Angular 21 zahlreiche Detailverbesserungen: automatische Bereitstellung des HttpClient, präzisere Typisierung bei Lifecycle-Hooks, Fortschritte im Accessibility-Bereich sowie aktualisierte CLI-Migrationen. Gleichzeitig hebt das Framework die Mindestanforderung auf TypeScript 5.9 an und entfernt weitere Altlasten.

Angular entwickelt sich weiter – nicht spektakulär, sondern strukturell. Und vor allem passt es sich modernen Anforderungen in Sachen AI an. Die Entscheidung für Zoneless, die konsequente Signal-Architektur und die Integration von KI-Werkzeugen zeigen ein Framework, das seine Grundlagen überprüft und anpasst.

Sitterbergs vollständiger Artikel liefert die technische Substanz hinter diesen Entwicklungen. Er spiegelt damit direkt die technische Substanz der vielen Workshops auf den JavaScript Days wider. Wer darüber hinaus diskutieren, ausprobieren und vor allem vertiefen möchte, findet bei den JavaScript Days den passenden Rahmen. Dort treffen sich Expert:innen, die Framework-Entwicklungen nicht nur verfolgen, sondern einordnen – und Angular 21 ist nur eines der Themen, die dort mit fachlicher Tiefe und praktischer Konsequenz behandelt werden.

 

Links und Quellen

Sitterberg, Karsten: AI-ngular: Im Zeichen der AI mit Angular 21. Verfügbar unter: https://app.entwickler.de/Qarkq8q1rYb

 

🔍 Frequently Asked Questions (FAQ)

1. Was ist Angular 21?

Angular 21 ist die neueste Version des Angular-Frameworks und markiert den strategischen Wandel hin zu einem AI-First-Framework. Mit Zoneless Change Detection, moderner Toolchain und integrierter AI-Unterstützung in der CLI modernisiert Angular seine Architektur grundlegend.

2. Was bedeutet „Zoneless Change Detection“ in Angular 21?

Zoneless Change Detection ersetzt die bisherige Abhängigkeit von Zone.js. Statt automatischer, impliziter Aktualisierung setzt Angular 21 auf explizite Steuerung. Das macht Anwendungen transparenter, verbessert Debugging-Prozesse und sorgt für kalkulierbarere Performance – besonders in komplexen Enterprise-Projekten.

3. Was ändert sich beim Testing mit Angular 21?

Angular 21 setzt bei neuen Projekten auf Vitest statt Karma. Die esbuild-basierte Toolchain sorgt für schnellere Testläufe und eine modernere Infrastruktur. Bestehende Projekte können optional migrieren, profitieren aber langfristig von einer performanteren Testing-Umgebung.

4. Was sind Signal Forms in Angular 21?

Signal Forms sind ein experimentelles API-Konzept, das auf Angular Signals basiert. Ziel ist eine typsichere, deklarative und klar strukturierte Formulararchitektur. Langfristig könnten Signal Forms die Trennung zwischen template-driven und reactive forms auflösen.

5. Wie integriert Angular 21 künstliche Intelligenz (AI)?

Angular 21 integriert AI direkt in die Entwicklungsumgebung. Über einen MCP-Server in der Angular CLI können Befehle aus AI-gestützten Editoren ausgeführt werden. Zusätzlich gibt es LLM-optimierte Dokumentation und einen Codegen-Scorer zur Bewertung von KI-generiertem Code.

6. Wer sollte Angular 21 einsetzen?

Angular 21 richtet sich an Entwickler:innen und Unternehmen, die moderne Webanwendungen mit klarer Architektur, hoher Performance und AI-gestützten Workflows entwickeln möchten. Besonders für größere Projekte mit langfristiger Wartung und Skalierung bietet das Update strategische Vorteile.

7. Was bedeutet das Angular-21-Update für bestehende Projekte?

Bestehende Projekte können schrittweise migrieren. Während Zoneless Change Detection für neue Anwendungen Standard ist, bleibt die Nutzung von Zone.js zunächst möglich. Auch der Umstieg auf Vitest ist optional. Gleichzeitig erfordert Angular 21 mindestens TypeScript 5.9 und bringt mehrere Infrastruktur-Anpassungen mit sich.

The post Angular 21: Die Evolution zum AI-First-Framework appeared first on JavaScript & Angular Days.

]]>
Mit Angular Signals das Web-Frontend verändern https://javascript-days.de/blog/angular/signals-vs-rxjs-angular-updates/ Tue, 02 Dec 2025 13:31:07 +0000 https://javascript-days.de/?p=107868 Angular Signals bringen erstmals tief integrierte Reaktivität direkt ins Framework und verändern damit nachhaltig, wie moderne Web-Frontends entwickelt werden. Mit writable(), computed(), effect() und den neuen Resource-APIs entsteht ein performantes, leichtgewichtiges Reaktivitätsmodell, das Change Detection (Synchronization), Input-Bindings und State Management deutlich vereinfacht. Gleichzeitig bleibt Angular dank RxJS-Interop, Abwärtskompatibilität und Code-Generatoren gut migrierbar – ein zentraler Vorteil für Enterprise-Projekte.

The post Mit Angular Signals das Web-Frontend verändern appeared first on JavaScript & Angular Days.

]]>
Die Angular APIs bringen die lang ersehnte Reaktivität inklusive Input-Bindings direkt ins Framework. Dadurch verändern sich viele Bereiche der Enterprise Web Frontend Plattform. Die Synchronisation zwischen JavaScript Zustand und gerendertem Browser-Inhalt wird performanter und das Framework im gesamten einfacher zu erlernen und auch leichtgewichtiger in der Verarbeitung. Dieser neue Stil verlangt allerdings weitgehendst manuelle Refactorings, die in großen Codebases auch entsprechend eingeplant werden müssen. Das Angular Team stellt allerdings auch Abwärtskompatibilität bereit und liefert unterstützende Code-Generatoren.

 

Die wichtigsten Angular Signal APIs sind nun für den produktiven Einsatz freigegeben (stable) und daher sollte jedes Entwicklerteam spätestens ab jetzt neue Features mit Signals ausstatten. Das Angular-Team hat in einem Evaluierungsprozess mehrere Konzepte bewertet – auch RxJS und JavaScript Proxies wurden für deren Eignung als neues „Reactive Primitive“ des Frameworks analysiert – und das Signals Konzept hat die größte Übereinstimmung mit deren Zielsetzungen gezeigt. Was können Signals also so besonders gut, sodass sie sich sogar gegen RxJS durchsetzen konnten, das seit Angular Version 2 als Drittanbieter Bibliothek verpflichtend in jedem Angular Projekt mitinstalliert wird? Signals sind mit Sicherheit nicht das Universalwerkzeug wie RxJS samt all seiner Operatoren, aber sie glänzen in Teilbereichen, die speziell für den Synchronisationsprozess zwischen Browser-DOM und JavaScript-Zustand verantwortlich sind. Zudem ist das (reaktive) Abrufen von Werten im Vergleich zu RxJS deutlich komfortabler und vor allem wird der Wert immer synchron – also sofort – zurückgegeben. Kein subscribe/unsubscribe, kein async/await – einfach ein Funktionsaufruf und der im Signal verwaltete Wert steht schon typkonform bereit. Ein wichtiges Kriterium ist auch, dass man das Framework einfacher machen will. Einfacher nicht im Sinne von weniger bereitgestellten Features, aber hinsichtlich der Erlernbarkeit für neue Developer, die vor der Entscheidung stehen, auf welches Frontend-Framework sie sich in Ihrer künftigen Karriere fokussieren wollen. Dies ist für jeden und jede von uns wichtig – denn nur wenn ausreichend ausgebildete Angular Entwickler am Markt verfügbar sind, werden sich Unternehmen weiterhin für diese Plattform entscheiden. Bliebe Angular für alle Ewigkeit bei den bestehenden APIs, während andere Frameworks mutig neue Konzepte implementieren und etablieren, würde sich jeder Entscheidungsträger irgendwann die Frage stellen, ob Angular noch immer die beste Wahl für die eigenen Projekte ist. Das Angular-Team geht eben diesen mutigen Weg, allerdings auch einen, der es ermöglicht, all diese Innovation möglichst sanft in die eigene Codebase einbauen zu können und uns damit vor vergleichsweise sehr wenige Breaking Changes stellt. Für jeden Breaking Change wird – wenn technisch möglich – ein Angular CLI Schematics Generator angeboten, der die notwendigen Refactorings entweder vollautomatisch umsetzen kann oder bei der Migration zumindest Unterstützung anbietet. Im Fall von Signals gibt es mehrere unterstützende Generatoren, die beim manuellen Refactoring helfen [1], [2], [3]. Ein alternatives Beispiel für Update Schematics, die fast zu 100 Prozent automatisch umbauen können, sind jene für den neuen Control Flow (@if, @for, @switch) [4].

 

„Zahlreiche Vertreter der Angular Community wünschen sich schon sehr lange eine tiefere reaktive Framework-Unterstützung.“

 

Warum Signals?

Die neue reaktive Reise hat mit Angular 16 begonnen und ist nun, mit Version 21, schon sehr weit fortgeschritten. Signals sind allerdings kein Konzept das vom Angular Team erfunden wurde, sondern sie wurden vom „CEO of Signals“, dem Entwickler von SolidJS, Ryan Carniato, 2016 in die moderne Single-Page-Application-Welt gebracht [5]. Die früheren Wurzeln gehen zu Xerox und Libraries wie Knockout zurück. Heute verwenden zahlreiche Frontend Bibliotheken und Frameworks, wie z. B. Vue, Preact und Qwik Signal Konzepte. Mittlerweile ist auch ein offizielles TC39 Proposal [6] veröffentlicht worden, mit dem das Ziel verfolgt wird, Signals zu einem EcmaScript Sprachstandard zu machen. Das Angular Team war hier beteiligt und hat auch die Referenzimplementierung bereitgestellt [7]. 

Zahlreiche Vertreter der Angular Community wünschen sich schon sehr lange eine tiefere reaktive Framework-Unterstützung. Ein Teil dieser Initiative war die Etablierung des NgRx-Namespaces, der genau dieses Ziel verfolgt und anfangs vor allem für seine Event-basierte, reaktive State Management Implementierung nach dem Redux-Pattern bekannt wurde. NgRx und auch andere Drittanbieter Bibliotheken stoßen allerdings spätestens dann an Ihre Grenzen, wenn es um tiefere Framework-Integration wie Change Detection (wird mittlerweile „Synchronization“ genannt) oder Data Bindings geht. Letzteres erfordert auch Compilerintegration, damit die Rendering Engine Ivy korrekt angebunden werden kann. Aus diesen Gründen war es notwendig, dass das Angular Team selbst die relevanten reaktiven APIs direkt in den Framework Code einbindet.

Aus dem internen Evaluierungsprozess ist das Signals Konzept als Sieger hervor gegangen, weil es die gesetzten Ziele am besten umsetzen konnte:

  • Werte werden per Funktionsaufruf sofort – also synchron – zurückgegeben
  • Signals können in bestimmen Fällen abhängigen Quellcode benachrichtigen und als Konsequenz kann dieser erneut ausgeführt werden
    • um abgeleiteten Zustand zu ermitteln
    • Side-Effects auszulösen
    • das UI zu aktualisieren
  • Werteupdates, die zum selben Resultat führen, lösen keine Benachrichtigung aus
    • bei Objekten wird die Referenz verglichen
    • Immutable Updates sind hier also wichtig: wird eine Property eines Objekts verändert, muss auch eine neue Objektreferenz erzeugt werden

 

Als die ersten Signal APIs mit Version 16 veröffentlicht wurden, waren auch noch mutable Updates für schreibbare Signals erlaubt. Dies wurde mit Version 17 – sinnvollerweise – korrigiert. Einen Wert zunächst durch Objektmutation zu verändern hat einen Preis, den man später durch einen aufwendigen Vergleich, potenziell auf mehreren verschachtelten Objektebenen, hinsichtlich beeinträchtigter Runtime-Perfomance bezahlen muss. Zudem hat die Version 17 grundlegende APIs wie signal() und computed() sowie andere relevante Signal APIs und Typen aus dem Core Paket als stabil definiert. Andere wichtige Features wie effect() und toSignal(), toObservable() aus dem RxJS-Interop API wurden jedoch im Developer Preview Status belassen, da die interne Einplanung der Effects mehrmals angepasst werden musste. Auch diese APIs wurden nun mit Version 20 in den stabilen Zustand befördert. Dazwischen wurden mit Feature-Releases der Version 17 – endlich – Signal-basierte, reaktive Bindings Realität: input() für readonly Property-Bindings und model() für schreibbares Two-Way-Binding. Ergänzend wurde mit output() auch noch ein dekoratorloses Event-Binding API veröffentlicht, das aber keine Signals für die eventbasierte Kommunikation verwendet, sondern weiterhin einen EventEmitter. Alle diese APIs wurden mit Version 19 stabil. Dieses Major Release brachte uns auch noch das linkedSignal() und die Familie der Resource APIs, um Signals als Trigger für asynchrone Prozesse zu verwenden, die nicht zwingend mit RxJS implementiert sein müssen, aber erneut damit kompatibel sind. Das linkedSignal() wurde bereits mit Version 20 stabil, während die Resource APIs weiterhin experimentell sind. All diese Features werden im Rahmen dieses Artikels näher betrachtet.

 

„Das Signal kann einfach per Funktionsaufruf gelesen werden – ohne AsyncPipe.“

Erlebe Angular Trends live

16. - 20. März 2026

>

 

Grundlegende APIs

Starten wir mit jenem Signal API, mit dem man üblicherweise zuerst in Kontakt kommt – das Writable Signal, das mit der Funktion signal() erstellt wird. Diese und viele andere Features können direkt aus dem @angular/core Paket importiert werden. Das Angular Team hat sich – im Gegensatz zu RxJS – diesmal dafür entschieden, keine Drittanbieter Library zu verwenden, sondern den Code selbst zu implementieren, damit die rückwärtskompatible Unterstützung mit der „alten Welt“, vor der Signals-Ära, bestmöglich gewährleistet werden kann. signal() erzeugt ein Objekt, das aber gleichzeitig auch eine Funktion ist (ja, auch das kann JavaScript). Der Funktionsaufruf retourniert den aktuell verwalteten Wert sofort. Über die Methoden set() und update() kann der Wert verändert werden, was wie erwähnt, immer immutable erfolgen muss. set() überschreibt den vorhanden Wert mit einem neu Übergebenen, während update() eine Lambda-Expression entgegennimmt, die auf den aktuellen Wert zugreifen kann und deren Return-Value als neuer Signal-Wert übernommen wird. signal() wird initial ein Wert übergeben, der per Type-Inference festlegt, welchen Typ das Signal künftig verwaltet. Wird kein Wert übergeben und stattdessen der Typ über die Spitzen Klammern angeführt, ist auch undefined Teil der erlaubten Typsignatur:

 

const firstname = signal<string>(); // WritableSignal<string | undefined>

 

Der Wert des Signals kann einfach per Funktionsaufruf, sowohl im TypeScript-Code als auch im Template – und zwar ganz ohne AsyncPipe – gelesen werden:

 

<p>Firstname: {{ firstname() }}</p>

 

Mit computed() wird abgeleiteter Zustand als Signal (readonly) definiert. Dieser Signaltyp bietet keine set() oder update() Methode an, kann aber stattdessen Signals lesen und wird dann zuverlässig benachrichtigt, wenn sich die abhängigen Signals verändern. computed() wird eine Lambda-Expression übergeben, die einen Return-Value zurückgibt, der den abgeleiteten Zustand ermittelt. Allerdings sind Computed Signals lazy – werden Sie von niemandem in der Codebase gelesen, läuft die Lambda-Expression auch niemals. Auch ein indirekter Leseprozess über ein oder mehrere weitere abgeleitete Signals ist aber ausreichend, solange am Ende der Kette ein Binding im Template erfolgt oder ein effect() direkt oder indirekt (z. B. toObservable(), resource(), rxMethod() [8]) zur Anwendung kommt.

Side-Effects können über effect() definiert werden. Dies erfolgt, wie bei computed, über eine übergebene Lambda-Expression, aber mit dem Unterschied, dass hier kein Wert zurückgegeben wird. Es werden aber auch hier Signals gelesen und der Effect wird für eine neue – immer asynchrone – Ausführung eingeplant, wenn zumindest eines der gelesenen Signals eine Wertänderung aussendet.

 

„Warum verwendet man nicht immer model()?“

 

Kommunikation zwischen Components

In modernen Angular Frontends werden üblicherweise zwei Komponentenkategorien verwendet. Smart Components sind Container, die in der Routing Konfiguration verwendet werden und Feature-spezifische Services wie z. B. Data Access oder State Management Services injecten. Die zweite Kategorie sind UI (oder Presentational bzw. Dump) Components, die sich um das Detail-Rendering kümmern, oft wiederverwendbar sind und ihren Zustand lediglich über Data Bindings erhalten. Eben dieses Data Binding ist nun auch mit Signals realisierbar und ermöglicht das lang ersehnte Feature der reaktiven Inputs. Diese UI Components werden von einer übergeordneten Smart Component in deren Template verwendet und dadurch am Bildschrim angeordnet. Das Schöne an der Signal Integration für Bindings ist, dass das Angular Team auch hier wieder die Kompatibilität mit bisherigen Implementierungen klar im Fokus hat. Dadurch ist es möglich, dass eine Smart Component refactort wird, ohne die UI Component verändern zu müssen oder umgekehrt. Die gebundenen Daten kommen zwar als Signal in der Kind-Komponente an, müssen aber nicht zwingend im Template der Parent Component als Signal gebunden werden. Die Smart Component kann einen ganz normalen Wert binden und in der UI Component wird der Wert über ein Input Signal bereitgestellt. Dies kann mit der Funktion input() erreicht werden, die ein nur lesbares Signal erzeugt. Änderungen können in diesem Fall also nur per Binding durchgeführt werden und nicht innerhalb der Komponente, die den Input definiert, selbst. Dieser Einschränkung muss beim Refactoring beachtet werden und macht gegebenenfalls eine Änderung der Implementierung notwendig. 

 

readonly firstname = input.required<string>();

 

Der Beispielcode zeigt einen Signal-basierten Input vom Typ string, der in diesem Fall als required definiert ist und daher von der Parent Component zwingend gesetzt werden muss. Erfolgt das nicht, zeigt die IDE eine rote Fehler-Unterwellung im Template und auch der Compiler wirft den Fehler aus. Inputs können mittlerweile auch durch den Router gesetzt werden, sofern dieser entsprechend konfiguriert ist:

 

provideRouter([{ path: 'edit/:id', component: EditComponent }], withComponentInputBinding())

 

Damit versucht der Router nun, seinen Zustand an gleichnamige Inputs der referenzierten Komponente zu binden. Dies betrifft dynamische Pfad-, Query- und Matrix-Parameter genauso wie Zustand aus dem statischen data-Objekt der Route oder dynamischen Daten, die per Router Resolver abgeholt werden [9].

Im nachfolgenden Beispiel wird der Input mit einem Initialwert definiert, der im Laufe des Lebenszyklus der Component aktualisiert werden kann. Im gegebenen Fall wird er Zustand des dynamischen Routen Parameter id an den gleichnamigen Component Input gebunden. Da der Parameter in diesem Fall aus der URL ermittelt wird, liegt er als String vor und kann über eine simple Transform-Funktion, die das Framework bereitstellt, in einen Number-Wert umgewandelt werden:

 

readonly id = input(0, { transform: numberAttribute });

 

Ein required Input darf keinen Initialwert definieren, da vorausgesetzt wird, dass die Parent Component diesen setzen muss. Trotzdem ist es wichtig zu beachten, dass der initial gebundene Wert zum Zeitpunkt der Komponenteninstanzierung – also bei Property-Zuweisung oder im Konstruktor – noch nicht bereitsteht. Will man diesen lesen, benötigt man, wie bisher, eine ngOnInit-Implementierung. Alternativ kann eine effect()-Definition im Konstruktor verwendet werden, der den Input liest. Für den Effect wird garantiert, dass dessen erstmalige Ausführung erst dann erfolgt, wenn der Input gesetzt wurde.

 

constructor() { effect(() => console.log(this.firstname()); }

 

Ein weiteres, neues Konzept wird bereitgestellt, um Bindungs zu definierten – erstmals kann ein Two-Binding mit einem einzigen Funktionsaufruf definiert werden. model() liefert ein schreibbares Signal inkl. set() und update() Methoden. Obwohl weiterhin intern ein EventEmitter verwendet wird, um die Parent Component zu benachrichtigten – dieses Konzept bleibt gleich – wird keine emit()-Methode angeboten. Stattdessen wird intern automatisch emit() aufgerufen, sobald per set() oder update() ein neuer Wert gesetzt wird.

 

readonly selected = model(false);

 

Um auch für den bisherigen @Output() ein dekoratorloses API anzubieten, gibt es nun die Funktion output<string>(). Diese retourniert einen OutputEmitterRef inkl. emit()-Methode – wie bisher.

 

protected readonly firstnameChange = output<string>();

 

Die Frage ist nun – warum verwendet man nicht immer model() anstatt einer Kombination von input() und output()? Möchte man wirklich jede Komponenten-interne Änderung sofort an die Elternkomponente weitergeben, dann ist die Verwendung von model() ideal und einfacher als bisher. Wäre das Model Signal aber z. B. mit einem Input Control in UI Component verbunden, will man wahrscheinlich nicht jedes neu eingegebene Zeichen an die Parent Component senden. Diese könnte gegebenenfalls bei jedem Event-Trigger einen http-Call senden. Stattdessen macht es mehr Sinn, die Elternkomponente erst dann zu informieren, wenn der Suchknopf geklickt wird. In diesem Fall kann ein manueller emit()-Aufruf über eine output()-Property eine einfach zu realisierende Performance-Optimierung sein. 

 

Signals und RxJS

Signals bieten zwar, wie RxJS, ein reaktives Konzept an, aber trotzdem gibt es einige relevante Unterschiede. Signals können RxJS auch nicht in allen Bereichen ersetzen – trotzdem strebt das Angular Team an, RxJS optional zu machen. Da sich das widersprüchlich anhört, sollten wir das genauer betrachten.

Signal stellen Zustand dar, der sich über die Zeit verändern kann. Der Zustand ist von Beginn an typkonform vorhanden. Ein RxJS Observable bildet einen Strom von Events ab. Hierbei ist nicht garantiert, dass der Stream ein initiales, synchrones Event aussendet oder überhaupt jemals ein einziges Event. Das BehaviorSubject bietet verpflichtend ein initiales, synchrones Event an und verhält sich dies bezüglich einem Signal sehr ähnlich. Auch ein startWith()-Operator kann für ein initiales, synchrones Event sorgen. In beiden Fällen können nachfolgende Operatoren, wie z. B. debounceTime() die synchrone Antwort allerdings wieder zunichtemachen. Zusammengefasst, kann RxJS sehr viel, aber es erfordert die „richtige“ Implementierung, wenn es sich wie ein Signal verhalten soll. Signals haben hingegen ein sehr striktes, klar ausgewähltes Verhalten, von dem man kaum abweichen kann. Das macht sie zwar nicht zum Generalisten, dafür glänzen sie in jener Nische, für die sie vorgesehen sind: am Übergang von unserem JavaScript-Zustand zum Rendering als Browser-DOM und für die generelle Verwaltung von Zustand auch abseits von Komponenten. Sie sind allerdings klar nicht für Event-basierte Kommunikation geeignet – mehr dazu später.

Um RxJS optional machen zu können, sind noch einige Änderungen an öffentlichen APIs notwendig, die aktuell noch auf Observables & Co basieren. Dennoch ist es bereits ersichtlich, wie dieses Ziel erreicht werden soll. Der Kern des Frameworks wird in Zukunft sowohl intern als auch hinsichtlich der öffentlichen APIs keine Abhängigkeit auf RxJS mehr haben. Trotzdem werden auch künftige Angular Versionen Kompatibilität mit RxJS gewährleisten, allerdings könnte ein ng new irgendwann die Frage stellen, ob RxJS verwendet werden soll. Vielleicht zu Beginn mit Standard-Antwort „Yes“ und später „No“. Jene Projekte, die weiterhin RxJS verwenden wollen, können dann von weiterhin gut unterstützten Secondary-Entrypoints bestimmter Framework-Pakete importieren und nur dann wird es notwendig sein, RxJS zu installieren. Im Core-Paket gibt es bereits den Secondary-Entrypoint @angular/core/rxjs-interop. Dieser bietet u. a. die Funktionen toObservable() und toSignal() an, die zwischen den beiden reaktiven APIs übersetzen können.

toObservable() wird eine Signal Referenz übergeben – das Signal wird diesmal also nicht per Funktionsaufruf gelesen. Stattdessen wird intern ein Effect verwendet, der bei Änderung des Signal-Werts ein neues Event im Observable-Stream aussendet. Da ein Effect standardmäßig an den Lebenszyklus der Component gebunden ist, erfolgt in diesem Fall ein automatischer Cleanup – der Effect wird beendet und nicht mehr für weitere Neuausführungen eingeplant. Manuelle Cleanup-Logik kann ebenso definiert werden, falls eine Registrierung unabhängig der Komponente-Lifetime verwaltet werden soll oder der Effect z. B. in einem Root-Level-Service definiert wurde. Hierbei gilt es zu beachten, dass sich alle Eigenheiten des Signals, die in weiterer Folge näher beschrieben werden, auch auf das Observable auswirken:

  • Mehrere synchron geänderte Werte werden zusammengefasst
  • Der Initialwert kommt aufgrund des Effects erst mit einer asynchronen Verzögerung im Observable-Stream an
  • Sendet das Signal keinen Update-Trigger aus, weil der Wert z. B. nicht korrekt immutable aktualisiert wurde, wird auch kein Observable-Event erzeugt

 

Der umgekehrte Anwendungsfall – also ein Observable in ein Signal umzuwandeln – kann mit toSignal() realisiert werden. Dadurch erfolgt eine sofortige interne Subscription auf den Stream und wird im Falle einer Component bis zum Destroy-Zeitpunkt aufrechterhalten. Die Entwickler:in muss also nicht manuell unsubscriben, sofern das Standardverhalten der automatischen Beendigung beim Zerstören der Komponente das Beabsichtigte ist.

Alle an das Observable gekettete Operatoren wirken sich auch auf das Setzen der Event-Werte im resultierenden Signal aus. Somit kann die volle Funktionsvielfalt von RxJS auch für Signals verwendet werden. Da der Signalwert, wie immer, sehr einfach per Funktionsaufruf gelesen werden kann, hat die Entwickler:in das Beste aus den beiden reaktiven Welten zur Hand: sehr komfortables Registrieren, um über Wertänderungen informiert zu werden (aus dem Signals API) und die Möglichkeit, maßgeschneidertes reaktives Verhalten zu definieren (über die Vielfalt der Operatoren von RxJS). Es ist klar zu empfehlen, in Komponenten verstärkt Signals zu verwenden. RxJS darf zwar weiterhin auch in Components Anwendung finden, ist aber zumeist sinnvoller in den Service-basierten Logik-Schichten hinter den Komponenten – also z. B. den Side-Effects von State Management Implementierungen – aufgehoben.

Ein Fallbeispiel mit Router Component Input Binding, Signal-based Input, RxJS Interop und Reactive Forms findet die Leser:in im Artikel-begleitenden GitHub-Repository. [10]

 

Listing 1

// passenger-edit.component.ts

private passengerService = inject(PassengerService);

id = input(0, { transform: numberAttribute });

private passenger = toSignal(

  toObservable(this.id).pipe(

    switchMap(id => this.passengerService.findById(id))

  ), { initialValue: initialPassenger }

);

 

Signals werden erwachsen

Das Angular Team hat von Beginn an klargestellt, dass es nicht das Ziel verfolgt, Utility-Funktionen, ähnlich der RxJS-Operatoren, für Signals zu veröffentlichen. Stattdessen ist die Zielsetzung, die grundlegenden reaktiven APIs als direkten und gut integrierten Bestandteil des Frameworks bereitzustellen und damit für die überwiegenden Anforderungen der Angular Community ein Tool bereitzustellen, das in den meisten Fällen zwar eine klare Meinung verfolgt, aber nicht bis ins letzte Detail (wie durch RxJS-Operatoren) angepasst werden kann. Als Konsequenz soll jedoch diese API-Familie einfacher zu erlernen sein als es RxJS bisher war. Will man solche auf Signals aufbauende Hilfsfunktionen nutzen, kann man diese entweder selbst entwickeln und über eine generische Library in den eigenen Repositories verwalten oder eine Community Bibliothek verwenden. Ein Beispiel dafür sind die ngxtension [11]. Wie immer gilt, sorgfältig zu entscheiden für welche Zwecke der Mehraufwand für die Verwaltung von Drittanbieterpaketen gerechtfertigt ist und wann man lieber – wenn das die jeweilige Lizenz erlaubt – sinnvolle, schlanke Implementierungen in die eigene Codebase übernehmen möchte. Ein Beispiel für eine hilfreiche, simple Utility-Funktion ist direkt mit dem vorhergehenden Kapitel verbunden: hat man ein Signal aus Ausgangspunkt, dessen Verhalten man mithilfe mehrerer RxJS Operatoren maßgeschneidert anpassen möchte, um dann am Ende das Resultat wieder als Signal im Template zu lesen, dann ist eine verschachtelte Verwendung von toSignal() und toObservable() sinnvoll. Dafür kann man alternativ auch eine generische Funktion schreiben [12].

 

Listing 2

// passenger-edit.component.ts

private passengerService = inject(PassengerService);

id = input(0, { transform: numberAttribute });

private passenger = signalOperators(this.id, pipe(

  switchMap(id => this.passengerService.findById(id))

), initialPassenger);

 

Das Angular Team trifft Design-Entscheidungen oft auch aus pragmatischen Gründen. Obwohl man, wie es als Nicht-Ziel sieht, für jeden denkbaren Anwendungsfall eine Signal-basierte Hilfsfunktion oder ein neues Signal API im Framework direkt anzubieten, weicht man von dieser Strategie gelegentlich ab, wenn damit ein klarer Mehrwert geschaffen werden kann. Ein Beispiel dafür ist das linkedSignal(). Zunächst wurde in einem Video-Interview [13] das Reset-Pattern vorgestellt und darauf verwiesen, dass man sich bei Bedarf vergleichbare Funktionalität auf Basis der bestehenden APIs selbst implementieren kann. Mit Version 19 wurde diese Funktionalität schlussendlich doch als Framework API veröffentlicht. Die Idee dahinter ist, dass man Signale miteinander in Verbindung setzen kann, ohne einen Effect verwenden zu müssen. Zusammengefasst, ist das linkedSignal() eine Kombination aus dem Writable- und dem Computed-Signal. Bei der Definition werden ein Source-Signal und eine Computation-Funktion angeführt. Gleichzeitig erlaubt das Linked Signal aber auch die Verwendung der Schreib-APIs set() und update(). Der hiermit gesetzt Zustand ist so lange gültig, bis das Source-Signal einen neuen Wert aussendet, damit die Computation-Funktion ausführt und so den aktuellen Signal-Wert überschreibt – deshalb der Name Reset-Pattern.

Wofür kann man so ein API in der Praxis verwenden?

Es hilft z. B. beim Refactoring von @Input() zu input(). Wie besprochen, ist der Signal-basierte Input readonly. Daher könnte man das Input Signal als Source für ein Linked Signal definieren und somit einen Komponenten-internen Zustandscontainer schaffen, dessen aktueller Wert nur per Bedarf per Output-EventEmitter zur Elternkomponente gesendet wird [14].

Ein weiter Anwendungsfall kann der Einsatz in einer Auswahlbox sein. Wird im ersten Select-Control ein Land ausgewählt, ändern sich die Stammdaten der Regionen im zweiten Feld. Bei Auswahl „Deutschland“ werden die Regionen „Bayern, Hessen, Hamburg, etc.“ verknüpft, bei „Österreich“ hingegen „Wien, Niederösterreich, Steiermark, etc.“. Bildet man die Auswahl mit linkedSignal() ab, hat der User die Möglichkeit eine konkrete Region auszuwählen, die als lokaler State im Signal-Container verwaltet wird. Ändert sich später das Land, werden die Regionen per neuem Trigger durch das verknüpfte Source-Signal aktualisiert und die Computation wird erneute angestoßen, wodurch initial z. B. die alphabethisch erstgereihte Region vorausgewählt wird. Ab nun kann durch User-Interaktion erneut eine passende Region ausgewählt werden, die ins Linked Signal geschrieben wird und so lange gültig bleibt, bis die Computation erneute angetriggert wird – eben das ist das Reset-Verhalten [15].

 

„Bei Wertänderung wird die asynchrone Loader-Funktion angetriggert und bisherige Abläufe werden abgebrochen.“

 

Asynchronität auch ohne RxJS

Noch mehr Aufsehen hat allerdings eine andere Angular 19 Neuerung ausgelöst – die Familie der Resource APIs. Damit ist es nun erstmals möglich ohne direkte Effect-Verwendung und ohne RxJS Interop APIs Signals mit asynchronen Abläufen in Verbindung zu setzen. Die hier eingesetzten Konzepte lösen ein Problem, das durch das aktiv angestrebte Signal-Verhalten geschaffen wurde: Signals, die immer einen synchron verfügbaren Zustand haben müssen, vertragen sich nicht so gut mit der Natur von asynchronen Operationen. Daher hat man nicht einfach wie bei RxJS Hilfsfunktionen geschaffen, die Signals in Promises transformieren können – diese beiden APIs haben zu wenige Gemeinsamkeiten – sondern eine komplexere Signatur entworfen, um die synchrone mit der asynchronen Welt zu verbinden. Am Ende entsteht nicht nur einzelnes Signal, sondern gleich ein Objekt, das mehrere Signals als Properties anbietet.

Alle Resource APIs sind aktuell experimentell und es ist erkennbar, dass das API zwar schon gut durchdacht, aber die Gesamtstory noch nicht fertiggestellt ist. Seit Angular 19 gibt es die Funktionen resource() und rxResource() [16]. Version 19.2 hat uns die httpResource() spendiert. Damit kann Asynchronität über Promises, über Observables oder mithilfe einer internen Abhängigkeit auf den HttpClient angetriggert werden.

resource() kommt daher ohne RxJS Dependency aus und kann direkt aus @angular/core importiert werden. Im Vergleich zu einer direkten Promise-Verwendung in einem Effect, werden hiermit einige Nachteile behoben: die Resource verhindert Race-Conditions, indem ein Signal, das die asynchrone Loader-Funktion triggert auch zum Abbruch eines ggf. noch laufenden vorhergehenden Aufrufs führt. Dies macht ein Abort-Signal als Eingehender-Parameter in die Lambda-Funktion möglich. Dabei gilt es zu beachten, dass das Abort-Signal ein (leider nicht sehr ergonomisches) Sprachkonstrukt von EcmaScript ist, um asynchrone Operationen abbrechen zu können – sie haben allerdings abseits der Namensgleichheit nichts mit dem Angular Signals API zu tun. Zusammengefasst, haben wir also ein Signal, das bei Wertänderung die Promise-basierte asynchrone Loader-Funktion antriggert und bisherige, noch andauernde Abläufe abbrechen kann (vgl. switchMap() Verhalten in RxJS). Auch ein Reload wird unterstützt, wenn man z. B. Daten vom Backend API erneut abrufen will, obwohl sich z. B. die ID als Signal-Trigger nicht verändert hat. In diesem Fall werden erneute Reload-Trigger so lange ignoriert bis der Neuladeablauf beendet ist (vgl. exhaustMap() Verhalten in RxJS). Diese beiden Verhaltensweisen passen vermutlich für die allermeisten Anwendungsfälle sehr gut. Will man allerdings ein abweichendes Verhalten definieren, das bei neuen Triggern angewendet wird, gibt es aktuell keine Möglichkeit dies zu konfigurieren und dabei wird es voraussichtlich auch bleiben. Hier bestätigt sich also der zuvor beschriebene Angular-Weg: klare Meinung, einfache Anwendbarkeit, aber nicht so viele Detailauswahlmöglichkeiten, als wir von RxJS gewohnt sind.

 

Listing 3

// passenger.service.ts

findByIdAsResource(id: Signal<number>): ResourceRef<Passenger | undefined> {

  return resource({

    params: id,

    loader: ({ params: id, abortSignal }) => fetch(

      [this.baseUrl, 'passenger', id].join('/'),

      { signal: abortSignal }

    ).then(res => res.json() as Promise<Passenger>)

  });

}

 

Braucht man also mehr Design-Freiheit, bietet es sich erneut an, RxJS miteinzubeziehen – dafür gibt es die rxResource() aus @angular/core/rxjs-interop. Hiermit kann der Loader anstelle eines Promises per Observable-Stream inkl. Folgeevents implementiert werden. Erneut triggert das Signal den Loader-Stream und verwendet hierfür das switchMap-Verhalten.

 

Listing 4

// passenger.service.ts

findAsResource(

  filter: Signal<{ firstname: string, lastname: string }>

): ResourceRef<Passenger[] | undefined> {

  return rxResource({

    params: filter,

    stream: ({ params: filter }) =>

      this.find(filter.firstname, filter.lastname)

  });

}

 

Last but not least, bietet die httpResource() aus @angular/common/http ein einfach zu verwendendes Interface an, um lesende http-Anfragen verwalten zu können. Wenngleich es derzeit noch keine Möglichkeit gibt, Daten über einen Backend-Call per PUT oder HOST verändern zu können (Projektname Mutation API), zeigt die httpResource() schon jetzt, wie eine Zukunft ohne RxJS, aber mit ergonomischen Signal APIs aussehen könnte. Im aktuellen experimentellen Status ist es zwar nicht zu empfehlen, die gesamte Codebase von der aktuellen HttpClient Verwendung auf die httpResource() umzustellen, aber eine künftige http-Implementierung könnte ohne interne RxJS-Abhängigkeit auskommen und die öffentliche Signatur der httpResource() müsste nicht zwingend verändert werden [17].

 

Listing 5

// passenger-edit.component.ts

id = input(0, { transform: numberAttribute });

protected passengerResource = httpResource<Passenger>(() => ({

  url: 'https://demo.angulararchitects.io/api/passenger',

  params: { id: this.id() }

}), { defaultValue: initialPassenger });

 

Bleib informiert

Brancheninsights im Newsletter erhalten:

[mc4wp-simple-turnstile]

Fazit

Die Signal Story ist mittlerweile schon sehr weit fortgeschritten. Das bedeutet für bestehende Codebases, das spätestens jetzt, wo die meisten APIs bereit stabil verfügbar sind, verstärkt auf Signals gesetzt werden sollte. Dies macht den eigenen, reaktiven Code leichtgewichtiger und einfacher zu lesen als dies zumeist bei RxJS der Fall war. Dennoch wird uns RxJS jedenfalls noch mittelfristig begleiten bis auch asynchrone Abarbeitungen, wie Backend-API-Kommunikation stabil per Resource samt Mutation API unterstützt werden. Bei Einsatz von maßgeschneiderter, reaktiver, service-basierter Logik, wie sie auch in Side-Effects von Enterpise State Management Patterns (Redux und Flux mit @ngrx/store oder @ngrx/signals) vorkommt, ist ebenfalls noch offen, ob es eine „bessere“ Lösung als RxJS geben wird, denn für event-basierte Datenströme sind Signals nicht geeignet. Es spricht also nichts dagegen, bestehenden RxJS-Code weiterzuverwenden bzw. auch auf Observables und Operatoren zurückzugreifen, wenn spezifisches Verhalten modelliert werden soll, das mit Signals nicht gut abbildbar ist.

 

Links & Literatur

[1] https://angular.dev/reference/migrations/signal-inputs

[2] https://angular.dev/reference/migrations/outputs

[3] https://angular.dev/reference/migrations/signal-queries

[4] https://angular.dev/reference/migrations/control-flow

[5] https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob

[6] https://github.com/tc39/proposal-signals

[7] https://github.com/proposal-signals/signal-polyfill/blob/main/src/signal.ts

[8] https://ngrx.io/guide/signals/rxjs-integration#rxmethod

[9] https://angular.dev/api/router/withComponentInputBinding

[10] https://github.com/mikezks/windev-signal-apis/blob/main/libs/domain/checkin/src/lib/feature-passenger/passenger-edit/passenger-edit.component.ts

[11] https://ngxtension.netlify.app

[12] https://github.com/mikezks/windev-signal-apis/blob/main/libs/shared/core/src/lib/util-signals/signal-operators.ts

[13] https://youtu.be/aKxcIQMWSNU?t=520

[14] https://github.com/mikezks/windev-signal-apis/blob/linked/libs/domain/booking/src/lib/ui-flight/flight-card/flight-card.component.ts

[15] https://github.com/mikezks/windev-signal-apis/blob/main/libs/domain/booking/src/lib/feature-flight/airport/airport.ts

[16] https://github.com/mikezks/windev-signal-apis/blob/main/libs/domain/checkin/src/lib/logic-passenger/data-access/passenger.service.ts

[17] https://github.com/mikezks/windev-signal-apis/blob/http-resource/libs/domain/checkin/src/lib/feature-passenger/passenger-edit/passenger-edit.component.ts

 

🔍 Frequently Asked Questions (FAQ)

1. Was sind Angular Signals?

Angular Signals sind neue reaktive Primitive im Angular-Framework, die einen synchronen, sofort verfügbaren Zustand bereitstellen. Sie ermöglichen eine direkte, performante Synchronisation zwischen JavaScript-Zustand und DOM – ohne async/await, without subscribe/unsubscribe und ohne AsyncPipe.

2. Was können Angular Signals besonders gut?

Signals glänzen überall dort, wo abgeleiteter Zustand und UI-Synchronisation gefragt sind. Sie liefern Werte synchron, vermeiden unnötige Re-Renders, fassen schnelle Update-Serien zusammen und helfen, die Framework-Komplexität zu reduzieren. Dadurch reagieren Komponenten effizienter und vorhersehbarer.

3. Wofür werden Angular Signals in der Praxis genutzt?

Signals kommen vor allem zum Einsatz bei:

  • State Management innerhalb von Komponenten
  • reaktiven Bindings wie input(), model() oder output()
  • abgeleiteten Werten mit computed()
  • Side-Effects über effect()
  • sowie asynchronen Operationen über Resource APIs (resource(), rxResource(), httpResource()).

4. Wer sollte Angular Signals einsetzen?

Jedes Angular-Team, das neue Features entwickelt oder bestehende Codebases modernisieren möchte, sollte Signals nutzen. Mit Angular 17–21 sind die APIs stabil geworden, und das Angular-Team empfiehlt ausdrücklich, Signals für neue Features standardmäßig einzusetzen.

5. Warum setzt Angular auf Signals und nicht weiter ausschließlich auf RxJS?

RxJS bleibt weiterhin wichtig, ist aber für viele grundlegende UI-Synchronisationsaufgaben überdimensioniert. Signals bieten einen einfacheren, deterministischen und eingebauten Weg, Zustand und DOM miteinander zu verbinden. Für komplexe Event-Streams oder spezielle Reaktivität bleibt RxJS weiterhin das Mittel der Wahl.

6. Wie verändert Signals das Entwickeln von Angular-Components?

Mit Signals können Inputs (input()), Two-Way-Bindings (model()) und Outputs (output()) ohne Decorators definiert werden. Der Zustand ist ohne AsyncPipe lesbar, und Change Detection wird schlanker. Dadurch werden Komponenten klarer, leichter zu lesen und simpler zu refactoren.

7. Was bedeutet die Einführung der Resource APIs für asynchrone Prozesse?

Die Resource APIs ermöglichen erstmals eine reaktive Asynchronität ohne RxJS. Sie verhindern Race Conditions, unterstützen Reload-Mechanismen und liefern mehrere Signals (z. B. value, loading, error) in einem konsistenten Objekt. Das macht datengetriebene Komponenten einfacher und robuster, auch wenn die API derzeit noch experimentell ist.

The post Mit Angular Signals das Web-Frontend verändern appeared first on JavaScript & Angular Days.

]]>
NgRx neu gedacht: Wie das Event API den Signal Store erweitert https://javascript-days.de/blog/ngrx-signal-store-event-api/ Tue, 12 Aug 2025 15:53:06 +0000 https://javascript-days.de/?p=107577 Das neue Event API für den NgRx Signal Store bringt bewährte Flux- und Redux-Prinzipien in die Welt von Angular State Management. Entwickler können flexibel zwischen leichtgewichtigen Signal Stores und komplexeren Redux-Workflows wechseln. Mit Event Groups, Reducer und Effects lassen sich Zustandsänderungen strukturiert umsetzen – ideal für skalierbare Anwendungen.

The post NgRx neu gedacht: Wie das Event API den Signal Store erweitert appeared first on JavaScript & Angular Days.

]]>
Das neue Event API bringt die Arbeitsweise, die man vom Redux-basierten NgRx Global Store kennt, in die Welt des Signal Stores. Da Redux impliziert, dass es nur einen zentralen Store gibt, nutzt das NgRx den allgemeineren Begriff Flux.

Eine der großen Stärken dieses neuen API ist es, dass es sich gezielt für ausgewählte Fälle nutzen lässt. Somit kann man leichtgewichtig starten und bei Bedarf einzelne Stores auf Flux umstellen.

In diesem Artikel zeige ich, wie sich dieses neue API nutzen lässt. Die verwendeten Codebeispiele finden sich unter [1] in den Branches 10b-first-reducer und 10d-redux.

Eventing zwischen Stores

Um zu veranschaulichen, dass sich das Event API gezielt einsetzen lässt, demonstriere ich zunächst die Etablierung einer losen Kopplung zwischen zwei Stores. Hier steht zunächst das Eventing an sich und weniger der Einsatz von Flux im Vordergrund. Letzterem wende ich mich weiter unten zu.

Das verwendete Beispiel besteht aus drei Stores: Der DessertStore verwaltet die geladene Liste mit Desserts und für die Bearbeitung eines ausgewählten Desserts kommt der DessertDetailStore zum Einsatz. Außerdem finden sich die Bewertungen der einzelnen Desserts in einem RatingStore (Abb. 1).

steyer_kolumne_1
Abb. 1: Signal Stores im Beispiel

Eine solche Aufteilung ist bei leichtgewichtigen Stores wie dem Signal Store nicht unüblich. Im Gegensatz zu Redux erfolgt hier die Aufteilung nicht anhand technischer Aspekte wie Reducer, Effects oder Selectors. Stattdessen wird ein kleiner Teil des Zustands gemeinsam mit darauf basierenden Berechnungen und Operationen gekapselt.

Der verwaltete Zustand entspricht häufig einer Entität aus Sicht eines (Teil-)Features. Diese Aufteilung hat im hier betrachteten Beispiel zur Folge, dass ein vom DessertDetailStore aktualisiertes Dessert ggf. auch im DessertStore aktualisiert werden muss, um die geladene Übersicht aktuell zu halten.

Um zu verhindern, dass beide Stores aneinandergekoppelt werden, bietet sich ein Event an, das die Aktualisierung eines Desserts anzeigt. Solche Events lassen sich mit der Funktion event definieren. Üblicher ist es jedoch, mehrere zusammengehörige Events im Rahmen einer EventGroup bereitzustellen (Listing 1).

Listing 1

import { type } from '@ngrx/signals';
import { eventGroup } from '@ngrx/signals/events'; 
import { Dessert } from './dessert';

export const dessertDetailStoreEvents = eventGroup({
  source: 'Dessert Detail Store',
  events: {
    dessertUpdated: type<{
      dessert: Dessert
    }>(),
  },
});

Alle Events einer Gruppe haben dieselbe Source. Diese gibt an, welche Teile der Anwendung sie auslösen. Somit lassen sich die Nachrichtenflüsse beim Debuggen besser nachvollziehen. Jedes Event definiert sich durch einen Namen (hier: dessertUpdate) und einem Typ (type) für seine Payload. Diese Payload enthält Daten, die das Event näher beschreiben.

Der DessertDetailStore löst das dessertUpdate Event nach dem Speichern eines Desserts aus. Dazu nutzt er den von der Eventing API bereitgestellten Dispatcher, den sich der Store injizieren lässt (Listing 2).

Listing 2

import { Dispatcher } from '@ngrx/signals/events';

[...]

export const DessertDetailStore = signalStore(
  [...]
  withProps(() => ({
    [...],
    _dispatcher: inject(Dispatcher)
  })),
  withMethods((store) => ({
    [...],
    save(id: number, dessert: Partial<Dessert>): void {
      
      [...]
      // Trigger event
      const event = dessertDetailStoreEvents.dessertUpdated({
        dessert: savedDessert
      });
      store._dispatcher.dispatch(event);

    },
  })),
);

Als Empfänger im DessertStore fungiert ein Reducer. Er nimmt den Payload des Events entgegen und liefert einen Updater zurück (Listing 3).

Listing 3

import { on, withReducer } from '@ngrx/signals/events';

[...]

export const DessertStore = signalStore(
  { providedIn: 'root' },
  withState({
    [...]
    desserts: [] as Dessert[],
  }),
  withReducer(
    on(dessertDetailStoreEvents.dessertUpdated, ({ payload }) => {
      const updated = payload.dessert;
      return (store) => ({
        desserts: store.desserts.map((d) =>
          d.id === updated.id ? updated : d,
        ),
      });
    }),
  ),
  [...]
)

Erlebe Angular Trends live

16. - 20. März 2026

>

Der Updater aktualisiert das aktualisierte Dessert in der Liste mit sämtlichen abgerufenen Desserts. Der zurückgelieferte Zustand ist partiell, d. h., er muss lediglich die geänderten Eigenschaften enthalten. Wie vom Signal Store gewohnt, lassen sich Updater auch in eigene Funktionen auslagern (Listing 4). Damit lässt sich der Reducer verkürzen (Listing 5).

Listing 4

export type DessertSlice = {
  desserts: Dessert[];
};

function updateDessert(updated: Dessert) {
  return (store: DessertSlice) => ({
    desserts: store.desserts.map((d) => 
      (d.id === updated.id ? updated : d)),
  });
}

Listing 5

withReducer(
  on(dessertDetailStoreEvents.dessertUpdated, ({ payload }) => {
    return updateDessert(updated);
  }),
),

Diese Möglichkeit kommt auch zum Einsatz, um Updater von Custom Features wie withEntity zu nutzen. Wird kein Updater benötigt, kann der Reducer auch direkt einen aktualisierten partiellen Zustand liefern. Ich greife diese Möglichkeit weiter unten auf.

 

Flux/Redux für einzelne Stores

Im nächsten Schritt möchte ich zeigen, wie sich das Event API nutzen lässt, um das Flux-Muster für einzelne Stores zu implementieren. Somit ergibt sich die vom Redux-basierten NgRx Global Store gewohnte Arbeitsweise:

  • Der Konsument löst ein Event aus (beim Global Store ist hier von Action die Rede).

  • Effects empfangen Events und stoßen asynchrone Seiteneffekte an. Das Ergebnis veröffentlichen sie in Form weiterer Events.

  • Reducer im Store nehmen Events entgegen und aktualisieren den verwalteten Zustand.

Auch hier gilt, dass man diese Vorgehensweise selektiv anwenden kann und nicht allen Stores aufzwingen muss. Besonders interessant ist dieses Muster, wenn mehrere Komponenten denselben Zustand konsumieren sowie verändern.

Die Implementierung startet wieder mit einer Event Group. Sie enthält ein Event loadDesserts, das einen Effect veranlasst, Desserts zu laden. Dieser Effect informiert daraufhin über den Ausgang dieses Unterfangens mit dem Event loadDessertsSuccess oder im Fehlerfall mit loadDessertsError (Listing 6).

Listing 6

import { eventGroup } from '@ngrx/signals/events'; 
import { type } from '@ngrx/signals';
import { Dessert } from './dessert';

export const dessertEvents = eventGroup({
  source: 'Dessert Feature',
  events: {
    loadDesserts: type<{
      originalName: string,
      englishName: string,
    }>(),
    loadDessertsSuccess: type<{
      desserts: Dessert[]
    }>(),
    loadDessertsError: type<{
      error: string
    }>(),
  },
});

Reducer

Die Reducer für das Laden von Desserts sind etwas simpler als im ersten Fall. Anstatt eines Updaters, der den aktuellen Zustand auf den neuen abbildet, liefern sie lediglich die neuen Werte zurück (Listing 7).

Listing 7

import { on, withReducer } from '@ngrx/signals/events';
[...]

export const DessertStore = signalStore(
  { providedIn: 'root' },
  withState({
    filter: {
      originalName: '',
      englishName: '',
    },
    loading: false,
    desserts: [] as Dessert[],
    error: '',
  }),
  withReducer(
    [...],
    on(dessertEvents.loadDesserts, ({ payload }) => {
      return {
        filter: payload,
        loading: true,
      };
    }),
    on(dessertEvents.loadDessertsSuccess, ({ payload }) => {
      return {
        desserts: payload.desserts,
        loading: false,
      };
    }),
    on(dessertEvents.loadDessertsError, ({ payload }) => {
      return {
        error: payload.error,
        loading: false,
      };
    }),
  ),
  [...]
);

Effects

Die Effects im Event API, die nichts mit dem gleichnamigen Konzept in der Signals-Welt zu tun haben, sind technisch gesehen RxJS-basierte Pipes, die eingehende Events auf ausgehende Events abbilden. Dazu binden sie in der Regel über einen Flattening-Operator wie switchMap oder exhaustMap eine asynchrone Aktion ein (Listing 8).

Listing 8

import { Events, withEffects } from '@ngrx/signals/events';
import { mapResponse } from '@ngrx/operators';

[...]

export const DessertStore = signalStore(
  [...],
  withProps(() => ({
    _dessertService: inject(DessertService),
    _toastService: inject(ToastService),
    _events: inject(Events),
  })),
  [...]
  withEffects((store) => ({
    loadDesserts$: store._events.on(dessertEvents.loadDesserts).pipe(
      switchMap((e) =>
        store._dessertService.find(e.payload).pipe(
          mapResponse({
            next: (desserts) => dessertEvents.loadDessertsSuccess({ desserts }),
            error: (error) =>
              dessertEvents.loadDessertsError({ error: String(error) }),
          }),
        ),
      ),
    ),
  })),
);

Auch wenn ein sprechender Name, z. B. loadDesserts$ im gezeigten Fall, wichtig für die Wartbarkeit ist, spielt er technisch gesehen keine Rolle, denn das Event API stößt den Effect nicht über seinen Namen, sondern über Events an. Der Store erzeugt Effects über den Service Event. Seine Methode on filtert die vom Event API empfangenen Events und liefert sie über ein Observable zurück. Übergibt der Store keinen Parameter an on, erhält er sämtliche Events. Übergibt der Store einen oder mehrere Event-Typen, liefert on nur die dazu passenden Events.

Um das Ergebnis der angestoßenen Seiteneffekte auf ein ausgehendes Event abzubilden, bietet sich der mit NgRx gelieferte Operator mapResponse an. Er ist eine Kombination aus map und catchError und erlaubt eine Transformation der Ergebnisse sowie von Fehlern.

Wie in den ersten Tagen des NgRx Global Store ist es essenziell, dass Fehler behandelt werden, z. B. mit catchError oder eben – wie gezeigt – mit mapResponse. Ansonsten schließt RxJS das Observable und weitere Events werden nicht mehr vom Effekt behandelt. Wie üblich gilt es, diese Fehler auch auf der Ebene zu behandeln, auf der sie aufgetreten sind. Im gezeigten Fall ist das innerhalb von switchMap, was zu einer weiteren Verschachtelung führt.

Eine Option wie resubscribeOnError beim Global Store, die Fehler automatisch kompensiert, existiert derzeit noch nicht. Das NgRx-Team denkt jedoch darüber nach.

Bleib informiert

Brancheninsights im Newsletter erhalten:

[mc4wp-simple-turnstile]

Store konsumieren

Um den Store zu konsumieren, lassen sich die einzelnen Komponenten in den Dispatcher injizieren und lösen damit das gewünschte Event aus:

this.#dispatcher.dispatch(
  dessertEvents.loadDesserts({
    originalName: this.originalName(),
    englishName: this.englishName(),
  }),
);

Bonus: Redux DevTools

Derzeit kommt der Signal Store mit keiner offiziellen Integration in die Redux DevTools [2], die beim Debuggen einen Blick in den Store erlauben und auch über den Verlauf der einzelnen Zustandsänderungen informieren. Allerdings lässt sich diese Möglichkeit auf der Basis des vom Signal Store gebotenen Erweiterungsmechanismus nachrüsten.

Eine Implementierung dieser Idee findet man im Communitypaket @angular-architects/ngrx-toolkit. Es bietet ein Feature withDevtools, das in den Store aufzunehmen ist (Listing 9).

Listing 9

import { withDevtools } from '@angular-architects/ngrx-toolkit';
[...]

export const DessertStore = signalStore(
  [...]
  withDevtools('DessertStore')
);

Der übergebene String kommt als Name für den Knoten zum Einsatz, der in den Dev Tools den jeweiligen Store repräsentiert. Wurden die DevTools als Browsererweiterung installiert, lassen sich die im Store hinterlegten Daten sowie deren Verlauf in der Developer-Konsole einsehen (Abb. 2).

steyer_kolumne_2.tif
Abb. 2: Redux DevTools für den Signal Store

Zusammenfassung

Das neue Event API für den NgRx Signal Store erlaubt es, leichtgewichtig zu starten und bei Bedarf gezielt Flux-Muster einzuführen. Mit zusätzlichen Tools wie den Redux DevTools lässt sich das Debugging vereinfachen. Damit liefert das Event API eine flexible Brücke zwischen einfacher Signals-Nutzung und erprobten State-Management-Praktiken aus der Redux-Welt.

 

🔍 Frequently Asked Questions (FAQ)

1. Was ist das neue Event API im NgRx Signal Store?
Das Event API erweitert den NgRx Signal Store um Flux-/Redux-Prinzipien. Es ermöglicht, Events, Reducer und Effects einzusetzen, ohne das gesamte Projekt auf komplexes State Management umzustellen.

2. Was kann man mit dem Event API umsetzen?
Mit dem Event API lassen sich Events definieren, die Zustandsänderungen in einem Store auslösen. Dadurch können Entwickler lose gekoppelte Stores, asynchrone Workflows und strukturierte Datenflüsse in Angular-Anwendungen realisieren.

3. Was wird mit Reducern und Effects im Signal Store gemacht?
Reducer reagieren auf eingehende Events und aktualisieren den Zustand, während Effects asynchrone Aktionen wie API-Aufrufe oder Fehlerbehandlung übernehmen und neue Events auslösen.

4. Wer nutzt das NgRx Signal Store Event API?
Das Event API richtet sich an Angular-Entwickler, die die Einfachheit von Signals nutzen, aber gleichzeitig die bewährten Muster aus Redux und NgRx in einzelnen Stores anwenden möchten.

5. Was ist der Vorteil gegenüber reinem Redux?
Im Gegensatz zu einem globalen Redux Store kann das Event API selektiv eingesetzt werden. Entwickler entscheiden selbst, ob ein Store leichtgewichtig bleibt oder Flux-/Redux-Prinzipien nutzen soll.

6. Wie unterstützt das Event API beim Debugging?
Über die Integration mit Redux DevTools lassen sich Zustandsänderungen im Signal Store visualisieren. Dadurch können Entwickler Events und State-Verläufe im Detail nachvollziehen.

7. Was bedeutet das für Angular State Management insgesamt?
Das Event API schafft eine Brücke zwischen einfacher Signal-Nutzung und komplexen NgRx-Workflows. Damit können Angular-Projekte flexibel skalieren – vom schlanken Store bis zum vollwertigen Redux-Ansatz.

The post NgRx neu gedacht: Wie das Event API den Signal Store erweitert appeared first on JavaScript & Angular Days.

]]>
Streaming Resources in Angular 19.2 – Datenströme mit dem Resource API https://javascript-days.de/blog/angular/streaming-resources-in-angular-19-2-datenstrome-mit-dem-resource-api/ Mon, 03 Mar 2025 14:11:10 +0000 https://javascript-days.de/?p=107106 Mit den Streaming Resources dringen das Resource API sowie Signals in Bereiche von RxJS vor – sie erlauben es, kontinuierliche Datenströme darzustellen. Neben den Gemeinsamkeiten zu RxJS gibt es jedoch auch einige nicht offensichtliche, jedoch besonders wichtige semantische Unterschiede, die es zu berücksichtigen gilt.

The post Streaming Resources in Angular 19.2 – Datenströme mit dem Resource API appeared first on JavaScript & Angular Days.

]]>
Heute gehen wir anhand von zwei Beispielen auf die Details rund um Streaming Resources sowie auf die semantischen Feinheiten und das Zusammenspiel mit RxJS ein. Den Quellcode finden Sie unter [1].

Beispielanwendungen

Um die Nutzung von Streaming Resources zu demonstrieren, kommen hier zwei Beispiele zum Einsatz. Das erste ist quasi das „Hello World“ der reaktiven Programmierung: ein einfacher Timer, der im Sekundentakt hochzählt (Abb. 1).

steyer_kolumne_1
Abb. 1: Ein einfacher Timer auf Basis einer Streaming Resource

Das Schöne an diesem sehr einfachen Beispiel ist, dass es bereits vieles enthält, an dem die kleinen, jedoch wichtigen Feinheiten von Streaming Resources diskutiert werden können. Eine Variante dieses Beispiels kommt auch zum Einsatz, um die Interoperabilität mit RxJS zu zeigen.

NEU! Generative AI für Angular-Entwickler:innen

Erlebe das neue Bootcamp vom 19. – 20. März 2026

Das zweite Beispiel ist ein wenig praxisnäher. Es handelt sich dabei um einen einfachen WebSockets-basierten Chat auf der Basis einer Streaming Resource (Abb. 2).

steyer_kolumne_2
Abb. 2: Ein einfacher Chat auf Basis einer Streaming Resource

Dieses Chatbeispiel unterscheidet sich technisch gesehen in einem wichtigen Punkt vom Timer: Hier sind zum selben Zeitpunkt alle empfangenen Nachrichten und nicht nur die letzte von Bedeutung. Das stellt eine kleine Herausforderung in der Signal-basierten Welt dar, die sich allerdings lösen lässt.

Als Chatserver kommt ein Fork eines Beispielprojektes aus dem Mozilla Developer Network zum Einsatz [2]. Dieser Node-basierte Server liegt dem Beispielprojekt [1] bei und die readme.md zeigt, wie er sich starten lässt.

Anatomie einer Streaming Resource

Im Gegensatz zu herkömmlichen Resources liefert der Loader einer Streaming Resource immer die folgende Struktur zurück:

PromiseLike<Signal<
  { value: T; } 
  | { error: unknown;}>>;

Es handelt sich hierbei um ein Promise mit einem Signal, das ein Objekt vorhält. Das Objekt verweist entweder auf den zuletzt veröffentlichten Wert (value) oder auf einen aufgetretenen Fehler. Dadurch, dass das Signal im Verlauf der Zeit mehrere Werte bzw. Fehlerzustände annimmt, repräsentiert es einen Datenstrom.

Um in weiterer Folge den Programmcode zu vereinfachen, definiere ich für den Inhalt dieses Signals einen Typ namens StreamItem (Listing 1). Angular bietet derzeit dafür keinen expliziten Typ an. Das soll sich aber im Laufe der weiteren Entwicklung noch ändern.

Listing 1

export type StreamItem<T> =
  | {
      value: T;
    }
  | {
      error: unknown;
    };

Um eine Streaming Resource zu definieren, nutzt der Programmcode die Funktion resource, die auch bei herkömmlichen Resources zum Einsatz kommt. Anstatt eines Loaders bekommt diese Funktion jedoch einen Streaming Loader über die Eigenschaft stream übergeben (Listing 2). Da dieses Beispiel das Schlüsselwort async nutzt, ergibt sich das Promise, das das Signal bereitstellt, implizit.

Listing 2

const myResource = resource({
  request: requestSignal,
  stream: async (params) => {

    // 1. Create Signal representing the Stream
    const result = signal<StreamItem<number>>({ 
      value: 4711 
    });

    // 2. Set up async logic updating the Signal
    […]

    // 3. Set up clean-up handler triggered by AbortSignal
    params.abortSignal.addEventListener('abort', () => {
      […]
    });

    // 4. Return Signal
    return result;
  },
});

Typischerweise lässt sich ein Streaming Loader in vier Teile untergliedern, die im gezeigten Beispiel mit Kommentaren angedeutet sind:

  1. Zunächst erzeugt der Streaming Loader ein neues Signal, das den Datenstrom repräsentiert. Dieses Signal bekommt einen Initialwert.

  2. Danach startet der Streaming Loader eine asynchrone Operation, die im Laufe der Zeit mehrere Ergebnisse liefert. Diese Ergebnisse veröffentlicht er nach und nach über das Signal.

  3. Der Streaming Loader ist auch für das Bereitstellen einer Aufräumlogik verantwortlich. Diese beendet die zugrundeliegende asynchrone Operation, wenn ihre Werte nicht mehr benötigt werden.

  4. Am Ende liefert der Streaming Loader das Signal zurück.

Für das Bereitstellen der Aufräumlogik nutzt der Streaming Loader das vom Resource API bereitgestellte AbortSignal, das sich im übergebenen Parameterobjekt findet. Interessanterweise entsprechen diese Punkte mehr oder weniger auch der typischen Vorgehensweise bei der direkten Nutzung des Observable-Konstruktors in der RxJS-Welt.

switchMap-Semantik beim Übergang zwischen Streams

Die definierte Aufräumlogik läuft immer dann, wenn die Anwendung den aktuellen Datenstrom nicht mehr benötigt. Dafür gibt es zwei Gründe: Der erste tritt dann ein, wenn Angular den Building Block zerstört, der die Resource beherbergt. Stellen wir uns dazu eine Komponente mit einer Resource vor. Beim Verlassen zerstört der Router diese Komponente und im Rahmen dessen zerstört Angular auch die Resource.

Der zweite Grund ergibt sich, wenn sich das request-Signal der Resource ändert. Jede Änderung triggert den Streaming Loader und dieser liefert daraufhin einen neuen Stream. Beim Übergang zwischen diesen Streams nutzt das Resource API somit dieselbe Semantik wie switchMap in RxJS (Abb. 3).

steyer_kolumne_3
Abb. 3: switchMap-Semantik beim Übergang zu einem neuen Stream

Das bedeutet, dass die Resource immer nur den neuesten Stream konsumiert. Diese Vorgehensweise ist die gleiche, die in der Regel beim Laden von Daten zum Einsatz kommt. Wie generell bei Signals in Angular geht es also auch hier darum, für Standardfälle einfache Konzepte anzubieten. Für komplexere Szenarien kann die Anwendung auf Bibliotheken wie RxJS zurückgreifen.

Ein einfacher Resource-basierter Timer

Nachdem wir nun den prinzipiellen Aufbau einer Streaming Resource besprochen haben, möchte ich die konkrete Nutzung anhand eines ersten Beispiels demonstrieren: Ein Timer, der eine Zahl in einem bestimmten Intervall hochzählt. Listing 3 zeigt diese timerResource aus Sicht eines Konsumenten.

Listing 3

@Component([…])
export class TimerResourceComponent {
  ResourceStatus = ResourceStatus;

  startValue = signal(0);
  timer = timerResource(1000, this.startValue);

  forward(): void {
    this.startValue.update((v) => nextSegment(v));
  }
}

function nextSegment(currentValue: number): number {
  return Math.floor(currentValue / 100) * 100 + 100;
}

Der Timer liefert einen Stream, der beim übergebenen Startwert zu zählen beginnt. Der erste Parameter von timerResource repräsentiert das gewünschte Intervall in Millisekunden. Den Startwert repräsentiert das Signal startValue. Immer wenn er sich ändert, wechselt der Timer zu einem neuen Stream. Um diesen Umstand zu demonstrieren, springt die Methode forward zum nächsten vollen Hunderter, also z. B. von 17 auf 100 oder von 123 auf 200.

Erlebe Angular Trends live

16. - 20. März 2026

>

Factory für die Streaming Resource

Um den Einsatz der Streaming Resource für den Timer zu vereinfachen, handelt es sich bei der Funktion timerResource lediglich um eine Factory (Listing 4).

Listing 4

export function timerResource(
  timeout: number,
  startValue: () => number
): ResourceRef<number | undefined> {

  const request = computed(() => ({
    startValue: startValue(),
  }));

  const result = resource({
    request: request,
    stream: async (params) => {
      const counter = params.request.startValue;
      […]
    }
  });

  return result;
}

Diese Factory nimmt das gewünschte Intervall (timeout) sowie ein Signal mit dem Standardwert entgegen. Bei diesem Signal ist timerResource nur am Getter interessiert. Deswegen typisiert sie das Signal mit () => number. Der Rückgabewert ist eine ResourceRef<number | undefined>. Der Typparameter entspricht den Werten im Stream. Da die Resource den Stream asynchron erzeugen kann, setzt sie initial diesen Wert auf undefined. Um undefined zu vermeiden, wird Angular künftig auch die Möglichkeit bieten, einen eigenen Initialwert festzulegen.

Das Computed Signal request repräsentiert alle Parameter, die den Loader triggern. Da im besprochenen Beispiel lediglich startValue als Trigger zum Einsatz kommt, fühlt sich das Computed Signal ein wenig wie ein Durchlauferhitzer an. Trotzdem halte ich gerne an dieser Vorgehensweise fest, zumal sie es erlaubt, später den Trigger zu erweitern und den Namen startValue auch innerhalb des (Streaming) Loaders verfügbar macht: Dieser kann im diskutierten Fall über param.request.startValue den aktuellen Wert beziehen.

Streaming Loder für den Timer

Der Aufbau des Streaming Loaders entspricht den oben diskutierten vier Abschnitten (Listing 5). Die asynchrone Operation zum Hochzählen implementiert der Loader mit der althergebrachten setInterval-Funktion von JavaScript. Um das Verhalten von Fehlerzuständen zu demonstrieren, meldet der Timer bei den Werten 7 und 13 einen Fehler – abergläubige Benutzer:innen kommen somit auch auf ihre Kosten.

Listing 5

const result = resource({
  request: request,
  stream: async (params) => {
    let counter = params.request.startValue;

    // 1. Create Signal representing the Stream
    const resultSignal = signal<StreamItem<number>>({
      value: params.request.startValue,
    });

    // 2. Set up async logic updating the Signal
    const ref = setInterval(() => {
      counter++;
      console.log('tick', counter);

      if (counter === 7 || counter === 13) {
        resultSignal.set({ error: 'bad luck!' });
      } else {
        resultSignal.set({ value: counter });
      }
    }, timeout);

    // 3. Set up clean-up handler triggered by AbortSignal
    params.abortSignal.addEventListener('abort', () => {
      console.log('clean up!');
      clearInterval(ref);
    });

    // 4. Return Signal
    return resultSignal;
  },
});

Durch das asynchrone Verhalten von setInterval findet Schritt 4 vor der ersten Ausführung des Callbacks in Schritt 2 statt. Das bedeutet, dass der Loader zunächst das Signal mit seinem Initialwert zurückliefert und erst dann das Signal nach und nach neue Werte erhält.

Die Streaming Resource ausprobieren

Beim Ausprobieren des Beispiels sieht man zunächst, wie das Beispiel den Zähler im Sekundentakt inkrementiert. Anstatt der Werte 7 und 13 meldet es einen Fehler. Im Gegensatz zu RxJS beendet solch ein Fehler den Stream jedoch nicht. Sobald der Streaming Loader einen neuen Wert liefert, stellt die Resource ihn bereit.

Abbildung 4 zeigt die Konsolenausgaben des Loaders. Sie unterstreichen, dass die Streaming Resource zu einem Zeitpunkt lediglich den aktuellen Stream nutzt und sich somit die switchMap-Semantik ergibt.

steyer_kolumne_4
Abb. 4: Übergang zu neuen Streams

Kurz vor jeder Ausgabe von „clean up!“ hat der User die Funktion forward ausgelöst. Dies führt zu einem neuen Wert in startValue und dieser Umstand triggert den Loader erneut. Die Resource benachrichtigt das AbortSignal des alten Streams, den ihr abort-Handler beendet. Fortan nutzt die Resource den vom Loader gelieferten neuen Stream, der in unserem Beispiel mit dem nächsten Hunderter fortsetzt.

Interop mit RxJS und Observables

Gemeinsam mit dem Resource API führte Angular 19 auch die rxResource ein, die den Brückenschlag zur RxJS-Welt erlaubt. Ab Angular 19.2 sind rxResources immer Streaming Resources. Das bedeutet, eine rxResource liefert nach und nach die Werte des vom Loader zurückgelieferten Observables. Um dieses Verhalten zu demonstrieren, zeigt Listing 6 eine Variation des zuvor besprochenen Timers auf Basis von rxResource.

Listing 6

export function timerResource(
  timeout: number,
  startValue: () => number
): ResourceRef<number | undefined> {

  const request = computed(() => ({
    startValue: startValue(),
  }));

  return rxResource({
    request: request,
    loader: (params) => {
      const startValue = params.request.startValue;
      return interval(timeout).pipe(
        map((v) => v + startValue + 1),
        startWith(startValue),
        tap((v) => console.log('counter', v)),
        switchMap((value) => {
          if (value === 7 || value === 13) {
            return throwError(() => 'bad luck');
          }
          return [value];
        })
      );
    },
  });
}

Diese Implementierung ist dank der zahlreichen Operatoren, die RxJS anbietet, etwas kürzer. Prinzipiell ließen sich analoge Hilfsfunktionen auch für Signals und Resources bereitstellen. Allerdings habe ich das Gefühl, dass der Trend in der Signals-Welt eher in Richtung anwendungsfallbezogener und somit grobgranularer Building Blocks wie timerResource geht. Die Zeit wird zeigen, ob sich dieser Eindruck bewahrheitet.

Wichtige Details beim Einsatz von Streams mit rxResource

Ein kleines Detail bei der rxResource ist die Tatsache, dass die Anwendung den Streaming Loader nicht über die Eigenschaft stream, sondern über loader bereitstellt. Das mag der Tatsache geschuldet sein, dass ein Loader einer rxResource immer ein Streaming Loader ist. Möchte die Anwendung, wie in der ursprünglichen Implementierung in Angular 19.0 und 19.1 lediglich den ersten Wert des Observables nutzen, muss sie nun auf Operatoren wie first oder take(1) zurückgreifen.

Ein weiterer wichtiger Unterschied ergibt sich beim Auftreten eines Fehlers. Hier prallen die Semantiken von Observables und Resources aufeinander. Ein Observable schließt automatisch beim ersten unbehandelten Fehler. Aus diesem Grund erholt sich das Observable in der rxResource nach dem ersten Fehler, der in unserem Beispiel anstatt des Wertes 7 auftritt, nicht mehr. Allerdings kann sich eine rxResource aus dem Fehlerzustand retten, indem es auf einen neuen Stream wechselt. Dazu muss sich zum Beispiel das request-Signal ändern, das den Loader erneut anstößt, dass dieser ein neues Observable liefert.

Wie gewohnt kommt beim Wechsel auf neue Observables die switchMap-Semantik zum Einsatz. Das heißt, dass auch die rxResource zu einem Zeitpunkt lediglich das neueste Observable nutzt. Vorgänger schließt sie, indem sie sich abmeldet (unsubscribe).

Streaming Resources für einen Chat mit WebSockets

Das bis jetzt betrachtete Minimalbeispiel war bereits umfangreich genug, um die Nutzung von Streaming Resources und ihre semantischen Details zu betrachten. Nun möchte ich jedoch einen Schritt weiter gehen und einen etwas praxisnäheren Anwendungsfall zeigen. Dabei handelt es sich um einen einfachen Chatclient (Abb. 2), der es erlaubt, weitere Details zu diskutieren.

Der Angular-Client initialisiert den Chat mit der Factory chatConnection. Sie liefert eine Struktur, die die einzelnen Chatnachrichten als Streaming Resource repräsentiert und daneben noch weitere Signals sowie eine send-Methode anbietet (Listing 7).

Listing 7

@Component([…])
export class ChatResourceComponent {
  ResourceStatus = ResourceStatus;

  userName = signal('');
  chat = chatConnection('ws://localhost:6502', this.userName);
  messages = computed(() => this.chat.resource.value() ?? []);

  userNameInField = linkedSignal(() => this.chat.acceptedUserName());
  currentMessage = signal<string>('');

  send() {
    this.chat.send(this.currentMessage());
    this.currentMessage.set('');
  }

  join() {
    this.userName.set(this.userNameInField());
  }
}

Das an chatConnection übergebene userName-Signal triggert die Resource und führt zum Verbindungsaufbau mit dem Chatserver. Die Signals userNameInField und currentMessages sind an Eingabefelder gebunden. Um eine Verbindung aufzubauen, schreibt die Funktion join den Wert von userNameInField in das Signal userName.

Der Server kann den gewünschten Benutzernamen korrigieren, um für Eindeutigkeit zu sorgen. Deswegen ist userNameInField mit linkedSignal implementiert. Dieses Linked Signal überschreibt den lokalen Wert mit dem korrigierten Wert vom Server, der sich im Signal accpetedUserName befindet.

Möchten Benutzer:innen die aktuelle Nachricht absenden, übergibt die Anwendung die currentMessage an die send-Methode. Daraufhin setzt sie die currentMessage zurück, damit Benutzer:innen die nächste Nachricht erfassen können.

 

Glitch-free Property als Herausforderung bei Streaming Resources

Das Chatbeispiel unterscheidet sich in einem essenziellen Aspekt vom zuvor besprochenen Timer: Während beim Timer immer nur die neueste Nachricht aus dem Stream von Interesse war, sind es beim Chat sämtliche bisher empfangene Nachrichten oder zumindest alle Nachrichten aus einem definierten Zeitfenster. Anders ausgedrückt repräsentiert der Timer eher ein Objekt, das sich im Laufe der Zeit ändert, während der Chat hingegen mehrere Events mit Nachrichten repräsentiert, die die Anwendung im Laufe der Zeit empfängt und in Ihrer Gesamtheit zu berücksichtigen hat.

Die Art und Weise, wie wir hier den Timer betrachten, passt ganz gut zu Signals, zumal Signals jeweils den neuesten Wert wiederspiegeln. Hier passt auch die Glicht-free-Eigenschaft, die unnötige Zwischenwerte ignoriert: Würde die Resource den Timer in einem Atemzug, also in einem einzigen Task, von 0 auf 1, von 1 auf 2 und von 2 auf 3 ändern, sähe der Konsument der Resource nur den Wert 3.

Genau dieses Verhalten wäre jedoch bei einer Event-basierten Nutzung wie beim Chat fatal. Hier gingen einzelne Nachrichten verloren. Dieses Verhalten lässt sich leicht testen, indem man den Chatserver veranlasst, dieselbe Nachricht hintereinander mehrfach an denselben Client zu senden. Die meisten dieser Wiederholungen würde bei einer Implementierung, die lediglich die zuletzt empfangene Nachricht berücksichtigt, verloren gehen.

Diese Überlegung zeigt, dass Signals im Gegensatz zu Observables in RxJS nicht wirklich gut für das Repräsentieren von Events bzw. Nachrichten geeignet sind. Das stellt uns beim Einsatz von Streaming Resources vor eine Herausforderung, die sich glücklicherweise durch eine einfache Anpassung unserer Sichtweise bewältigen lässt: Lassen wir die Resource nicht nur den aktuellen Wert repräsentieren, sondern alle bisher empfangenen Nachrichten oder zumindest alle Nachrichten eines für uns interessanten Zeitfensters, können wir diese Gesamtheit an Nachrichten wieder als Wert betrachten, der sich im Laufe der Zeit ändert.

Anders ausgedrückt müssen wir das Sammeln und das Verwalten der einzelnen Nachrichten in die Resource verschieben. Diese Sichtweise passt auch gut zu meiner weiter oben erwähnten Beobachtung, wonach in der Signals-Welt eher anwendungsfallspezifische und somit grobgranulare Buildings Blocks zum Einsatz kommen.

Typen für Nachrichten

Für die einzelnen Nachrichten, die der Client mit dem Chatserver austauscht, legt das Beispiel einige Typen fest (Listing 8).

Listing 8

export type ChatRequest =
  | {
      type: 'username';
      id: number;
      name: string;
    }
  | {
      type: 'message';
      id: number;
      text: string;
    };

export type ChatResponse =
  | {
      type: 'id';
      id: number;
    }
  | {
      type: 'username';
      id: number;
      name: string;
    }
  | {
      type: 'message';
      id: number;
      name: string;
      text: string;
    };

Das Protokoll zwischen Client und Server gestaltet sich wie folgt:

  1. Der Client baut eine WebSockets-Verbindung auf.

  2. Der Server reagiert mit einer ChatResponse vom Typ id, über die der Client eine eindeutige Session-ID erhält.

  3. Der Client übersendet einen ChatRequest vom Typ username, mit dem er über den Namen des Benutzers informiert.

  4. Der Server bestätigt diesen Benutzernamen mit einer ChatResponse vom Typ username. Falls der gewünschte Benutzername schon vergeben ist, erhält der Client über diese Nachricht einen korrigierten Benutzernamen, den der Server durch Anhängen einer Zahl an den Wunschnamen bildet.

  5. Der Client übersendet ChatRequests mit seinen Nachrichten (Typ message). Der Server verteilt diese Nachricht in Form einer ChatResponse des Typs message an alle Clients.

Repräsentation des Chats

Die Verbindung mit dem Chat repräsentiert das Beispiel mit dem Typ ChatConnection (Listing 9).

Listing 9

export type SendFn = (message: string) => void;

export type ChatConnection = {
  resource: ResourceRef<ChatResponse[] | undefined>;
  connected: () => boolean;
  acceptedUserName: () => string;
  send: SendFn;
};

Das Herzstück der ChatConnection ist eine Resource, die sämtliche bisher empfangene Nachrichten in Form eines Arrays repräsentiert. Daneben liefert sie noch weitere Statusinformationen als Signals. Dazu gehört der Verbindungszustand (connected) und der ggf. vom Server korrigierte Benutzername (accpetedUserName). Die Methode send erlaubt es dem Chat, Nachrichten zum Server zu senden. Den übergebenen String bildet die Implementierung auf einen ChatRequest vom Typ message ab.

 

Factory für Chat

Die Factory für den Chat ist ähnlich wie die für den zuvor besprochenen Timer aufgebaut. Allerdings bereitet sie anfangs ein paar zusätzliche Variablen vor (Listing 10). Zu diesen Variablen gehört die connection, die die vom Streaming Loader aufgebaute WebSockets-Verbindung widerspiegelt, die zuvor erwähnten Signals connected und accpetedUserName sowie ein Signal id, das die aktuelle Usersitzung repräsentiert. Am Ende liefert die Factory ein paar dieser Signals sowie die erzeugte Streaming Resource und die Methode send als ChatConnection zurück. Das Signal id veröffentlicht sie nicht, da es sich dabei um ein Implementierungsdetail handelt.

Listing 10

export function chatConnection(
  websocketUrl: string,
  userName: () => string
): ChatConnection {

  let connection: WebSocket;

  const connected = signal(false);
  const id = signal(0);
  const acceptedUserName = signal('');

  const request = computed(() => ({
    userName: userName(),
  }));

  const chatResource = resource({
    request,
    stream: async (params) => {
      // init web socket connection
      // handle and collect messages
      […]
    },
  });

  const send: SendFn = (message: string) => {
    const request: ChatRequest = {
      type: 'message',
      id: id(),
      text: message,
    };
    connection.send(JSON.stringify(request));
  };

  return {
    connected,
    resource: chatResource,
    acceptedUserName,
    send,
  };
}

Der Streaming Loader der Resource legt sich zunächst ein Array messages zurecht, in dem er die empfangenen Chatnachrichten sammelt (Listing 11). Die restliche Implementierung folgt den zu Beginn besprochenen vier Punkten. Das vorbereitete Signal spiegelt den Stream wider und nimmt das verwaltete Array mit den empfangenen Nachrichten auf.

Listing 11

const chatResource = resource({
  request,
  stream: async (params) => {
    const userName = params.request?.userName;

    let messages: ChatResponse[] = [];

    // 1. Create Signal representing the Stream
    const resultSignal = signal<StreamItem<ChatResponse>>({
      value: messages,
    });

    if (!userName) {
      return resultSignal;
    }

    // 2. Set up async logic updating the Signal
    connection = new WebSocket(websocketUrl, 'json');

    connection.addEventListener('open', (event) => {
      console.log('[open]');
      connected.set(true);
    });

    connection.addEventListener('message', (event) => {
      const value = JSON.parse(event.data) as ChatResponse;
      console.log('[message]', value);

      if (value.type === 'id') {
        id.set(value.id);
        sendUserName(value.id, userName, connection);
      }

      if (value.type === 'username' && value.id == id()) {
        acceptedUserName.set(value.name);
      }

      if (value.type === 'message' || value.type === 'username') {
        messages = [...messages, value];
        resultSignal.set({ value: messages });
      }
    });

    connection.addEventListener('error', (event) => {
      const error = event;
      console.log('[error]', error);
      resultSignal.set({ error });
    });

    // 3. Set up clean-up handler triggered by AbortSignal
    params.abortSignal.addEventListener('abort', () => {
      console.log('clean up!');
      connection.close();
      connected.set(false);
      id.set(0);
      acceptedUserName.set('');
    });

    // 4. Return Signal
    return resultSignal;
  },
});


function sendUserName(id: number, userName: string, connection: WebSocket) {
  const message: ChatRequest = {
    type: 'username',
    id: id,
    name: userName,
  };
  connection.send(JSON.stringify(message));
}

Über die eingerichteten Event Handler empfängt der Client Informationen vom Server. Das message-Event kümmert sich um das weiter oben beschriebene Protokoll. Es ergänzt das Array messages um alle empfangenen Nachrichten. Das muss immutable, also durch eine flache Kopie, erfolgen, damit das Signal die Änderung wahrnimmt.

Das open-Event setzt connected auf true und das error-Event überführt den aktuellen Fehler in den Stream. Das abort-Event des AbortSignals schließt die WebSockets-Verbindung und setzt die verwalteten Eigenschaften auf ihren Ausgangszustand zurück. Am Ende liefert der Streaming Loader wie gewohnt das Signal zurück, das den Stream widerspiegelt.

Zusammenfassung

Streaming Resources ab Angular 19.2 repräsentieren Datenströme, ohne dabei auf RxJS angewiesen zu sein. Der asynchrone Streaming Loader aktualisiert über die Zeit hinweg ein Signal, das den Stream repräsentiert. Das request-Signal der Resource triggert den Streaming Loader, der einen neuen Datenstrom liefert und den alten beendet – das entspricht der aus RxJS bekannten switchMap-Semantik.

Allerdings repräsentieren Signals in Angular immer nur den aktuellen Wert; Zwischenwerte bei Änderungen, die im selben Task aufeinander folgen, werden ignoriert (Glitch-free-Eigenschaft). Das ist für sich ändernde Zustände, wie einen Zähler, gut geeignet, kann jedoch bei Event-Flüssen – wie einem Chat, der sämtliche empfangene Nachrichten anzeigen soll – zu Datenverlusten führen. Die Lösung liegt darin, die (in einem bestimmten Zeitfenster) empfangenen Nachrichten in der Resource zu sammeln und das so erhaltene Array über die Resource zu veröffentlichen. Dieses Array entspricht somit einem Wert, der sich im Laufe der Zeit ändert.

Die rxResource, mit der sich RxJS integrieren lässt, ist nun standardmäßig eine Streaming Resource. Möchte eine Anwendung lediglich den ersten Wert des zugrundeliegenden Observables nutzen, muss sie Operatoren wie first oder take(1) einsetzen. Im Gegensatz zu Observables wird eine Resource, die auf einen unbehandelten Fehler läuft, nicht geschlossen, sondern kann weiterhin Werte veröffentlichen. Da jedoch die rxResource auf einem Observable basiert, kann sich der aktuelle Stream nicht von einem Fehlerzustand erholen. Allerdings ist es auch hier möglich, auf einen neuen Stream zu wechseln, indem die Anwendung den Streaming Loader erneut anstößt.

 

Bleib informiert

Brancheninsights im Newsletter erhalten:

[mc4wp-simple-turnstile]

 

🔍 Frequently Asked Questions (FAQ)

1. Was sind Streaming Resources in Angular 19.2?
Streaming Resources sind eine Erweiterung des Resource API, mit der man kontinuierliche Datenströme darstellen kann — also ein Signal, das über die Zeit hinweg Werte (oder Fehlerzustände) liefert, anstatt nur einmalig.

2. Wozu verwendet man Streaming Resources?
Sie dienen dazu, reaktive, fortlaufende Updates (z. B. Timer, WebSocket-Nachrichten) mit dem Resource-API abzubilden, ohne direkt RxJS-Observables verwenden zu müssen.

3. Wie funktioniert der Streaming Loader unter der Haube?
Ein Streaming Loader erzeugt zunächst ein Signal, das initial einen Wert enthält. Dann startet eine asynchrone Logik, die im Verlauf neue Werte (oder Fehler) über dieses Signal verteilt. Zudem definiert er eine Aufräumlogik, z. B. via AbortSignal, um alte Streams sauber zu beenden.

4. Wie verhält sich der Übergang zwischen verschiedenen Streams (z. B. bei Parameteränderung)?
Wenn sich das request-Signal einer Resource ändert, startet Angular einen neuen Stream und beendet den alten — das entspricht der Semantik von switchMap aus RxJS. Somit wird immer nur der aktuellste Stream konsumiert.

5. Was ist der Unterschied zwischen Streaming Resources und rxResource / Observables?

  • rxResource ist eine Brücke zu RxJS und wird in Angular 19.2 automatisch als Streaming Resource behandelt.
  • Ein wichtiger Unterschied: Bei einem Fehler verhält sich eine Resource anders als ein Observable — ein unbehandelter Fehler schließt bei Resources den Stream nicht, sondern es ist möglich, danach wieder Werte zu liefern.
  • Wenn man mit rxResource nur den ersten Wert eines Observables nutzen will, muss man Operatoren wie first oder take(1) einsetzen.

6. Wie setzen Entwickler Streaming Resources praktisch ein?
Im Artikel werden zwei Beispiele vorgestellt:

  • Ein einfacher Timer, der im Sekundentakt zählt.
  • Ein WebSocket-Chat, in dem alle empfangenen Nachrichten in einem Array gesammelt und als Streaming Resource bereitgestellt werden (nicht nur der letzte Wert).

7. Welche Herausforderungen gibt es bei der Nutzung von Streaming Resources?
Eine Herausforderung ist die Glitch-Free-Eigenschaft von Signals: Wenn mehrere Zwischenwerte in einem Task erzeugt werden, werden nur Endwerte wahrgenommen. Das kann problematisch sein, wenn man ein Ereignis-Streaming (z. B. Chatnachrichten) darstellen will, weil man sonst Nachrichten verlieren könnte. Die Lösung besteht darin, die Nachrichten selbst in der Resource zu sammeln und als Gesamtwert (z. B. Array) zu publizieren.

 

The post Streaming Resources in Angular 19.2 – Datenströme mit dem Resource API appeared first on JavaScript & Angular Days.

]]>
Christian Liebel im Interview: Der Weg in die webbasierte Zukunft https://javascript-days.de/blog/angular/christian-liebel-im-interview-ki-trends/ Wed, 04 Dec 2024 00:38:17 +0000 https://javascript-days.de/?p=106877 Auf den JavaScript und Angular Days im Oktober 2024 haben wir Christian Liebel getroffen und über Progressive Web Apps, neue Technologien wie Service Worker und den Einfluss generativer KI auf die Anwendungsentwicklung gesprochen. Der Trainer erläuterte, wie moderne Webtechnologien Webanwendungen nativer machen und welche Architekturtechniken entscheidend für die Entwicklung von skalierbaren Webapps sind. Zudem ging es um die langfristige Rolle von generativer KI in der Softwareentwicklung.

The post Christian Liebel im Interview: Der Weg in die webbasierte Zukunft appeared first on JavaScript & Angular Days.

]]>

 

Was ist deiner Meinung nach der größte Vorteil, wenn generative KI lokal und offline in einer Anwendung läuft?

Es hat verschiedene Vorteile, die stark davon abhängen, was man konkret benötigt. Zum einen ist es für den Datenschutz sehr vorteilhaft, da die Anfragen und Daten nie das eigene Gerät verlassen. Es ist auch super für die Latenz, da wir direkt auf dem eigenen Gerät arbeiten.

Wie siehst du die Zukunft von KI in der Frontend-Entwicklung?

KI wird nicht verschwinden; sie wird bleiben. Ich denke, wir werden mehr und mehr Anwendungsfälle sehen, bei denen KI eine Rolle spielt. Aktuell ist alles noch sehr chat-fokussiert – immer diese Chat-Nachrichtenboxen. Aber es gibt viel coolere Use Cases für generative KI, die über die Chatfunktionalität hinausgehen, und ich glaube, da werden wir mehr sehen.

NEU! Generative AI für Angular-Entwickler:innen

Erlebe das neue Bootcamp vom 19. – 20. März 2026

Was ist dein wichtigster Tipp für Entwickler:innen, die neu in die Welt der Progressiven Web Apps einsteigen wollen?

Progressive Web Apps (PWA) als webbasiertes Anwendungsmodell sind aus meiner Sicht der Weg der Zukunft. Ich kenne keine Software, die noch nativ gebaut wird – alles geht ins Web. Die wichtigsten Schritte dorthin sind, Webentwicklung zu lernen: HTML, CSS, JavaScript und später TypeScript. Darauf aufbauend sollte man Architektur-Patterns lernen, um in die Welt der Webanwendungsentwicklung einzutauchen.

Welche praktischen Ansätze gibt es in deinen Workshops?

In meinem Workshop schauen wir uns speziell die Technologien an, mit denen man Webanwendungen noch nativer machen kann. Dazu gehört das Web Manifest für die Installierbarkeit, Service Worker für die Offline-Fähigkeit und viele neue Webanwendungsschnittstellen, die durch Project Fugu ins Web gekommen sind.

Du bist zum wiederholten Male auf den JavaScript & Angular Days, was lässt dich immer wieder zurückkehren?

Ich bin immer wieder gerne dabei, weil wir viel Zeit haben, ein Thema intensiv zu behandeln. Bei normalen Konferenzen hat man oft nur 30 bis 60 Minuten, aber hier haben wir wirklich mindestens einen halben Tag Zeit, um uns den Themen ausführlicher zu widmen, als es in kurzen Sessions möglich wäre.

Hat sich der Fokus auf KI-Integration gut im Programm der diesjährigen JavaScript & Angular Days widergespiegelt, und welche KI-Workshops sollten im nächsten Jahr nicht fehlen?

Ich finde das Programm sehr gut. Generative KI reflektiert das, und wir haben diesen Trend in unserem Track ebenfalls aufgegriffen. Es ist nicht nur ein Trend – ich bin überzeugt, dass KI als wesentlicher Architekturbaustein bleiben wird. In vielen Workshops und Vorträgen haben wir das auch gesehen, und ich glaube, es ist uns gut gelungen.

The post Christian Liebel im Interview: Der Weg in die webbasierte Zukunft appeared first on JavaScript & Angular Days.

]]>
Angular wird 18 – erwachsen aber dynamisch https://javascript-days.de/blog/angular-18-alle-neuen-features/ Thu, 13 Jun 2024 10:50:19 +0000 https://javascript-days.de/?p=16734 Mit Angular 18 steht Entwicklern eine Vielzahl von neuen Features und Verbesserungen zur Verfügung, die die Entwicklung moderner Webanwendungen weiter optimieren. Angular 18 setzt auf die Fortschritte der Vorgängerversionen auf und führt zahlreiche Performance-Optimierungen sowie neue APIs ein, die die Arbeit mit dem Framework noch effizienter gestalten. Um neue und bestehende Entwickler optimal zu unterstützten, wurde auch die mit Angular 17 neu releaste Entwickler-Doku unter angular.dev aus der Beta-Phase gehoben. Im gleichen Schritt leitet die alte Doku-Seite “angular.io” automatisch auf die neue Seite um.

The post Angular wird 18 – erwachsen aber dynamisch appeared first on JavaScript & Angular Days.

]]>
Typescript 5.3 / 5.4

Nachdem mit Angular 17 die TypeScript-Versionen ab 5.2 verwendet werden können, muss mit Angular 18 nun mindestens TypeScript in Version 5.4 genutzt werden. Die Versionen 5.3 und 5.4 enthalten jeweils viele Verbesserungen in Bezug auf Performance und Type-Inference bzw. Type-Erkennung. Außerdem wurden mit den beiden Versionen viele neue Features eingeführt. Beispielhaft sollen einige der Neuerungen an dieser Stelle erklärt werden.

Type-Narrowing bei Switch-Statements

Um genauere Entscheidungen über verwendete Typen treffen zu können, beherrscht TypeScript das Type-Narrowing. Type-Narrowing kann genutzt werden, um aus einem allgemeineren Typen einen spezielleren abzuleiten. Umgesetzt wird Type-Narrowing in TypeScript typischerweise durch sogenannte Type-Guards. In Listing 1 bekommt beispielsweise die Funktion getColors() den Parameter status übergeben, dessen Typ entweder ein Array verschiedener Status-Elemente sein kann, ein einzelner Status oder null. Soll nun auf ein Property des Status zugegriffen werden, etwa status.color, so ist das nicht direkt möglich, da nicht definiert ist, ob ein Status-Objekt vorliegt, mehrere Objekte in einem Array oder gar null. Zur Fallunterscheidung werden in Listing 1 if-Bedingungen verwendet, die als Type-Guards dienen. Innerhalb der if-Blöcke ist die Variable jetzt auf den entsprechenden Typ “eingeengt”, daher der Begriff “Type-Narrowing”.

Listing 1: Type-Narrowing mit If-Bedingungen

function getColors(status: Status[] | Status | null): string[] {
 if(!status) {
   return []; // `status` ist `null`
 }
 if(Array.isArray(status)) {
   return status.map(stat => stat.color) // `status` ist ein `Array<Status>`
 }
 return [status.color]  // `status` ist ein `Status`-Objekt
}

Mit TypeScript 5.3 wurde nun die Möglichkeit geschaffen, Type-Narrowing in Verbindung mit switch(true)-Ausdrücken zu verwenden. Bei switch(true)-Ausdrücken wird jeweils immer nur der Pfad ausgeführt, der momentan true ergibt. Daher sollte in einem solchen Fall auch Type-Narrowing möglich sein, dies wurde mit TypeScript 4.3 umgesetzt. Ein Beispiel dazu ist in Listing 2 gezeigt. Jeder Branch der Switch-Anweisung ist durch einen eigenen Type-Guard abgesichert, sodass ein sicherer Zugriff auf die jeweiligen Eigenschaften möglich ist. Ein solches Statement kann nun also grundsätzlich wie eine if-elseif-else-Verkettung verwendet werden, je nach Entwicklerpräferenz.

Listing 2: Type-Narrowing mit switch(true)


function getColors(status: Status[] | Status | null): string[] {
 switch (true) {
   case !status: {
     return []; // `status` ist `null`
   }
   case Array.isArray(status): {
     return status.map(stat => stat.color) // `status` ist ein `Array<Status>`
   }
   default: {
     return [status.color]  // `status` ist ein `Status`-Objekt
   }
 }
}

Object.groupBy und Map.groupBy

Mit dem neuesten ECMAScript-Standard ES2024 wird zu den bestehenden Klassen Object und Map jeweils die neue Methode groupBy() hinzugefügt. Diese kann verwendet werden, um die Einträge eines Iterable, also z.B. eines Array, anhand eines Kriteriums zu gruppieren und in ein Objekt zu überführen. In Listing 3 ist gezeigt, wie groupBy genutzt werden kann, um ein Array mit Zahlen in ein Array für gerade und eines für ungerade Zahlen aufzuspalten. Dazu bekommt die groupBy-Funktion das Array numberArray mit Zahlen übergeben und eine Iterator-Funktion, die jedem Wert im Array eine Gruppe zuweist. In diesem Fall wird mit Hilfe des Modulo-Operator geprüft, ob der Wert gerade oder ungerade ist und entsprechend die Gruppe even oder odd vergeben. Das Resultat ist ein Objekt, welches die Eigenschaften even und odd hat, wobei sich in einem Property das gerade, im anderen Property das ungerade Array befindet. Beide Properties müssen mit dem Safe-Navigation-Operator (auch Elvis-Operator, “?.”) aufgerufen werden, da ein Property nur dann angelegt wird, wenn die entsprechende Gruppe auch Einträge hat.

Von der Syntax her genau wie Object.groupBy() funktioniert auch Map.groupBy(), jedoch wird im Falle von Map.groupBy() eine Map zurückgegeben, deren Keys in diesem Fall even und odd wären. Anders als bei Objekten können bei Maps nicht nur Strings, sondern im Prinzip beliebige Objekte als Key dienen.

Listing 3: Verwendung von Object.groupBy()

const numberArray = [0, 1, 2, 3, 4, 5];
const evenOddObj = Object.groupBy(numberArray, (num) => {
 return num % 2 === 0 ? "even": "odd";
});


// Resultat ist Objekt mit Properties "even" und "odd"
console.log(evenOddObj?.even); // ->[0, 2, 4]
console.log(evenOddObj?.odd); // ->[1, 3, 5]

Erlebe Angular Trends live

16. - 20. März 2026

>

Einschränkungen bei Enums

Bisher war es möglich, in TypeScript-Enums die vorbelegten TypeScript-Keywords “Infinity”, “-Infinity” und “NaN” als Enum-Key zu verwenden, siehe Listing 4. Dies ist mit TypeScript 5.4 nicht mehr möglich.

Listing 4: Mit TypeScript 5.4 nicht mehr erlaubte Enum-Keys


//Errors mit diesen Keys
enum NotAllowsEnumKeys {
Infinity,
"-Infinity",
NaN
}

Angular CLI

Bereits mit den Angular-CLI Versionen 17.1/17.2/17.3 sind einige erwähnenswerte Neuerungen in Angular eingeflossen. Diese sollen hier auf dem Weg von Angular 17 zu Angular 18 ebenfalls kurze Erwähnung finden.

Angular CLI 17.1, 17.2 und 17.3

Der Karma-Testrunner, den Angular-CLI für das Ausführen der Unit-Tests nutzt, wird mittlerweile nicht mehr weiterentwickelt. Daher ist das Angular-Team dabei, als Ersatz für Karma entweder das Jest-Testframework anzubieten, oder den @web/test-runner. Mit Jest laufen die Tests dabei rein in der Node.js-Umgebung, während der Web-Test-Runner die Tests – wie von Karma gewohnt – in der Browser-Laufzeitumgebung ausführt. Der Jest-Support kam als Preview bereits mit Angular 17, mit 17.1 kam dann der erste (experimenteller) Support für den Web-Test-Runner, nämlich in Form eines eigenen Test-Builders (@angular-devkit/build-angular:web-test-runner). Das Angular-Team ist also weiter fleißig dabei, das Unit-Testing mit Angular in eine neue Zeit zu bringen.

Eine weitere Anpassung an moderne Web-Entwicklungsumgebungen ist die neue Möglichkeit, Angular-Projekte nicht nur mit NPM- oder Yarn-Paketmanagern erzeugen und nutzen zu können, sondern auch mit Bun. Bun ist ein neuer Paketmanager im NPM-Umfeld, der deutlich schneller als NPM und Yarn sein soll.

Für eine bessere Übersicht beim Entwickeln ist außerdem ermöglicht worden, den Terminalinhalt zwischen Rebuilds zu leeren. Dazu muss lediglich die neue Option clearScreen in der angular.json angegeben werden, wie in Listing 5 dargestellt.

Listing 5: “clearScreen”-Option in der angular.json

{
 "projects": {
   "clear-demo": {
     "projectType": "application",
     "root": "",
     "sourceRoot": "src",
     "architect": {
       "build": {
         "builder": "@angular-devkit/build-angular:application",
         "options": {
           "clearScreen": true
         }
       }
     }
   }
 }
}

Angular CLI 18

Die wichtigste Neuerung im Angular-CLI 18 ist, dass Version 22 der Node.js-Laufzeitumgebung unterstützt wird. Damit können Entwickler auch auf die neueste Version der Node.js-Laufzeit im Rahmen der Angular-Entwicklung zurückgreifen. Daneben gibt es einige Erweiterungen, die das Entwickler-Leben einfacher machen sollen. Zum Beispiel hat das Kommando ng serve einen neuen Alias bekommen. Bisher gab es beispielsweise den Alias “ng s”, mit dem der Dev-Server gestartet werden konnte, neu dazu gekommen ist nun “ng dev”.

Gerade bei neuen Entwicklern konnte es laut Angular-Team unter Umständen zu Verwirrungen um den assets-Ordner in Angular-Projekten kommen. Dieser Ordner ist explizit für statische (Laufzeit-)Assets gedacht, während die Build-Zeit-Assets unterhalb des src-Ordner abgelegt sein sollten. Um diesen Unterschied klarer zu machen, heißt der Ordner bei neu generierten Projekten in Zukunft deshalb nicht mehr assets, sondern public.

Weiterhin wird mit Angular-CLI 18 der Befehl ng doc entfernt, da dieser ohnehin nur selten genutzt wurde. Die offizielle Empfehlung ist, stattdessen auf der Seite “https://angular.dev/” nachzuschauen.

Breaking Changes

Neben der neuen unterstützten Node.js-Version 22 wurde auch die minimal notwendige Node.js-Version angepasst. Die minimal notwendige LTS-Node.js-Version ist nun 18.19.1. Node.js 19 wird gar nicht mehr unterstützt und Node.js 20 erst ab Version 20.11.1.

Weiterhin wurde die Anbindung der alten, nur noch im Webpack-Build unterstützten Sass-API (“NG_BUILD_LEGACY_SASS”) entfernt. Das Legacy-API ermöglichte beispielsweise noch die alte “~”-Import-Syntax.

 

Angular 18

Auch mit den minor Angular-17-Versionen sind einige interessante Neuerungen in Bezug auf die Zukunft von Angular hinzugekommen. Das betrifft zum einen die Weiterentwicklung des Frameworks im Allgemeinen. Zum anderen sind vor allem die Angular-Signal-APIs deutlich erweitert worden, um so das Ziel zu erreichen, Angular-Anwendungen in Zukunft ohne zone.js laufen lassen zu können. Die Neuerungen sollen an dieser Stelle kurz vorgestellt werden.

Angular 17.1, 17.2 und 17.3

Ganz besondere Aufmerksamkeit haben die Signal-APIs innerhalb der letzten Angular-Versionen erhalten. Dazu gehören vor allem APIs, mit denen die Decorators innerhalb von Komponenten durch Signal-Varianten ersetzt werden können. Dies gilt vor allem für Komponenten bzw. Anwendungen, die künftig ohne Zone.js auskommen sollen. Um die Neuerungen zu veranschaulichen, ist in Listing 6 die Komponente DemoFormComponent gezeigt, die ein boolesches @Input für den Nachtmodus und ein String-@Input für den momentanen Formularwert hat. Das Input für den Formularwert hat außerdem das Flag required, es wird also ein Compiler-Fehler ausgegeben, wenn dieses Input nicht gebunden ist. Außerdem ist ein @Output mit dem Namen valueChange vorhanden, das bei jeder Wertänderung des Formulars emittiert. Zu jedem @Output gehört immer ein EventEmitter. (Da der EventEmitter jedoch vom RxJS-Subject erbt, könnte an dieser Stelle auch ein Observable stehen, dazu später mehr.)

Da der Input value und der Output valueChange heißt, können beide per Two-Way-Databinding angebunden werden:
<app-demo-form [(value)]=”firstname”></app-demo-form>

Die Variable firstname kann entweder eine normale String-Variable sein, oder wiederum ein signal(”).

Listing 6: Herkömmliche Angular-Standalone-Komponente mit Inputs und Outputs

@Component({
 selector: 'app-demo-form',
 standalone: true,
 imports: [FormsModule],
 template: `
   <div [class.night-mode]="nightMode">
     <input type="text" 
            [ngModel]="value"
            (ngModelChange)="valueChanged.emit($event)">
   </div>`
})
export class DemoFormComponent {
 @Input() nightMode?: boolean;


 @Input({required: true}) value!: string;
 @Output() valueChange = new EventEmitter<string>();
}

In Listing 7 ist im Vergleich dazu eine Komponente, die auf den neuen Signal-APIs basiert. Der “nightMode”-Input hat nun keinen “@Input”-Decorator mehr, stattdessen wird die “input()”-Funktion verwendet. Dabei ist zu beachten, dass beim Input zwar der Typparameter “boolean” angegeben ist, jedoch das Signal eigentlich vom Typ “boolean | undefined”. Der “undefined”-Fall tritt dann ein, wenn von außen kein Wert an das Input gebunden wird. Ansonsten erzeugt die “input()”-Funktion ein Signal, das immer den aktuell von außen gebundenen Input-Wert enthält. Immer wenn von außen ein neuer Wert gebunden wird, wird auch das Template aktualisiert. Ein Input-Signal kann wie alle Signals auch als Quelle für ein “computed()”-Signal genutzt werden. “computed()”-Signals entsprechen somit Properties, die man ohne die Signal-APIs innerhalb des “ngOnChanges()”-Hook schreiben würde.

Das Two-Way-Databinding vereinfacht sich mit der Signal-API deutlich, denn es wird kein separates Input/Output mehr benötigt, stattdessen wird ein einfaches ModelSignal erstellt, das sowohl Input als auch Output in sich vereint.

Im Beispiel von Listing 7 ist auch die neue “.required”-API gezeigt, die dem Required-Flag bei den @Input-Decorators entspricht. Im Gegensatz zu den Decorators wirkt sich das Required bei den Signals auch auf den Typen aus: Ein “input<boolean>()” enthält Werte mit Typ “boolean | undefined”, während ein “input.required<boolean>()” immer ein echtes “boolean” enthält. Das gleiche gilt für “model” und “model.required”.

Listing 7: Angular-Standalone-Komponente mit Signal-Input und -Model

@Component({
selector: 'app-demo-form,
standalone: true,
imports: [FormsModule],
template: `
  <div [class.night-mode]="nightMode()">
    <input type="text" [(ngModel)]="value">
  </div>`
})
export class DemoFormComponent {
nightMode = input<boolean>();


value = model.required<string>();
}

Neben dem “@Input”-Decorator haben auch die “@ViewChild”-, “@ViewChildren”-, “@ContentChild”- und “@ContentChildren”-Decorators eine neue Signal-API bekommen. In Listing 8 ist eine Komponente gezeigt, wie sie z.B. zur Darstellung von Graphen bzw. Charts genutzt werden könnte. Um das HTMLCanvasElement aus dem Template heraus an die Charting-Library zu übergeben, wird der “@ViewChild”-Decorator verwendet. Außerdem wird, sobald der Chart gerendert ist, der “chartLoaded”-EventEmitter getriggert, um ein Angular-”@Output”-Event auszulösen.

Listing 8: Angular-Komponente mit Canvas, Output und ViewChild


@Component({
  selector: 'app-demo-chart',
  standalone: true,
  template: `<canvas #canvasEl></canvas>`
})
export class DemoChartComponent implements AfterViewInit {
  @ViewChild('canvasEl') canvas!: ElementRef<HTMLCanvasElement>;
  @Output() chartLoaded = new EventEmitter<string>();

  ngAfterViewInit(): void {
    const context = this.canvas.nativeElement.getContext('2d');
    const chart = chartingLib.drawChart(context);
    chart.onChartReady = chart => {
      this.chartLoaded.emit(chart.id);
    };
  }
}

In Listing 9 ist die gleiche Komponente gezeigt, diesmal jedoch mit der entsprechenden Signal-API. Die Variable “canvas” bekommt nun keinen Decorator, stattdessen wird die “viewChild()”-Funktion aufgerufen, die auch “Query-Funktion” genannt wird, da mit diesen Funktionen Elemente des Template abgefragt (engl. ge-queried) werden können. Analog zum ViewChild gibt es die Query-Funktionen “viewChildren()”-, “contentChild()”- und “contentChildren()”. Im Beispiel von Listing 9 ist außerdem gezeigt, dass die Query-Funktion “viewChild” analog zum Input die “.required”-API mitbringen. Das bedeutet, dass der Typ des ViewChild in diesem Fall nicht “Signal<ElementRef<HTMLCanvasElement> | undefined>” ist, sondern nur “Signal<ElementRef<HTMLCanvasElement>>” – also ohne undefined. Das funktioniert aber nur, wenn die Template-Referenz-Variable (hier “canvasEl”) immer direkt verfügbar ist. Ansonsten gibt es einen Laufzeitfehler.

Die Children-Query-Funktionen haben keine Required-API, stattdessen liegen die einzelnen ElementRefs hier als Array vor. Wenn es kein Child gibt, auf das die Query passt, enthält das Signal einfach ein leeres Array. Der Datentyp von “viewChildren<ElementRef<HTMLCanvasElement>>(‘canvasEl’)” wäre somit “Signal<ElementRef<HTMLCanvasElement>[]>”.

Ein weiteres Detail in Listing 9 ist die neue Output-API: Statt “@Output”-Decorator und “EventEmitter” kann einfach die Funktion “output()” verwendet werden. Die neue API baut auch nicht mehr auf dem EventEmitter auf, stattdessen liefert “output()” eine “OutputEmitterRef” zurück. Das wurde auch deshalb gemacht, um die API etwas unabhängiger von RxJS zu machen, denn der alte EventEmitter ist im Kern ein RxJS-Subject. Diese Eigenschaft wurde von einigen Entwicklern so interpretiert, dass statt des EventEmitters ein “normales” Observable an die “@Output”-Felder gebunden wurde. Um die Nutzung der Outputs mit Observables auch in Zukunft zu ermöglichen, kann in der neuen API statt “output()” die Funktion “outputFromObservable(this.demoObservable)” verwendet werden. Dabei ist “this.demoObservable“ das Observable. Der Output emittiert dann jedesmal ein Event, wenn auch das DemoObservable einen Wert emittiert.

Die neue API ist zwar nicht direkt eine Signal-API, wurde aus Konsistenzgründen allerdings so konzipiert, dass sie wie die neuen Signals-APIs aussieht und auch so verwendbar ist.

Eine wichtige Anmerkung zu den neuen Signal-Komponenten-APIs ist, dass sie bisher noch in Developer-Preview sind. Das bedeutet, dass sie im Allgemeinen schon Produktionsqualität haben, allerdings kann sich im Detail die eine oder andere API noch leicht verändern. Deshalb sollten diese APIs ggf. noch nicht in produktiven Anwendungen eingesetzt werden.

Listing 9: Angular-Komponente mit Canvas und Signal-APIs


@Component({
selector: 'app-demo-chart',
standalone: true,
template: `<canvas #canvasEl></canvas>`
})
export class DemoChartComponent {
canvas = viewChild.required<ElementRef<HTMLCanvasElement>>(
'canvasEl'
);
chartLoaded = output<string>();

constructor() {
afterNextRender(() => {
 const context = this.canvas().nativeElement.getContext('2d');
 const chart = chartingLib.drawChart(context);
 chart.onChartReady = chart => {
  this.chartLoaded.emit(chart.id);
 };
}, {phase: AfterRenderPhase.Write});
}
}

Angular 18

Um die begonnene Angular-Renaissance zu festigen, sind mit Version 18 zunächst die neuen Control-Flow-APIs und die Deferrable-Views als “stable” markiert worden. Angular 18 bringt außerdem viele unterschiedliche neue Features mit. Einige sind im Rahmen der “Angular Renaissance” und den neuen Signal-APIs zu betrachten. Dazu zählt zum Beispiel die Möglichkeit, die App komplett ohne Zone.js laufen zu lassen, sodass nur noch die Signals für die Change-Detection sorgen. Dazu kann der neue ZonelessChangeDetection-Provider beim App-Bootstrap verwendet werden, siehe Listing 10. Es sollte aber beachtet werden, dass dieser noch als experimentell markiert ist und daher nicht in produktiven Anwendungen verwendet werden sollte.

Listing 10: Bootstrap der Anwendung mit ZonelessChangeDetection


 bootstrapApplication(AppComponent, {providers: [
   provideExperimentalZonelessChangeDetection(),
 ]});

In Angular kann der sogenannte Content-Projection-Mechanismus für eine bessere Komposition verschiedener Ober- und Unterkomponenten verwendet werden. Das wird häufig verwendet, um Komponenten zu bauen, die zur Strukturierung von Inhalten gedacht sind. Etwa eine Modal-Komponente, die das Modal in Header, Content und Footer aufteilt; oder eine ListComponent, die per Content-Projection die einzelnen ListItemComponents entgegen nimmt. In Listing 11 ist eine “DemoPageComponent” gezeigt, die die Struktur einer Seite vorgibt. Hier wird die Seite einfach in einen separaten Header- und Content-Bereich aufgeteilt (das Template ist hier bewusst einfach gehalten, um das Beispiel nicht zu verkomplizieren). Das Neue mit Angular 18 ist nun, dass die “”-Slots einen eigenen (Default-)Inhalt haben. Vor Angular 18 war dies nicht möglich. Nun wird zur Laufzeit der Default-Inhalt angezeigt, wenn kein Inhalt übergeben wird. Das Laufzeit-Resultat des Beispiel aus Listing 11 sähe damit in etwa aus wie in Listing 12 gezeigt.

Listing 11: Default-Content für Content-Projection

@Component({
  selector: 'demo-page',
  template: `
    <ng-content select="header">Default Header</ng-content>
    <br>
    <ng-content select="main">Default Content</ng-content>
  `
})
export class DemoPageComponent {}

@Component({
  template: `
    <demo-page>
      <main>My Demo Content</main>
    </demo-page>
  `
})
export class AppComponent {}

Listing 12: DOM-Struktur zur Laufzeit

<app-root>
  <demo-page>
    Default Header
    <br>
    <main>My Demo Content</main>
  </demo-page>
</app-root>

In der Angular Forms-API ist ein lange nachgefragtes Feature hinzugekommen: Auf den FormControls bzw. FormGroups gibt jetzt nicht mehr nur die ValueChange- und StatusChange-Observables (“formControl.valueChanges” bzw. “formControl.statusChanges”). Neuerdings gibt es auch ein allgemeines Events-Observable (“formControl.events”). Dies kann spezielle neue Events emittieren:

  • PristineChangeEvent (Control wechselt zwischen Pristine und Dirty)
  • TouchedChangeEvent (Control wechselt zwischen Untouched und Touched)
  • ValueChangeEvent (Wert-Änderung auf einem Control)
  • StatusChangeEvent (Änderung des Control-Status)
  • Status kann sein :
    ‘VALID’ | ‘INVALID’ | ‘PENDING’ | ‘DISABLED’

Ein weiteres, häufig gewünschtes Feature wird ebenfalls durch die neuen ChangeEvents ermöglicht: Jedes Event enthält das Property “source”, das diejenige FormControl oder FormGroup enthält, die das Event ausgelöst hat. Das kann sehr hilfreich sein, gerade in größeren Formularen: Wenn etwa zentral überprüft werden soll, welche einzelne FormControl sich geändert hat und dadurch eine ganze FormGroup von “valid” auf “invalid” gewechselt ist. Das “events”-Observable kann durch InstanceOf-Checks gefiltert werden, etwa folgendermaßen:

control.events.pipe(filter(e => e instanceof ValueChangeEvent))

Im Router ist es möglich, einen Errorhandler anzugeben, der Navigation-Errors abfängt, damit diese durch die Anwendung behandelt werden können. Der Navigation-Errorhandler muss beim Setup des Router über die Funktion “withNavigationErrorHandler()” angegeben werden. Bisher konnte der Navigation-Error-Handler nur eine “void”-Funktion sein. Mit Angular 18 ist es nun auch möglich, dass der Navigation-Error-Handler ein “RedirectCommand” zurückgibt, sodass eine Error-Navigation durchgeführt werden kann. In Listing 13 ist beispielhaft gezeigt, wie das RedirectCommand genutzt wird, um bei einem Navigationsfehler auf die Route “/error” zu navigieren.

Listing 13: Router-Errorhandler mit RedirectCommand


export const appConfig: ApplicationConfig = {providers: [
 provideRouter(
  appRoutes,
  withRouterConfig({ resolveNavigationPromiseOnError: true }),
  withNavigationErrorHandler(
   () => new RedirectCommand(inject(Router).parseUrl('/error')),
  ),
 ),
])

Eine wichtige Ankündigung des Angular-Teams bezieht sich auf den zukünftigen Entwicklungspfad von Angular, speziell im Vergleich zu anderen, von Google ansonsten genutzten Web-Frameworks. So gibt es neben Angular z.B. noch das interne Google-”Wiz”-Framework. Während Angular von Google eher für geschäftliche und dynamische Anwendungen wie die Google Cloud Console oder Google Analytics verwendet wird, wird Wiz für Anwendungen wie die Google-Suche oder Youtube verwendet, für die vor allem die (initiale Load-)Perfomance wichtig ist. Da Google erkannt hat, dass sich die Anforderungen an neue Apps aber immer mehr überdecken, werden beide Framework-Teams in Zukunft deutlich enger zusammenarbeiten. Das äußert sich für Angular zum Beispiel darin, dass die Serversite-Rendering-Funktionalität (SSR) von Angular deutlich verbessert wurde und auch weiterhin wird. So wird nun z.B. die Bibliothek aus Wiz für Angular zur Verfügung gestellt. “jsaction” wird in SSR-Anwendungen genutzt, um User-Events zu cachen, die vor der vollständigen Hydrierung der Anwendung (also dem Start des dynamischen Teils) erfolgt sind. Nachdem die Anwendung dann im Client läuft, werden diese User-Events wieder abgespielt (“replay”).

Als weitere Verbesserung der SSR-Eigenschaften und damit auch der initialen Lade-Performance von Angular wurde die Client-Hydration als solches erweitert. Dem Client-Hydration-Feature aus Angular 17 wird mit Version 18 die Möglichkeit hinzugefügt, das Angular-I18N-Feature (Angular Internationalization) im Zusammenspiel mit der Hydration zu nutzen. Dazu muss die Funktion “withI18nSupport()” beim Einstellen der Client-Hydration mit angegeben werden, siehe dazu Listing 14.

Listing 14: Client-Hydration mit Angular I18N-Support


bootstrapApplication(AppComponent, {
 providers: [provideClientHydration(withI18nSupport())]
});

Um API-Calls auch in komplexen Systemumgebungen zwischen Client- und Server-App cachen zu können, wurde das Injection-Token “HTTP_TRANSFER_CACHE_ORIGIN_MAP” exportiert. Damit kann ein Mapping zwischen Server- und Client-Origins angegeben werden. Dies ist nötig, wenn eine SSR-Anwendung für API-Calls auf dem Server andere Origins aufrufen muss als die Client-Anwendung zur Laufzeit. Wenn Calls eine Autorisierung benötigen, werden solche Calls jedoch standardmäßig nicht mehr gecachet. Falls dies doch gewünscht ist, gibt es die Option “includeRequestsWithAuthHeaders”, die beim Erzeugen des Cache (per “withHttpTransferCache()”) mitgegeben werden kann.

Die neuen APIs zum Erzeugen des HTTP-Client werden vom Angular-Team mittlerweile als stabil angesehen. Das betrifft allen voran die “provideHttpClient()”-Funktion und deren Plugins. Daher werden mit Angular 18 die älteren APIs als deprecated markiert. Im speziellen sind das das “HttpClientModule”, das “HttpClientXsrfModule” und das “HttpClientJsonpModule”. Diese können einfach durch die jeweilige provide-Funktion ersetzt werden. Falls bisher HTTP-Interceptors verwendet werden (provided mit dem “HTTP_INTERCEPTS”-InjectionToken), so sollte bei der Migration auf die neue Syntax das Plugin “withInterceptorsFromDi()” mit angegeben werden, siehe Listing 15.
Um die Transition möglichst einfach zu gestalten, wird außerdem eine automatische Migration durch das Angular-Team bereitgestellt. Die Migration sorgt bei einem “ng update” dafür, dass die alte Syntax in die neue überführt wird.

Listing 15: HttpClient mit DI-Interceptors konfigurieren

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient(withInterceptorsFromDi())]
};

Breaking Changes

Mit Angular 18 werden auch wieder einige alte, bisher als deprecated markierte APIs entfernt, um das Angular-Framework möglichst einfach und geradlinig zu halten. Dafür wird zunächst einmal der TypeScript-Support für alle TypeScript-Versionen kleiner als 5.4 eingestellt.

Weiterhin wurden einige Optimierungen in Bezug auf die Change-Detection durchgeführt. Das sollte in den meisten Anwendungen keine Auswirkungen haben, kann aber in einigen Grenzfällen zu Fehlern führen. Ein solcher Fall bezieht sich auf die Root-Komponente einer Anwendung (typischerweise “AppComponent”). Wenn bei dieser die ChangeDetection auf OnPush eingestellt ist und die Komponente HostBindings besitzt, wurden die HostBindings bisher auch dann überprüft, wenn die Komponente nicht als dirty (also “geändert”) gekennzeichnet war. Mit Version 18 werden solche Bindings nur noch gecheckt, wenn die Komponente selbst auch als dirty markiert ist.

Um feingranulare Signal-Change-Detection zu ermöglichen, wurde die ChangeDetection auch so verändert, dass nach einem ChangeDetection-Zyklus so lange weiter gecheckt wird, wie es Anwendungsteile gibt, die noch für ChangeDetection markiert sind. Das kann in bestimmten Situationen zu fehlerhaftem Verhalten führen, wenn Views mit “ChangeDetectorRef.detectChanges” immer wieder aufs Neue markiert werden. In solchen Fällen wird ein Fehler mit Fehlercode “NG0103” geworfen. Tatsächlich kann es aber auch dazu kommen, dass in Fällen, in denen vorher ein “ExpressionChangedAfterItHasBeenChecked”-Error gekommen wäre, in Zukunft die Change-Detection normal durchläuft. Das liegt daran, dass die Views ja so lange weiter gecheckt werden, bis es keine Dirty Views mehr gibt. Auch dies kann in sehr speziellen Fällen zu unerwartetem Verhalten führen. Das Angular-Team hat beispielsweise von einer Komponente berichtet, die durch diesen Mechanismus ungewollt zu früh initialisiert wurde. Vermutlich ist dies aber als eher seltener Fall zu bewerten.

Auch im Router gibt es mit Angular 18 einige Breaking Changes, die auf neuen Features beruhen. Als Breaking Change fallen diese Features aber nur dann auf, wenn die Router-APIs sehr intensiv genutzt werden. So können Guards nun nicht nur ein Boolean oder einen UrlTree zurückgeben, sondern mit Angular 18 auch ein “RedirectCommand”. Das Besondere am RedirectCommand ist, dass man damit auch spezifizieren kann, mit welchen “NavigationBehaviorOptions” (also z.B. “replaceUrl”) die Navigation stattfinden soll. Ein einfacher CanActivate-Guard, der ein Redirect-Command zurückgibt, ist in Listing 16 gezeigt.

Listing 16: Einfacher Guard mit Redirect-Command

export const loginGuard: CanActivateFn = (route, state) => {
  const router = inject(Router);
  return new RedirectCommand(
    router.parseUrl('/login'),
    {replaceUrl: true}
  );
};

Ein weiteres neues Feature betrifft Redirects, die direkt in der Routen-Konfiguration angegeben sind. Bisher konnte die “redirectTo”-Angabe innerhalb einer Redirect-Route nur ein fixer String sein. Nun sind hier auch Funktionen möglich, die Teile des ActivatedRouteSnapshot übergeben bekommen und entweder wiederum einen String oder einen UrlTree zurückgeben können. Ein einfaches Beispiel, das einen Query-Parameter aus den Snapshot-Daten ausliest, ist in Listing 17 gezeigt.

Listing 17: Redirect mit Redirect-Funktion

{
  path: '',
  pathMatch: 'full',
  redirectTo: redirectData => {
    const parentId = redirectData.queryParams['id'];
    const router = inject(Router);
    return router.parseUrl('/user/' + parentId);
  }
}

Auch im Angular-Testing-Paket gibt es ein paar Breaking Changes, die sich zum einen auf die aktualisierten Change-Detection-Mechanismus von Angular allgemein beziehen. Zum anderen wurde die Test-Change-Detection in der Angular-Test-Umgebung näher an die “echte” App-Change-Detection angepasst. Das kann sich vor allem dann bemerkbar machen, wenn die Tests sehr sensitiv auf das Timing im Change-Detection-Zyklus reagieren. In einem solchen Fall müssen Tests ggf. auf die neue Situation angepasst werden.

In einem solchen Kontext wird zum Warten auf den Change-Detection-Zyklus gerne das “ComponentFixture.whenStable”-Promise genutzt. Dieses Promise wurde so angepasst, dass es nun dem “ApplicationRef.isStable” entspricht. Daher kann es mit Angular 18 passieren, dass das Promise nicht in allen Fällen resolved. Das passiert vor allem dann, wenn noch Router-Navigationen oder HttpClient-Requests offen sind. In solchen Fällen muss dann über die entsprechenden Mocks (z.B. “HttpTestingController.verify()”) sichergestellt werden, dass die Requests vor Aufruf des “whenStable” abgeschlossen sind. Alternativ muss eine andere geeignete Bedingung als “ComponentFixture.whenStable” zum Warten verwendet werden.

Schlussendlich wurde noch die “async()”-Funktion entfernt, die früher zum Erzeugen der asynchronen Angular-Testing-Umgebung verwendet wurde. Stattdessen sollte nun “waitForAsync()” verwendet werden.

Weitere APIs, die weggefallen sind, betreffen ehemals verwendete SSR-APIs. So wurde der deprecated “platformDynamicServer” entfernt. Genutzt werden sollte der “platformServer”. Auch das “ServerTransferStateModule” wurde in der Vergangenheit deprecated und wird nun entfernt, der “TransferState” kann auch ohne dieses Modul verwendet werden.

Bleib informiert

Brancheninsights im Newsletter erhalten:

[mc4wp-simple-turnstile]

 

Fazit

Angular 18 markiert einen weiteren Fortschritt in der Evolution des Frameworks, indem es eine Vielzahl neuer Funktionen und Optimierungen einführt. Diese Version macht wichtige Control-Flow-APIs und Deferrable-Views stabil, außerdem wird alles für die Nutzung von Angular ohne Zone.js vorbereitet. Die verbesserten SSR-Eigenschaften und die neue Client-Hydration sorgen für eine schnellere und effizientere Ausführung von Anwendungen. Trotz einiger Breaking Changes bieten die neuen Versionen viele Vorteile und Anpassungen, die die Entwicklung und Wartung von Angular-Anwendungen deutlich vereinfachen und verbessern. Es lohnt sich insofern, bei dem Update mitzumachen, da eine typische Anwendung in überschaubarem Maße angepasst werden muss und man damit verhindert, dass es zu einem Update-Rückstau kommt. Wer von den Features nicht direkt Gebrauch macht, kann jedoch auch mit dem Update auf die nächste Version warten.

Entwickler können sich auf eine robuste, zukunftssichere Plattform freuen, die kontinuierlich weiterentwickelt wird, um den wachsenden Anforderungen der modernen Webentwicklung gerecht zu werden.

Als Herausforderung zeichnet sich jedoch ab, dass durch die zunehmende Einführung neuer APIs Angular immer schwerer zu lernen wird, denn die wenigsten Projekte starten auf der grünen Wiese. Entwickler müssen somit alte und neue Konzepte parallel beherrschen. Damit geht einer der größten Vorteile von Angular verloren, nämlich dass es für eine Sache genau ein Konzept gibt und es nicht, wie beispielsweise oft in React-Projekten zu beobachten, unterschiedlichste Umsetzungsvarianten und APIs für dieselben zugrundeliegenden Anforderungen. Hier darf man sehr gespannt sein, wie das Angular-Team mit dem Spannungsfeld Rückwärtskompatibilität und “optionalen neuen Konzepten” im Gegensatz zu klaren, opinionated Strukturen und kognitivem Ballast umgehen wird.

The post Angular wird 18 – erwachsen aber dynamisch appeared first on JavaScript & Angular Days.

]]>
WebAssembly – auch für Angular-Anwendungen https://javascript-days.de/blog/webassembly-auch-fuer-angular-anwendungen/ Fri, 15 Sep 2023 09:16:05 +0000 https://javascript-days.de/?p=16193 Mit WebAssembly, kurz „Wasm“, lassen sich in Webanwendungen rechenintensive Features umsetzen, die dann mit (nahezu) derselben Geschwindigkeit wie nativer Code laufen. Gleichzeitig können mit Hilfe der Technologie viele bestehende Bibliotheken, die in anderen Programmiersprachen wie C oder C++ geschrieben sind, für den Browser verfügbar gemacht werden. Wir sehen uns in diesem Beitrag an, wie die Integration von Wasm in eine Angular-Anwendung technisch ablaufen kann und worauf es zu achten gilt.

The post WebAssembly – auch für Angular-Anwendungen appeared first on JavaScript & Angular Days.

]]>
Zum einen kann mit Wasm anspruchsvolle Logik implementiert werden, die dann das Potenzial der darunterliegenden Hardware besser nutzen kann, als das in JavaScript der Fall wäre.

Einsatzbereiche von WebAssembly

Bereits jetzt besteht die Möglichkeit, SIMD-Instruktionen in WebAssembly zu verwenden und so in nur einem CPU-Zyklus mehrere Datenelemente gleichzeitig zu bearbeiten. Damit lassen sich – je nachdem, ob die zu implementierenden Berechnungen zu diesem Modell passen – Operationen um mehrere Faktoren beschleunigen.

Zum anderen ist es mit WebAssembly möglich, existierenden, nicht auf JavaScript basierenden Bibliotheken für komplexe Probleme wie algorithmische Berechnungen, Grafikverarbeitung und Machine Learning den Weg in den Browser zu öffnen. Dadurch lässt sich im Browser umsetzen, was bisher nativen Anwendungen vorbehalten waren. Beispiele sind Videokonferenzsoftware und Kollaborationslösungen. Ein guter Einsatzbereich für WebAssembly ist etwa, den Hintergrund in einer Videoübertragung durch eine alternative Grafik zu ersetzen oder weichzuzeichnen. Dazu muss die Kontur der Person erkannt und anschließend vom Hintergrund separiert werden. Typischerweise wird diese Aufgabe mit einem neuronalen Netz und Kantenerkennung umgesetzt. Ein für SIMD gut passender Anwendungsfall.

Auch andere technisch anspruchsvolle Features wie Grafikbearbeitung oder Umsetzung von komplexen Algorithmen zur Konfliktauflösung bei der Kollaboration mehrerer Personen, wie sie beispielsweise in Google Docs verwendet wird, sind oft bereits in einer Sprache implementiert, die nicht direkt durch den Browser unterstützt wird. Ist es aber möglich, von dieser Sprache zu WebAssembly zu kompilieren, kann das Ergebnis nun ohne aufwendige Portierung direkt im Browser eingebunden und genutzt werden. Nahezu alle Sprachen unterstützen mittlerweile die Ausgabe von WebAssembly.

Erlebe Angular Trends live

16. - 20. März 2026

>

Angular-Strukturen für komplexe Anwendungen

Im Gegensatz zu reinen Komponentenbibliotheken, wie beispielsweise React, liefert Angular wichtige architektonische Konzepte direkt mit, um auch komplexe Anwendungen sinnvoll modularisieren zu können. So gibt es in Angular das Konzept eines Service: Damit können unter anderem Bestandteile der Anwendung umgesetzt werden, die sich mit Datenhaltung, Datenbeschaffung oder Datentransformation beschäftigen. Mit Services werden zum Beispiel Backends via HTTP mit dem Angular HttpClient oder auch per WebSocket angebunden. Angular verfügt auch über einen Dependency-Injection-Mechanismus. Damit können in der Anwendung sehr einfach Abhängigkeiten definiert werden, ohne dass eine entsprechende Infrastruktur gebaut werden muss, welche die Abhängigkeiten für den jeweiligen Konsumenten bereitstellt. Dabei gibt es einen wichtigen Aspekt: Das Angular-Dependency-Injection-System instanziiert standardmäßig jede Abhängigkeit lediglich einmal. Es handelt sich also effektiv um Singletons, ohne dass das Singleton-Muster dabei implementiert werden muss. Damit ist im Kontext von WebAssembly auf recht einfache Weise sicherzustellen, dass nicht mehrere Instanzen einer ggf. sehr ressourcenintensiven Bibliothek erstellt werden.

Komponenten in Angular dienen schließlich dazu, die Integration mit den für den Nutzer relevanten Browserbestandteilen vorzunehmen. Zum einen können Komponenten durch DOM-Bearbeitung die gewünschte Anzeige, wie z. B. den Inhalt eines Labeltexts, sicherstellen. Zum anderen sind Komponenten auch dafür zuständig, Benutzereingaben in Form von Input-Bindings und Output-Events zu verarbeiten. Klickt ein Nutzer z. B. auf einen Knopf auf der Oberfläche, kann eine Angular-Komponente einen Listener auf das zugehörige Klick-Event registriert haben und daraufhin entsprechende Logik ausführen.

Ergänzend dazu liefert Angular ein eigenes Modulsystem, das die Umsetzung einer gut wartbaren und übersichtlichen Architektur innerhalb der Anwendung unterstützt. So erlaubt es das Modulsystem, zweifelsfrei zu definieren, welche Komponenten für andere Module sichtbar sind. Damit ist eine klar definierte API-Oberfläche möglich und unbeabsichtigte Kopplungen, die sich bei späteren Erweiterungen und Refactorings als Aufwandstreiber oder sogar Show-Stopper herausstellen könnten, werden reduziert. Außerdem bietet Angular APIs, um Module oder auch Komponenten lazy zu laden, also erst bei Bedarf. Das ist ein wichtiger Baustein zur Umsetzung von schnell geladenen und performanten Anwendungen; es müssen nicht alle Features auf Vorrat geladen und initialisiert werden. Weitere Optimierungen wie Lazy Preloading können ebenfalls eingesetzt werden, sodass der initiale Anwendungsstart sehr performant erfolgt und später benötigte Features verdeckt im Hintergrund geladen werden, um sofort zur Verfügung zu stehen, wenn sie vom Nutzer aufgerufen werden.

Soll nun durch WebAssembly eine Funktionalität für die Angular-Anwendung bereitgestellt werden, bietet sich eben der Einsatz eines Service an. Damit steht das Feature per Dependency Injection in der gesamten Anwendung zur Verfügung. Gleichzeitig ist dadurch sichergestellt, dass alle Anwendungskomponenten auf denselben Daten und mit derselben Instanz des Service arbeiten. Damit kann z. B. dieselbe Instanz einer WebAssembly-Funktionalität von unterschiedlichen Komponenten bei der Anzeige genutzt werden. Ein Beispiel dafür könnte die Anzeige einer Simulation in Form einer Übersicht mit niedriger Auflösung und eine Ausschnittansicht der Simulation mit hoher Auflösung sein.

Zum Einbinden von Wasm in eine Webanwendung ist immer auch ein gewisser Anteil an Glue-Code notwendig. Er übernimmt die Kommunikation mit dem Wasm-Code, indem beispielsweise Zeichenketten (de-)kodiert werden oder bei der Instanziierung über Import- und Exportfunktionen die Integration in die umgebende Anwendung konfiguriert wird. Bei einer Angular-Anwendung liefert der Glue-Code dann auch die Angular-spezifische Abstraktion als Modul bzw. Service. Dieser Glue-Code ist typischerweise ein wenig unterschiedlich, je nach Funktionalität, die der Wasm-Code zur Verfügung stellt, und je nach verwendetem Build-Tool bzw. der Sprache, mit der der Wasm-Code erzeugt wurde.

Gerade, wenn mehrere Anwendungen mit Wasm-Funktionalität ausgestattet werden müssen, bietet es sich in der Regel an, den Glue-Code in eine Library auszulagern, die dann von den Projekten eingebunden werden kann. Bei firmeninternen Anwendungslandschaften ist es entsprechend sinnvoll, diese Library dann z. B. in einem firmeninternen Artifact bzw. npm-Repository unterzubringen, um sie firmenweit wiederverwenden zu können.

Beispiel mit AssemblyScript

Da WebAssembly aus verschiedenen Sprachen generiert werden kann, bietet es sich an, die Sprache zu nutzen, die zum jeweils eigenen Anwendungsfall passt. Zum Starten kann beispielsweise AssemblyScript genutzt werden [1]. Dabei handelt es sich um eine mit TypeScript verwandte Sprache, deshalb wird AssemblyScript Webentwicklern vertraut vorkommen. Auch wenn es sich bei AssemblyScript um eine eigenständige Sprache handelt, kann man sie sich auch als Untermenge von TypeScript vorstellen.

Die Installation ist einfach und setzt lediglich eine Installation von Node und npm voraus. Dann kann ein neuer Projektordner angelegt werden, in dem die folgenden Befehle ausgeführt werden müssen:

npm init
npm install --save-dev assemblyscript
npx asinit .

Dadurch wird ein Projekt mit einer Struktur entsprechend Abbildung 1 erzeugt. Der assembly-Ordner enthält den Quellcode, während der build-Ordner die kompilierten Wasm-Skripte enthält. Zum Kompilieren nach Wasm muss dann nur noch der Build mit npm run asbuild gestartet werden.

Abb. 1: Generierte Projektstruktur eines AssemblyScript-ProjektsAbb. 1: Generierte Projektstruktur eines AssemblyScript-Projekts

Hier ein Codebeispiel in AssemblyScript, das eine Addition implementiert:

export function add(a: i32, b: i32): i32 {
  return a + b;
}

Beim Kompilieren werden aus diesem Code nun mehrere Dateien erzeugt. Ein solches Build-Resultat zeigt Abbildung 2. Für uns ist hier besonders die Datei release.wasm von Interesse. In diese Datei hinein wurde die AssemblyScript-Funktion im Wasm-Format kompiliert. Um die Datei einfach aus unserem Angular-Projekt heraus aufrufen zu können, können wir sie etwa in den assets-Ordner unserer Angular-App kopieren. Eine spätere entsprechende Automatisierung der Build-Schritte ist hier offensichtlich angeraten.

Abb. 2: Dateien der Build-Ausgabe des AssemblyScript-ProjektsAbb. 2: Dateien der Build-Ausgabe des AssemblyScript-Projekts

Gemeinsam sind wir stark

Um die Wasm-Logik aus Angular heraus anzubinden, bietet es sich an, einen Service für den Glue-Code zu schreiben. Das hat den Vorteil, dass die Wasm-Instanz nicht direkt an den Lebenszyklus einer Angular-Komponente gebunden wird. Ein solcher Wasm-Service ist zum Beispiel in Listing 1 dargestellt. Dort wird die add-Funktion aus dem AssemblyScript als privates Property wasmAdd gehalten. Bevor wir die add-Funktion jedoch nutzen können, muss der Service zunächst initialisiert werden. Das geschieht in der init()-Funktion, die aufgrund des einfachen Beispiels hier direkt im Konstruktor aufgerufen wird. Weitere Optimierungen werden weiter unten besprochen. In der init()-Funktion wird zuerst die Wasm-Datei per fetch-Request geladen. Es könnte prinzipiell auch der Angular HttpService genutzt werden. Mit dem fetch-Ansatz kann die Wasm-Instanz allerdings etwas einfacher erzeugt werden, da vom WebAssembly-Browser-API direkt die Funktion WebAssembly.instantiateStreaming() zur Verfügung gestellt wird, die direkt die fetch-Response zum Erzeugen der Wasm-Instanz nutzt. Mit dem Angular HttpClient geladene Wasm-Dateien müssen manuell in einen Buffer verwandelt und dann zum Instanziieren an WebAssembly übergeben werden. Da wir im Beispiel jedoch das fetch-API verwenden, können wir alternativ auch die fetch-Response direkt an WebAssembly weiterreichen. Aus dem so erhaltenen WebAssembly-Modul können wir nun über die WebAssembly-Instanz und aus deren Exports die add()-Funktion referenzieren und an this.wasmAdd binden. Da Wasm jedoch in der Regel keine konkreten Typings mitliefert, hat TypeScript zunächst keine spezielleren Typinformationen über die Wasm-Exports. Daher setzen wir an dieser Stelle eine Type Assertion ein, um den exakten Typ der Funktion zu korrigieren. Danach kann einfach die add()-Funktion des Service aufgerufen werden, die dann direkt die Wasm-Funktion aufruft.

Listing 1: Angular-Service zum Aufruf von Wasm-Funktionen

@Injectable({providedIn: 'root'})
export class WasmService {
  private wasmAdd?: (valA: number, valB: number) => number;
 
  constructor() { this.init(); }
 
  async init(): Promise<void> {
    const wasmBlob = fetch('assets/release.wasm');
    const module = await WebAssembly.instantiateStreaming(wasmBlob);
    const exports = module.instance.exports;
    this.wasmAdd = exports['add'] as (valA: number, valB: number) => number;
  }
  
  add(valueA: number, valueB: number): number {
    if (!this.wasmAdd) {
      throw new Error('Wasm Add not initialized');
    }
    return this.wasmAdd(valueA, valueB);
  }
}

Der Service kann dann wie ein gewöhnlicher Angular-Service verwendet werden. In Listing 2 ist exemplarisch eine Angular-Komponente gezeigt, die ein Formular enthält. In das Formular können in zwei unterschiedliche Inputfelder jeweils Zahlen eingetragen werden. Die Inputfelder sind mit einer Angular-Reactive-formGroup verbunden. Jedes Mal, wenn sich – z. B. durch eine Nutzereingabe – der Wert in einem der Felder ändert, wird ein valueChange gefeuert. Die momentanen Input-Werte werden an den WasmService weitergeleitet, der die Berechnung in Wasm anstößt. Das Resultat dieser Berechnung landet schließlich in der Variablen result, die im Template ausgegeben wird. Im Beispiel aus Listing 2 wird dafür die async-Pipe verwendet, da es sich bei result um ein asynchrones Observable handelt, das immer genau dann aktualisiert wird, wenn sich einer der beiden Formularwerte ändert.

Listing 2: Angular-Komponente, die Wasm-Funktionalität verwendet

@Component({
  selector: 'app-add',
  template: '
    <div [formGroup]="calcForm">
      <div>
        <label>Wert A</label>
        <input type="number" formControlName="valueA">
      </div>
      <div>
        <label>Wert B</label>
        <input type="number" formControlName="valueB">
      </div>
    </div>
    <p>Ergebnis: {{result|async}}</p>
  '
})
export class AddComponent {
  readonly calcForm = new FormGroup({
    valueA: new FormControl(0, {nonNullable: true}),
    valueB: new FormControl(0, {nonNullable: true})
  });
  result = this.calcForm.valueChanges
    .pipe(
      map(() => {
        const values = this.calcForm.getRawValue();
        return this.wasm.add(values.valueA, values.valueB);
    })
  );
 
  constructor(private readonly wasm: WasmService) {}
}

Optimierungen beim Laden

Es kann durchaus vorkommen, dass Wasm-Dateien im Vergleich zu JavaScript sehr groß werden, z. B., wenn komplexe Simulationen oder große Datenmodelle per HTTP geladen werden müssen. In einem solchen Fall muss der Nutzer natürlich auf eventuelle Wartezeiten aufmerksam gemacht werden. Typischerweise wird dazu ein Ladeindikator (z. B. Spinner) angezeigt. Um diesen Indikator zu steuern, kann ausgenutzt werden, dass das WebAssembly API mit Promises arbeitet.

In Listing 3 ist ein WasmService zu sehen, der eine generische init()-Funktion hat, die die Exports der Wasm-Instanz in Form eines Promise zurückliefert. Diese Funktion wird von der Komponente in Listing 4 aufgerufen, um die Berechnung einer Fibonacci-Folge per Wasm durchführen zu lassen (der Code zur Berechnung einer Fibonacci-Folge ist zwar weder sehr komplex noch sehr groß, soll hier aber auch nur als Platzhalter für eine umfangreichere Logik fungieren).

Da die Funktion in ein Promise gewrappt ist, kann die Angular-Async-Pipe verwendet werden, um im Template so lange den Ladebildschirm anzuzeigen, bis die Wasm-Datei geladen und die Wasm-Instanz erzeugt wurde.

Listing 3: Angular-Service, um generische Wasm-Funktion zu laden

@Injectable({providedIn: 'root'})
export class WasmService {
 
  init(filename: string): Promise<WebAssembly.Exports> {
    const request = fetch('assets/' + filename)
    return WebAssembly.instantiateStreaming(request)
      .then(module => module.instance.exports);
  }
}

Listing 4: Laden einer Wasm-Funktionalität aus einer Komponente

@Component({
  selector: 'app-fib,
  template: '
    <div *ngIf="wasmFib|async; else loading" class="card-container">
      <button (click)="startWasmFib(25)">Start</button>
      <div>Result: {{result}}</div>
    </div>
    <ng-template #loading><div>Loading...</div></ng-template>
  '
})
export class FibonacciComponent {
  result = 0;
  readonly wasmFib: Promise<(num: number) => number>;
 
  constructor(private readonly wasmUtil: WasmService) {
    this.wasmFib = this.wasmUtil.init('fibonacci.wasm')
      .then(exports => exports['fibonacci'] as (num: number) => number);
  }
 
  startWasmFib(ordnung: number): void {
    this.wasmFib.then(fibonacci => {
      result = fibonacci(ordnung);
    });
  }
}

Angular-Modulsystem als Architekturpfeiler

Das Angular-Modulsystem bietet die Möglichkeit, eine Anwendung in einzelne und isolierte Fachlichkeiten zu zerlegen bzw. von Anfang an so zu entwickeln. Module können separat geladen werden. Das kann zum einen genutzt werden, um die subjektive Ladegeschwindigkeit der Anwendung zu optimieren. Auf der anderen Seite bieten sich Module an, um Funktionalität, die z. B. zur Wiederverwendung ausgelagert werden soll, zu einem gemeinsamen Bündel zu aggregieren. Damit kann ein Modul als Fassade dienen, die Komplexität der Implementierung verbergen und eine reduzierte bzw. vereinfachte API-Oberfläche anbieten.

Ein Modul kann dabei wiederum andere Module referenzieren und sowohl Services als auch Komponenten oder Direktiven beinhalten. Ein Beispiel für ein mögliches Angular-Modul wäre eine Funktionalität wie Google Maps, die in verschiedenen Bereichen einer Anwendung oder sogar verschiedenen Anwendungen eingesetzt werden kann. Jedes Modul verfügt selbst über einen Konstruktor, mit dem Initialisierungen koordiniert werden können, falls erforderlich.

Module können als Bibliotheken über ein Artefakt-Repository verteilt werden. Damit sind die gemeinsame Versionierung und der Test der Bestandteile möglich. Entsprechend reduziert sich die Menge der zu pflegenden Versionen und auch der zu testenden Permutationen. Im Fall einer Wasm-basierten Funktionalität können Module genutzt werden, um als Abstraktionsebene zugrunde liegende Komponenten und Services zusammenzufassen und versioniert bereitzustellen. Wird innerhalb eines Unternehmens mit Angular gearbeitet, können damit Teams auf entsprechend hochwertige Artefakte zurückgreifen und müssen nicht redundant für ihren jeweiligen Kontext die Einbettung in Angular vornehmen.

Auch wenn in jüngster Zeit der Trend dahin geht, das Modulsystem nicht mehr so sehr in den Fokus zu rücken, so bietet es sich zum Beispiel an, wenn in sich geschlossene Features zur Verfügung gestellt werden sollen, die neben Services auch aus mehreren Komponenten bestehen. Das kann natürlich auch im Kontext von Wasm Sinn ergeben. Alternativ gibt es bei einfacheren Anwendungsfällen, in denen zum Beispiel nur der Glue-Code in die Library ausgelagert werden soll, die Möglichkeit, auch nur einen Service oder globale Funktionen in eine Library auszulagern. Ein Entscheidungskriterium könnte sein, ob der Fokus eher darauf liegt, generischen Wasm-Code für Angular bereitzustellen, oder ob es sich um eine umfangreichere Teilfunktionalität handelt, die neben Wasm bereits mehrere Komponenten zur Darstellung und Interaktion mitbringt. Ein weiteres Entscheidungskriterium ist natürlich, ob das Angular-Modulsystem auch sonst verwendet wird oder nicht.

Eine neue Library mit dem Namen wasm-lib kann innerhalb eines bestehenden Angular-Projekts durch Eingabe des Befehls ng generate lib wasm-lib erzeugt werden. In das Library-Projekt können dann die Funktionen, Services oder auch Komponenten, die zu dem auszulagernden Feature gehören, verschoben werden. Das Projekt kann dann per ng build wasm-lib gebaut und mit Hilfe von npm in eine Registry gepublisht werden. In anderen Projekten wird die Library ganz normal als versionierte Abhängigkeit über die package.json ausgedrückt.

Aufgepasst, es gibt Changes

Da Wasm mit nahezu nativer Performance läuft, eignet es sich unter anderem für rechenintensive Aufgaben. Der Aufruf der Wasm-Funktionen, inklusive Datentransfer von und zu Wasm, erfolgt synchron. Daher kann es passieren, dass der Wasm-Aufruf bei sehr rechenintensiven Tasks den Browser längere Zeit blockiert. Das ist natürlich schlecht für die Performance und insbesondere schlecht für die Nutzbarkeit der Seite, die das betreffende Wasm-Skript einbindet. Auch diese Aspekte sollten im Entwicklungsprozess der jeweiligen Anwendung immer bedacht werden.

Eine Möglichkeit, mit einem solchen sehr rechenintensiven Skript umzugehen, sind Web Worker. Mit ihnen ist es möglich, einen weiteren Thread im Browser zu starten, um etwa solche rechenintensiven Tasks auszuführen. Tatsächlich ist es auch möglich, WebAssembly aus Web Workern heraus anzustoßen.

Bleibt noch die Frage nach der Integration von Web Workern mit Angular. Auch hier wird Entwicklern bereits alles Notwendige zur Verfügung gestellt, sodass man es direkt anwenden kann. Angular erlaubt es sogar, neue Web Worker per Schematic anzulegen. Dazu muss einfach nur der Befehl ng generate web-worker wasm-demo innerhalb eines Angular-Projekts eingegeben werden. In diesem Fall wird also ein Web Worker Wasm-Demo angelegt. Den Web Worker können wir dann zum Beispiel aus unserer Fibonacci-Komponente heraus anbinden, wie es Listing 5 zeigt. Dafür muss der Worker als new Worker() instanziiert werden, und es muss ein relativer URL zur Web-Worker-TypeScript-Datei angegeben werden, damit auch der Worker von TypeScript kompiliert werden kann. Um die Daten, die im Web Worker durch Wasm errechnet werden, in der Komponente nutzen zu können, muss ein Listener am message Event des Workers registriert werden, da ein Web Worker nur über diesen Mechanismus mit dem Haupt-Thread kommunizieren kann. Wir können dem Web Worker dann per postMessage Daten (oder sogar ein sogenanntes Offscreen Canvas) hineinreichen, mit denen innerhalb des Web Worker die Wasm-Funktion aufgerufen wird. So kann ein Web Worker genutzt werden, um Wasm-Berechnungen effektiv parallel zum Brower-UI-Thread auszuführen.

Listing 5: Komponente, die den Web Worker aufruft

@Component({})
export class FibonacciComponent implements OnDestroy {
  results = '';
  private wasmWorker = new Worker(
    new URL('./wasm-demo.worker', import.meta.url), 
    {type: 'module'}
  );
 
  constructor() {
    this.wasmWorker.addEventListener('message', event => {
      this.results = event.data
    });
  }
 
  startWebWorkerWasm(ordnung: number): void {
    this.wasmWorker.postMessage(ordnung);
  }
 
  ngOnDestroy(): void {
    this.wasmWorker.terminate();
  }
}

Der Web Worker selbst könnte zum Beispiel aussehen, wie in Listing 6 dargestellt. Zunächst wird auch in diesem per fetch die Wasm-Datei geladen, die dann zur Instanziierung der WebAssembly-Umgebung genutzt wird. Das Promise, das von der instantiateStreaming()-Funktion zurückgeliefert wird, wird hier mit der then()-Syntax aufgelöst, generell kann aber je nach Vorliebe auch die async-await-Syntax genutzt werden.

Der wichtigste Bestandteil dieses Web Worker ist die addEventListener()-Funktion, die man nutzen kann, um auf Message-Events von außen zu horchen. Sobald ein solches Event kommt, werden daraus die Daten extrahiert (data) und an die Wasm-Fibonacci-Funktion weitergeleitet. Das Ergebnis wird dann wiederum per postMessage() an die Komponente im Haupt-Thread zurückgereicht.

Listing 6: Web Worker, der Wasm-Funktion anbindet

/// <reference lib="webworker" />
 
const wasmReq = fetch('assets/release.wasm');
const wasmFib = WebAssembly.instantiateStreaming(wasmReq)
  .then(wasmFibonacci => wasmFibonacci.instance.exports)
  .then(exports => exports['fibonacci'] as (num: number) => number);
 
addEventListener('message', ({data}) => {
  wasmFib
    .then(fibonacci => fibonacci(data))
    .then(result => postMessage(result));
});

Anspruchsvolleres Beispiel: Videohintergrund

Um das Potenzial von Wasm und gerade von SIMD-Operationen zu verdeutlichen, werfen wir zum Abschluss einen Blick auf ein anspruchsvolleres Beispiel. Es ist vollständig unter [2] auf GitHub zu finden. Es handelt sich um eine Angular-Anwendung, die per getUserMedia() einen Livestream der Webcam erhält. Dabei kann durch einen Schalter, der als Angular-Komponente umgesetzt ist, das Feature Hintergrund weichzeichnen aktiviert werden. Das Feature verwendet Tensorflow-Lite [3], das als Wasm-Kompilat im Projekt bereitsteht. Dabei wird auch von SIMD Gebrauch gemacht, um die Operationen maximal effizient auszuführen. Die Verarbeitung könnte als Optimierung durch einen Web Worker erfolgen, der zum einen die Wasm-Funktionen aufruft und zum anderen direkt in das zugehörige Canvas zeichnet. Damit würde die zusätzliche Kommunikation mit dem Haupt-Thread der Anwendung vermieden, was dann noch einmal zu mehr Effizienz beitragen würde. Falls im jeweiligen Browser SIMD-Funktionalität nicht zur Verfügung steht, wird eine Tensorflow-Lite-Variante genutzt, die ohne SIMD-Operationen gebaut ist. Dazu bietet Wasm die Abfrage der zur Verfügung stehenden Features an und es kann bei der Initialisierung entsprechend darauf reagiert werden.

In Abbildung 3 ist ein Screenshot der Anwendung mit aktiviertem Weichzeichnen des Hintergrunds zu sehen. Durch die Hintergrundverarbeitung und die inzwischen selbst im mobilen Umfeld standardmäßig anzutreffenden Multi-Core-CPUs beeinträchtigt diese rechenintensive Funktionalität die Hauptanwendung in keiner Weise.

Abb. 3: Webcamvideo mit weichgezeichnetem HintergrundAbb. 3: Webcamvideo mit weichgezeichnetem Hintergrund

Fazit

Wasm wird von allen aktuellen Browsern unterstützt. Die meisten Browser stellen dabei die SIMD-Operationen der zugrunde liegenden Hardwareplattform zur Verfügung. Damit lassen sich deutlich performantere und komplexere Umsetzungen realisieren, als das allein mit JavaScript oder TypeScript möglich wäre. Allein die Wiederverwendung bestehender Bibliotheken, wie hier im Beispiel Tensorflow-Lite, zeigt, welches Potenzial sich durch Wasm für Webanwendungen ergibt. Angular liefert dabei nützliche Konzepte, die den strukturierten Einsatz von Wasm ermöglichen bzw. vereinfachen. Mit Angular sind komplexe Anwendungen mit einer gut wartbaren Architektur und guter Testbarkeit effektiv umsetzbar. Das wirkt sich positiv auf die Langlebigkeit aus und hilft so, Investments zu sichern. Wasm liefert dazu einen wichtigen Beitrag, da bestehender Programmcode für das Web verfügbar gemacht werden kann.

Wasm eignet sich jedoch nicht für alle denkbaren Anwendungsfälle. So ist zum heutigen Stand der Zugriff auf das Browser-DOM oder die direkte Netzwerkkommunikation nicht möglich. Da diese aber durch den Browser – und auch von Angular – in optimierter Weise bereitgestellt werden, ergeben sich keine gravierenden Einschränkungen.

Bleib informiert

Brancheninsights im Newsletter erhalten:

[mc4wp-simple-turnstile]

 

🔍 Frequently Asked Questions (FAQ)

1. Was ist WebAssembly und warum für Angular nützlich?
WebAssembly (Wasm) erlaubt es, Code aus Sprachen wie C oder AssemblyScript im Browser schnell auszuführen. In Angular kann man so rechenintensive Aufgaben auslagern, zum Beispiel Bildbearbeitung oder komplexe Berechnungen, ohne dass die App langsamer wird.

2. Wer profitiert von WebAssembly in Angular?
Entwickler, die viel Performance brauchen oder bestehende native Bibliotheken nutzen möchten, profitieren besonders. Auch kleine Projekte können davon profitieren, wenn rechenintensive Logik in Wasm ausgelagert wird.

3. Wie bindet man WebAssembly in Angular ein?
Die Wasm-Datei wird meist ins assets-Verzeichnis gelegt und per fetch oder instantiateStreaming geladen. Ein Angular-Service übernimmt die Initialisierung und den Aufruf der Funktionen. Für rechenintensive Aufgaben kann ein Web Worker helfen, die UI flüssig zu halten.

4. Welche Einsatzbereiche eignen sich für WebAssembly?
Besonders sinnvoll ist Wasm bei Bild- oder Videobearbeitung, Machine Learning oder Simulationen, die viel Rechenleistung benötigen. Es sorgt dafür, dass diese Aufgaben schneller laufen als in reinem JavaScript.

5. Welche Einschränkungen gibt es?
Wasm-Dateien können groß sein, was Ladezeiten erhöht. Außerdem kann Wasm nicht direkt auf DOM oder Browser-APIs zugreifen, und der Datenaustausch zwischen JavaScript und Wasm benötigt manchmal zusätzlichen Aufwand.

6. Welche Sprachen und Tools werden genutzt?
Häufig werden AssemblyScript, C oder C++ verwendet. Angular-Services verwalten die Integration, während Web Worker und Promises die Performance und Asynchronität verbessern.

7. Wie bleibt die Angular-Architektur sauber?
Glue-Code sollte in Services ausgelagert werden, Komponenten sollten nicht direkt auf Wasm zugreifen. Lazy Loading und modulare Struktur helfen, die App wartbar und wiederverwendbar zu halten.

 

Links & Literatur

The post WebAssembly – auch für Angular-Anwendungen appeared first on JavaScript & Angular Days.

]]>
Manfred Steyer im Interview https://javascript-days.de/blog/manfred-steyer-im-interview/ Wed, 28 Jun 2023 12:21:50 +0000 https://javascript-days.de/?p=15991 Es war eine Freude Manfred Steyer – programmierender Architekt, Google Developer Expert (GDE) und Trusted Collaborator – auf den JavaScript Days 2023 begrüßen zu dürfen. Im Interview beantwortet er Fragen rund um Microfrontends und Neuerungen in Angular und gibt Einblicke in seine Workshops zu wiederverwendbaren Komponenten.

The post Manfred Steyer im Interview appeared first on JavaScript & Angular Days.

]]>

Herzlich willkommen. Wir sind hier live von dem 4-in-1-Trainingsevent zu JavaScript, Angular, React, HTML und CSS. Und neben mir sitzt Manfred, unser Trainer heute. Hi Manfred. Schön, dass du da bist und uns ein paar Fragen beantwortest, die wir uns ausgedacht haben zu deinem Workshop aber auch so zum Thema Angular. Manfred, du bist ja Trainer, Berater und programmierender Architekt; alles mit Fokus auf Angular. Natürlich Google Developer Expert, Trusted Collaborator im Angular-Team und du sprichst auf vielen Konferenzen und Seminaren. Du gibst zum Beispiel auch das Angular Camp unter anderem, das man unter unserem Entwickler Akademie Pseudonym kennt und bringst vielen Leuten Angular bei. Gibt es denn etwas, was du noch lernen möchtest?

Ich sage immer ich bin ein wenig wie ein streunender Hund. Ich habe da keine wirklichen Pläne, keinen Masterplan für die nächsten Jahre. Sondern ich schnappe einfach immer das, was mich gerade interessiert. Das ist auch gewissermaßen eine Luxussituation, weil das funktioniert eigentlich jetzt schon so seit ich in der Arbeitswelt bin, dass ich mich halt in Sachen reinstürze die mich gerade begeistern. Und weil ich eben dann entsprechend begeistert bin, kann ich dann das Wissen auch hoffentlich gut weitergeben.

Du bist unser Angular-Experte. Welche Herausforderungen stellen sich denn Experten bei der Erstellung von wiederverwendbaren Komponenten?

Also es ist irgendwie lustig, weil man glaubt vielleicht Angular zu kennen. Aber gerade dann, wenn man wiederverwendbare Komponenten schreibt, eine Komponenten library umsetzt, ein Designsystem implementiert, dann kommt man mit ganz neuen Aspekten von Angular in Kontakt die man so vielleicht noch gar nicht so richtig gesehen hat. So Sachen, wie Händels oder strukturelle Direktiven oder wie Templates und View Container haben wir gerade besprochen. Das sind Sachen die brauche ich typischerweise nicht oder maximal aus der Blackbox-Sicht heraus, wenn ich bestehende Komponenten zu Features zusammenfüge. Aber wenn ich die Komponenten selber schreibe, einen Date Ambika oder wie gesagt mein Design System, muss ich auch in diese Untiefen vordringen. Und da muss man sich einmal hineindenken, denn das ist nicht immer unbedingt ganz simpel. Wenn man mal drinnen ist, dann macht es natürlich Sinn, aber man muss sich wirklich reindenken.

Erlebe Angular Trends live

16. - 20. März 2026

>

Du stellst in deinem Workshop auch weiterführende Konzepte für wiederverwendbare Komponenten vor, wie zum Beispiel Komponenten Bibliotheken. Kannst du uns noch ein anderes Beispiel nennen?

Also die Bibliotheken sind eine Möglichkeit, um Komponenten zu kapseln und wiederverwendbar zu machen. Ich kombiniere das dann auch gerne mit Werkzeugen die zum Beispiel für die Library den Changelog automatisch generieren, weil so einen Changelog zu schreiben, was hat sich geändert von Version X auf Y ist ja immer ziemlich fad und das lässt sich tatsächlich wunderschön automatisieren. Zum Beispiel indem man ein LinkedIn für die Git Commit Messages hat und dann kann man aus dem LinkedIn und anderen einen schönen Changelog, wie man ihn auch von Angular direkt kennt, ableiten. Wenn es direkt um die Komponenten geht da geht’s um so Themen wie Direktiven – Attribut Direktiven, strukturelle Direktiven, Templates, View-Container, ViewChildren, die Kommunikation zwischen Komponenten.

Spannend. Und in deinem zweiten Workshop ging es um große Angular Anwendungen und Struktur in diese zu bringen. Welche Themen hast du dort behandelt?

Also das Problem ist bei großen Anwendungen, die von vielen Entwicklern aktiv entwickelt oder gewartet werden, wo ich vielleicht sogar die Situation habe, dass ich die über Jahre hinweg weiter warten muss und wo vielleicht sogar mehrere Teams arbeiten, da muss ich irgendwie sicherstellen, dass ich links was ändern kann, ohne rechts was kaputt zu machen. Und um das Ziel zu erreichen haben wir uns unter anderem mit Domain Driven Design beschäftigt. Das bietet mal Lösungen auf der logischen Ebene. Die Lösungen haben wir übertragen auf ein NX Monorepo, also ein großes Quellcode Repository, dass aus kleinen Fragmenten besteht die gemeinsam eben das gesamte System ergeben. Das heißt mit dem Monorepo kann ich eine große Lösung in kleine Häppchen untergliedern, kann dann sogar bei NX festlegen welches Häppchen auf welches andere Häppchen Zugriff hat. Somit vermeidet man, dass jeder mit jedem kommuniziert und dass ich links was ändere und damit rechts etwas kaputt mache, obwohl das gar nicht beabsichtigt ist. Dieses „Verschlimmbessern“, wie manche Leute auch sagen. Basierend auf dem haben wir uns dann mit Micro-Frontends beschäftigt. Micro-Frontends sind die eigentlich ja nichts anderes wie die Idee von Domain Driven Design auf die nächste Ebene gebracht. Plötzlich hat man pro Untergliederung eine eigene Anwendung. Somit können verschiedene Teams möglichst autonom arbeiten. Jedes Team hat seine Anwendung oder Anwendungen und kann autonomen dran arbeiten. Wenn sie fertig sind, dann werden die deployed. Also die Teams müssen sich weniger untereinander abstimmen, was immer dann super ist, wenn man merkt bei meinen vielen Teams wird der Abstimmungsaufwand einfach zu groß. Somit bekomme ich dann die Agilität von kleinen Teams zurück, obwohl ich große Lösungen schreibe.

Klingt gut, nach einem sehr spannendem Thema. Du hast gerade schon viel erklärt, wann Micro Frontends Sinn machen. Wann machen Sie denn nicht Sinn?

Also ich würde sagen nicht Sinn machen sie, wenn ich nur ein Team habe oder wenn ich vielleicht eine kleine Anzahl an Teams habe, die eigentlich auch über ein Monorepo zusammenarbeiten könnten. Ist das nicht der Fall, habe ich mehrere Teams und vertragen sich die nicht in einem einzigen Monorepo, weil die einen anderen Hintergrund haben, weil die Experten für unterschiedliche Domänen sind, weil die vielleicht ganz woanders sitzen, dann sind Micro Frontends sehr charmant. Dann kann das Team eben autark arbeiten, sogar eigene Entscheidungen treffen – Architekturentscheidungen aber auch Technologieentscheidungen. Das heißt schlussendlich könnte ich sogar in einer Micro-Frontend Architektur mehrere Frameworks haben. Mache ich nicht aus Jux und Tollerei. Aber es macht langfristig Sinn weil wir alle wissen, Technologien kommen und gehen; Nach ich würde mal sagen sieben Jahren muss sich eine Technologie entweder drastisch neu erfinden oder sie ist weg vom Fenstern und aus dem Grund tut schon gut wenn ich nach sieben Jahren mal das nächste Modul, die nächste Domäne mit einer aktuelleren Technologie programmieren kann. Somit kann man vom Text wegmigrieren zu einer Moderneren hin. Also Sinn macht es vor allem dann, um auf die Frage noch einmal zurückzukommen, wenn ich mehrere Teams habe, und als Nebeneffekt bekommt man, dass man peu à peu den Technologiestack migrieren kann.

Bleib informiert

Brancheninsights im Newsletter erhalten:

[mc4wp-simple-turnstile]

 

Ok. Und dann habe ich jetzt noch eine letzte Frage an dich. Ich hatte vorhin schon erwähnt, du bist Trusted Collaborator im Angular-Team. Du hast also exklusive Einblicke. Kannst du uns sagen, ob es dann irgendwelche Neuerungen gibt, die demnächst auf uns zukommen in der Angular Welt?

Also was man derzeit auch sieht – da braucht man gar nicht exklusive Einblicke – das Angular Team arbeitet an zwei großen Themen derzeit. Das eine Thema ist Signale und das andere Thema ist Hydration. Bei Signalen/Signals geht es um einen neuen reaktiven Building Blog – so ähnlich wie ein RX-Chairs, aber viel einfacher – und weil das eine reaktive Bildung ist kann mir dieser Building Blog Bescheid geben wenn sich gebundene Daten ändern. Also ich habe vielleicht eine Adresse, die stelle ich dar im Browser und plötzlich ändert sich die Adresse, weil ich umgezogen bin und jetzt könnte das Signal Angular sagen: Pass auf, da hat sich was geändert und deswegen weiß Angular, es muss jetzt genau den Teil der Adresse aktualisieren. Also ich kann die Aktualisierung im Browser viel zielgerichteter machen. In der Vergangenheit sind immer ganze Komponenten aktualisiert worden und in der Vergangenheit war auch RX-Chairs nicht für jeden unbedingt immer simpel. Mit Signalen ändert sich beides. Es ist reaktiv, aber simpel und sehr zielgerichtet und nebenbei bekomme ich damit auch Altlasten Weg wie zum Beispiel Zone Chairs. Das ist von Anfang an bei Angular dabei und irgendwie so an der Grenze zwischen genial und ein wenig speziell und das kriegt man damit auch raus. Also Signale ist das eine. Das andere ist Hydration. Hydration ist eher interessant, wenn ich öffentliche Website schreibe, wo es um die Start Performance geht. Bei öffentlichen Websites muss relativ schnell was zum Schauen da sein, ansonsten springen die Benutzer ab. Da gibt es sehr schöne Statistiken wie sich der Umsatz nach unten entwickelt, wenn sich die Ladezeiten nach oben entwickeln – gerade bei anonymen Benutzern. Mit Hydration schafft man es, dass man zuerst serverseitig vorrendert. Das heißt ich liefere eine Seite aus und sehe sofort was – sehe sofort den Inhalt. Die Seite ist allerdings noch nicht interaktiv, weil sie erstmal statisch ist. Aus dem Grund lädt dann Angular peu à peu einzelne Komponenten nach damit die zum Leben erweckt werden. Das klappt prinzipiell schon länger. Das Stichwort war das Server-Side Rendering. Das Problem beim klassischen Server-Side Rendering war, ich habe auf einmal den gesamten Programmcode heruntergeladen. Das heißt ich habe ganz lange warten müssen, bis meine Seite, die ich schon gesehen habe, wirklich interaktiv war. Und das ändert sich jetzt mit Hydration, mit Progressive und Partial Hydration. Progressive Hydration bedeutet, das Ding überlegt sich welche Teile in welcher Reihenfolge heruntergeladen und mit Leben erfüllt werden. Vielleicht gibt es ein paar die ganz wichtig sind – der „Kaufen“ Button – und vielleicht gibt es ein paar Teile, die zwar auch wichtig sind, aber dennoch nachrangig sind. Bei einem Shop ist das vielleicht der Produktvorschlag – was könnte ich mir sonst noch kaufen. Da kann es dann eine Priorisierung geben und somit habe ich das Beste aus beiden Welten. Ich sehe relativ schnell was und die wichtigen Teile werden auch sehr schnell interaktiv, weil die zuerst geladen werden und nicht alles gemeinsam geladen wird. Das wäre progressive Hydration. Also nach und nach die Teile mit Leben erfüllen. Vielleicht gibt es dann irgendwann basierend darauf auch sowas wie Partial Hydration, wo sich ein Mechanismus überlegt was ich eigentlich gar nicht benötige. Wenn ich statischen Text habe den muss ich nicht zum Leben erwecken und Teile, die ich nie in den sichtbaren Bereich scrolle, muss ich auch nicht zum Leben erwecken. Die könnte man weglassen oder man könnte das rauszögern bis eben gescrollt wird. Und da arbeitet das Angular Team eben gerade fleißig dran und auch diesen Use Case abzudecken. Übrigens beide sind Use Cases, die derzeit auch von anderen Frameworks verfolgt werden. Da sieht man Angular ist bemüht am Puls der Zeit zu bleiben und diese ganzen Trends auch aufzunehmen.

Danke für das Interview und für die Beantwortung. Du bist bestimmt nächstes Jahr auch wieder mit dabei bzw. schon dieses Jahr im Oktober in Berlin. Also gerne vorbeikommen Manfred ist auch da.

The post Manfred Steyer im Interview appeared first on JavaScript & Angular Days.

]]>
Fabian Gosebrink im Interview https://javascript-days.de/blog/fabian-gosebrink-im-interview/ Tue, 20 Jun 2023 09:27:02 +0000 https://javascript-days.de/?p=15876 Es war eine Freude Fabian Gosebrink – Google Developer Expert, Softwareentwickler und Speaker – auf den JavaScript Days 2023 begrüßen zu dürfen. Im Interview beantwortet Fabian Gosebrink vor allem Fragen zu seinem Workshop auf dem 4-in-1-Trainingsevent. Er redet über (typisierte) Formulare in Angular und geht dabei auf die Herausforderungen bei der Entwicklung und die Testbarkeit ein.

The post Fabian Gosebrink im Interview appeared first on JavaScript & Angular Days.

]]>
 

Willkommen auf den JavaScript, Angular, React und HTML & CSS Days. Die finden gerade unter uns statt und wir haben uns einen Trainer geschnappt: Fabian Gosebrink. Hi Fabian. Schön, dass du da bist und uns ein paar Fragen beantwortest rund um dein Workshop Thema Angular Formulare. Bevor wir starten und du uns noch ein bisschen was dazu erzählst ganz kurz zu dir: Du hast ja auch einige Titel. Du bist Microsoft Services MVP, Google Developer Expert GTI und natürlich auch Softwareentwickler und Speaker bei ganz vielen Konferenzen nationalen und international. So bezogen auf deine Karriere gibt es irgendwas, worauf du besonders stolz bist?

Ich glaube ich finde es einfach cool oder bin stolz darauf, dass ich machen kann, was mir Spaß macht. Ich merke es im Alltag, gerade wenn man manchmal nicht mehr so viel programmiert, beim Kunden ist, viel Consulting macht und sowas. Wenn ich dann wieder zurück zum Programmieren komme. Es macht einfach Spaß. Ich finde es einfach extrem cool und schön, dass ich in einem Bereich arbeiten kann, der mir Spaß macht und ich das meinen Hauptberuf nennen darf. Das ist eigentlich cool und dass diese ganzen Titel dabei rausspringen, ist eigentlich nur weil ich in der Community aktiv bin, weil man eben ja ich glaube auch so ein bisschen Herzblut da rein legt und ist natürlich schön, wenn das rausspringt, dabei. Das ist schon mal cool, dass ich morgens aufstehen kann und ich kann, machen was ihr Spaß macht.

Erlebe Angular Trends live

16. - 20. März 2026

>

 

Sehr schön. Dann zu deinem Workshop. Wie gesagt du bist hier für Formulare. Was sind denn so typische Herausforderungen mit Formularen in Angular. Ich habe gehört eigentlich kommt fast jeder Programmierer mal mit Formularen irgendwie in Verbindung und da gibt’s bestimmt auch so ein paar Tücken, die man zu beachten hat.

Ja, es ist so bei Formularen muss man halt immer sehen, dass es das erste Mal ist, dass der User im Prinzip etwas eintippen muss. Das heißt, sie nimmt die Hand von der Maus und muss auf einmal für uns arbeiten auf unserer Webseite und das wollen wir natürlich den Benutzer so angenehm wie möglich machen. Das heißt wir müssen alles unternehmen, um gute Meldungen zu geben. Stichwort Validierung. Wir müssen gucken, dass die Forms richtig angeordnet sind, gut designt sind und so. Also da kommt viel zusammen. Validierung ist immer ein Thema. Ich meine jeder hat schon mal einen Flug gebucht oder irgendwo die Kreditkarte angegeben und das war nicht richtig oder so und man hat es verflucht, weil man die Fehlermeldung nicht richtig gesehen hat. Oder eine Telefonnummer zu validieren. Das ist immer ein Thema, immer eine Herausforderung, dass man das schön anzeigt und dass der User auch was damit anfangen kann. Und auch die Schachtelung von den Forms, die Architektur von den Forms. Also ich muss gucken, wie ordne ich das ganze an. Ich habe einen Namen, vielleicht ein, zwei, drei Adressen dazu, vielleicht gibt’s auch so Dynamic Forms, wo ich Felder hinzufügen kann. Das sind alles Herausforderungen, die ich habe, die ich auch schon bei Kunden in Projekten gesehen habe, wenn es zu Angular Forms kommt.

Ja, es gibt jetzt auch so ein Unterschied zwischen diesen herkömmlichen Formularen und typisierten Forms. Was ist denn so der Vorteil von diesen Typisierten Angular Forms?

Also ich hoffe mal, dass die Typisierten jetzt die Herkömmlichen werden in Zukunft. Also Angular hat eingeführt, dass jetzt Forms Typen haben. Vorher waren sie nicht typisiert. Das hatte den Nachteil, dass immer, wenn wir Daten bekommen haben – und wir haben die im Prinzip an unsere Forms gegeben – waren die nicht Type Safe. Wir wussten nicht genau, wenn die Daten aus der Form rauskommen, was ist das für ein Typ und wir mussten casten. Das heißt wir hatten einen Typ und mussten sagen das, was da rauskommt ist das und das und der und der Typ, könnte ein User sein oder so. Wir hatten aber keinerlei Sicherheit, um zu wissen okay habe ich jetzt eine Nummer in einen String umgewandelt oder eingegeben oder sowas. Das wussten wir nicht und jetzt mit typisierten Plattforms kriegen wir Fehlermeldungen, wenn das passiert. Das heißt, wenn ich jetzt versuche ein Alter von einem Benutzer in einen Namensfeld zu schreiben, wo der Typ nicht passt, dann kriege ich auf einmal eine Fehlermeldung. Das heißt wir sind wesentlich sicherer unterwegs, wenn wir mit Formularen arbeiten und ich kann sogar Typen allein für meinen Formular erstellen und kann dann im Prinzip mappen. Das heißt von meiner Datenbank kriege ich einen User den kann ich Mappen in meinen Form User, kann den dann behandeln und danach wieder zurück mappen und ich bin immer typisiert dabei. Und das ist eigentlich eine sehr coole Sache. Die Fehlermeldungen helfen enorm, wenn man dann einen Fehler macht, sieht man das sofort.

Du hast es schon ein bisschen gerade angerissen. Ist es auch so, dass die Formulare gut testbar sind, oder wie kann man das sicherstellen?

Also ich bin persönlich sehr großer Fan von Reactive Forms. Also es gibt zwei Ansätze. Es gibt die Template Driven und Directive Forms. Beide sind gut testbar. Die einen sagen das ist besser, die anderen sagen das ist besser. Ich persönlich bin eher so der Mensch, der auf Reactive Form steht, eben weil sie in meinen Augen sehr gut testbar sind, sehr einfach testbar sind. Ich kann in meinem Code meine Forms festlegen und habe sehr wenig mit dem Template zu tun. Wahrscheinlich mag ich es auch deswegen, weil ich style-technisch da überhaupt gar nicht so bewandert bin und dann eher froh bin, wenn ich alles in meinen TypeScript-Code habe. Das macht sie sehr gut testbar. Das ist schon mal die eine Sache mit Reactive Forms. Die andere Sache ist die Validierung. Wenn man von Testbarkeit bei Form spricht, muss man auch immer über die Validierung sprechen, weil diese Logik will man ja meistens testen. Das ist ja genau das. Kommt eine Fehlermeldung, wenn ich eine falsche Telefonnummer angebe? Kommt sie, wenn ich eine falsche IBAN-Nummer angebe oder Kreditkartennummer oder sowas? Das heißt, diese Validatoren abkapseln in eigene Klassen, gesondert behandeln als architektonische Elemente von meiner Form und dann kann ich sie auch super testen. Und wenn man das auch noch in kleine Components aufteilt; also ich sage immer, wenn man darüber nachdenkt – soll es eine Component sein oder nicht – und die dann auch noch mal wegkapselt, hat man auch wieder einen kleinen Teil, der meine Form darstellt und ich kann wirklich nur meine Form testen und kann gucken, dass das alles funktioniert. Und möglichst alle Fälle abdecken, die der Benutzer eingeben könnte, damit ich eben schön Error Meldungen anzeigen kann.

Bleib informiert

Brancheninsights im Newsletter erhalten:

[mc4wp-simple-turnstile]

 

Okay, ja cool. Dann kommen wir auch schon zur letzten Frage: Was wünscht du dir denn für die Weiterentwicklung des Angular Frameworks generell und gibt es etwas, wo du sagst hey Angular Team da müsst ihr noch mal nachbessern oder das könnte einfach besser werden?

In allererster Linie wünsche ich mir im Moment ein bisschen Ruhe bei all der Dynamik, die wir jetzt bekommen haben. Wir haben Standalone Components bekommen vor nicht allzu langer Zeit. Die Community muss das erstmal adaptieren, die müssen erstmal das jetzt einführen und damit arbeiten und das muss erstmal in meinen Augen neuer Standard werden. Das heißt, da könnte man noch mehr in der Dokumentation machen. Angular macht das aber auch gerade. Sie sehen zu, dass das eben alles danach umgestellt wird auf die Standalone Components. Dann haben wir jetzt Signals neu, die Angular angekündigt hat. Signals sind dazu da die Change Detection ein bisschen einfacher zu machen für Angular. Stichwort, wann hat sich was geändert und wann muss was geändert werden. Da wünsche ich mir auch mehr Dokumentation, Talks, Artikel darüber und Blogartikel auch vom Angular-Team, die das noch weiter erklären. Und das sind eben zwei Sachen die Angular gerade in den letzten Monaten gepusht haben. Angular sich da ein bisschen zurückgespielt, wenn man so auf Social Media guckt. So manche Entwickler sagen auf einmal Angular ist ja doch gar nicht so uncool, wie ich dachte. Das finde ich in erster Linie mal schön und wenn sich das jetzt mal so setzen würde, wenn wir jetzt mal so ein bisschen Ruhe da reinkriegen würden. Wir nehmen diese zwei Neuerungen und wir gucken jetzt mal, wie wir damit klarkommen und machen das. Ich finde es wirklich cool was da passiert aber wir müssen das erstmal adaptieren, wir brauchen Knowledge in diesen Bereichen und das muss sich dann auch beim Kunden durchsetzen. Ich meine wir sehen ja wie weit teilweise Kunden sind und die können auch nicht immer gleich aufs Neueste springen. Das dauert schon seine Zeit. Wenn wir in der Schlagzahl weitermachen, wie das in den letzten Wochen oder zwei, drei Monaten der Fall war, dann wäre es echt zu viel. Also ich wünsche mir ein bisschen Ruhe und ich freue mich extrem auf das, was da kommt – Diese Standalone Components und die Signals, wenn die adaptiert werden. Und wenn man in der nächsten Zeit da wie gesagt mehr Doku, mehr Knowledge aufbauen würden, die wir dann auch beim Kunden einsetzen würden und vor allem sehen würden, das wäre cool.

Dann gibt’s ja viele Neuerungen, wofür es sich lohnt auch mal wieder auf ein Trainings Event zu kommen. Das war’s. Danke dir für die Zeit.

The post Fabian Gosebrink im Interview appeared first on JavaScript & Angular Days.

]]>
HttpClient in Angular 15, Standalone APIs und funktionale Interceptors https://javascript-days.de/blog/httpclient-in-angular-15-standalone-apis-und-funktionale-interceptors/ Tue, 20 Dec 2022 14:16:59 +0000 https://javascript-days.de/?p=15503 Zweifelsfrei gehört der HttpClient zu den bekanntesten Services im Lieferumfang von Angular. Für Version 15 hat ihn das Angular-Team nun an die neuen Standalone Components angepasst. Bei dieser Gelegenheit wurde auch das Interceptors-Konzept überarbeitet. In diesem Artikel gehe ich auf diese Neuerungen ein.

The post HttpClient in Angular 15, Standalone APIs und funktionale Interceptors appeared first on JavaScript & Angular Days.

]]>
Das dazu verwendete Beispiel findet man unter [1].

Standalone APIs für HttpClient?

Angular 14 brachte die langersehnten Standalone Components, die nun endlich Angular-Module optional machen. Allerdings sind solche eigenständigen Komponenten nur eine Seite der Medaille, denn Komponenten stützen sich in der Regel auf Services. Solche Services gilt es jedoch bereitzustellen, und genau das ist eine der Aufgaben von Angular-Modulen, die wir nun aber loswerden wollen.

Zum Glück gibt es bereits seit einiger Zeit eine Möglichkeit, Services ohne Angular-Module bereitzustellen, nämlich via @Injectable({providedIn: ‘root’}). Auch wenn diese Vorgehensweise zu bevorzugen ist, lässt sie sich leider nicht immer anwenden. Gerade dann, wenn der Service parametrisierbar sein soll, stößt man an die Grenzen dieser komfortablen Option. Deswegen erlaubt Angular auch beim Bootstrapping der AppComponent, Services bereitzustellen:

bootstrapApplication(AppComponent, {
  providers: [
    MyService,
    importProvidersFrom(HttpClientModule)
  ]
}

Diese Services richtet Angular im Root Scope ein. Somit entsprechen diese Services u. a. jenen, die eine Angular-Anwendung früher im AppModule registriert hat. Die vom Angular-Team angebotene Funktion importProvidersFrom erlaubt den Brückenschlag zu Services in bereits existierenden Angular-Modulen. Somit lässt sich Bestandscode ohne Änderungen nutzen.

Der Einsatz von importProvidersFrom ist jedoch nur eine Übergangslösung, zumal künftig Angular-Anwendungen gänzlich ohne Angular-Module auskommen sollen. Die Lösung für dieses Problem nennt sich Standalone APIs. Sie erlauben das Einrichten von Services ohne Umweg über Module. Mit Version 15 erhält Angular nun Standalone APIs für den HttpClient (Listing 1).

Listing 1

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor]),
    ),
  ]
}

Die neue Funktion provideHttpClient liefert die Services rund um den HttpClient. Außerdem aktiviert sie optionale Features des HttpClient. Für jedes Feature steht eine eigene Funktion zur Verfügung. Die Funktion withInterceptors aktiviert zum Beispiel die Unterstützung für http Interceptors.

Die Kombination aus einer provideXYZ-Funktion und mehreren optionalen withXYZ-Funktionen ist hier nicht willkürlich gewählt, sondern entspricht einem Muster, das das Angular-Team generell für Standalone APIs vorsieht. Anwendungsentwickler:innen müssen somit beim Einrichten einer neuen Bibliothek nach Funktionen Ausschau halten, die mit provide oder with beginnen.

Dieses Muster führt übrigens zu einem sehr angenehmen Nebeneffekt: Bibliotheken werden besser Tree-shakable. Das liegt daran, dass sich über eine statische Quellcodeanalyse sehr einfach herausfinden lässt, ob die Anwendung eine Funktion jemals aufruft. Bei Methoden ist das aufgrund der Möglichkeit einer polymorphen Nutzung der zugrundeliegenden Objekte nicht ganz so einfach.

In guter Gesellschaft

Es ist nicht das erste Mal, dass das Angular-Team für eine bestehende Bibliothek Standalone APIs einführt. Bereits Angular 14.2 kam mit entsprechenden APIs für den Router. Auch hier hält sich das Framework an das besprochene Muster:

provideRouter(APP_ROUTES,
  withPreloading(PreloadAllModules),
),

Etwa zur selben Zeit hat auch das NgRx-Team eine Standalone-API für das Einrichten von Reducern, Effects sowie für die die Nutzung der Redux Dev Tools eingeführt:

provideStore(reducer),
provideEffects([]),
provideStoreDevtools(),

Funktionale Interceptors

Beim Einführen von Standalone APIs hat das Angular-Team auch gleich die Gelegenheit genutzt, den HttpClient ein wenig zu überarbeiten. Ein Ergebnis daraus sind die neuen funktionalen Interceptors. Sie erlauben es, Interceptors als einfache Funktion auszudrücken. Ein eigener Service, der ein vorgegebenes Interface realisiert, ist nicht mehr notwendig (Listing 2).

Listing 2

import { HttpInterceptorFn } from "@angular/common/http";
import { tap } from "rxjs";
 
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  console.log('request', req.method, req.url);
  console.log('authInterceptor')
 
  if (req.url.startsWith('https://demo.angulararchitects.io/api/')) {
    // Setting a dummy token for demonstration
    const headers = req.headers.set('Authorization', 'Bearer Auth-1234567');
    req = req.clone({headers});
  }
 
  return next(req).pipe(
    tap(resp => console.log('response', resp))
  );
}

Der gezeigte Interceptor erweitert HTTP-Aufrufe, die an bestimmte URLs gerichtet sind, um ein beispielhaftes Securitytoken. Abgesehen davon, dass der Interceptor nun eine Funktion des Typs HttpInterceptorFn ist, hat sich an der prinzipiellen Funktionsweise des Konzeptes nichts geändert. Wie in Listing 1 gezeigt, lassen sich funktionale Interceptors mit withInterceptors beim Aufruf von provideHttpClient einrichten.

Auch in guter Gesellschaft

Auch funktionale Interceptors folgen einem Trend, der bereits mit Angular 14.2 begonnen hat. Seit dieser Version lassen sich Guards und Resolver auch als Funktionen darstellen:

{
  path: 'passenger-search',
  component: PassengerSearchComponent,
  canActivate: [
    () => inject(AuthService).isAuthenticated()
  ]
},

Die neue inject-Funktion erlaubt es, solche Funktionen mit Services zu versorgen.

Erlebe Angular Trends live

16. - 20. März 2026

>

Interceptors und Lazy Loading

Seit jeher führen Interceptors in Lazy-Modulen zu Verwirrung: Sobald ein Lazy-Modul eigene Interceptors einführt, werden jene der übergeordneten Scopes – z. B. im Root Scope – nicht mehr angestoßen.

Auch wenn Module mit Standalone Components und APIs der Vergangenheit angehören, bleibt die prinzipielle Problematik bestehen, zumal nun (Lazy-)Routenkonfigurationen eigene Services einrichten können (Listing 3).

Listing 3

export const FLIGHT_BOOKING_ROUTES: Routes = [{
  path: '',
  component: FlightBookingComponent,
  providers: [
    MyService,
    provideState(bookingFeature),
    provideEffects([BookingEffects])
    provideHttpClient(
      withInterceptors([bookingInterceptor]),
      withRequestsMadeViaParent(),
    ),
  ],
}];

Diese Services entsprechen denjenigen Services, die die Anwendung früher in Lazy-Modulen registriert hat. Technisch gesehen führt Angular immer dann, wenn solch ein providers-Array vorliegt, einen eigenen Injector ein. Dieser sogenannte Environment Injector definiert einen Scope für die aktuelle Route sowie ihre Kindrouten.

Wie schon bei den Services beim Bootstrappen der AppComponent gilt auch hier, dass @Injectable({providedIn: ‘root’}) wenn möglich zu bevorzugen ist. Es ist zwar nicht offensichtlich, aber solche Services, die die Anwendung im Root Scope platziert, funktionieren auch mit Lazy Loading. Kommt der Service nur in einem Lazy-Anwendungsteil zum Einsatz, lädt ihn die Anwendung gemeinsam mit diesem Anwendungsteil. Das hat mit Angular selbst wenig zu tun, sondern mehr mit der Art und Weise, wie Bundlers funktionieren.

Möchte die Anwendung die eingerichteten Services hingegen konfigurieren, muss sie mit dem gezeigten providers-Array vorliebnehmen. Ein gutes Beispiel ist das in Listing 4 gezeigte Einrichten eines Feature-Slice samt Effects für NgRx.

Die Funktion provideHttpClient lässt sich ebenfalls im providers-Array nutzen, um Interceptors für den Lazy-Teil der Anwendung zu registrieren. Standardmäßig gilt dabei die zuvor besprochene Regel: Existieren Interceptors im aktuellen Environment Injector, ignoriert Angular die Interceptors in übergeordneten Scopes.

Genau dieses Verhalten lässt sich jedoch mit withRequestsMadeViaParent ändern. Diese Methode führt dazu, dass Angular nach den Interceptors im eigenen Scope jene in den übergeordneten Scopes anstößt.

Allerdings gibt es hier eine nicht offensichtliche Falle: Ein Service im Root Scope weiß nichts vom HttpClient und den registrierten Interceptors im inneren Scope. Er greift immer auf den HttpClient im Root Scope zu und somit kommen auch nur die dort eingerichteten Interceptors zur Ausführung. Abbildung 1 veranschaulicht das.

steyer_standalone_http_01.tif_fmt1.jpg
Um dieses Problem zu lösen, könnte die Anwendung den äußeren Service auch im providers-Array der Routenkonfiguration und somit im inneren Scope registrieren. Generell scheint es jedoch sehr schwierig zu sein, solche Konstellationen im Überblick zu behalten. Deswegen könnte es sinnvoll sein, auf Interceptors in inneren Scopes gänzlich zu verzichten. Als Alternative bietet sich ein generischer Interceptor im Root Scope an, der ggf. sogar Zusatzlogiken mit einem dynamischen import aus Lazy-Programmteilen lädt.

Legacy Interceptors und weitere Features

Auch wenn die neuen funktionalen Interceptors sehr charmant sind, können Anwendungen nach wie vor die ursprünglichen klassenbasierten Interceptors nutzen. Diese Option lässt sich mit der Funktion withLegacyInterceptors aktivieren. Anschließend sind die klassenbasierten Interceptors wie gewohnt über einen Multiprovider zu registrieren (Listing 4).

Listing 4

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor]),
      withLegacyInterceptors(),
    ),
    {
      provide: HTTP_INTERCEPTORS,
      useClass: LegacyInterceptor,
      multi: true,
    },
  ]
});

Daneben bringt der HttpClient noch weitere Features, die sich ebenfalls mit with-Funktionen aktivieren lassen: withJsonpSupport aktiviert zum Beispiel die Unterstützung für JSONP und withXsrfConfiguration konfiguriert Details zur Nutzung von XSRF-Tokens. Ruft die Anwendung withXsrfConfiguration nicht auf, kommen Standardeinstellungen zum Einsatz. Um die Nutzung von XSRF-Tokens komplett zu deaktivieren, ist hingegen withNoXsrfProtection aufzurufen.

Bleib informiert

Brancheninsights im Newsletter erhalten:

[mc4wp-simple-turnstile]

Zusammenfassung

Der überarbeitete HttpClient harmoniert nun wunderbar mit Standalone Components und den damit einhergehenden Konzepten wie Environment Injectors. Bei der Gelegenheit hat das Angular-Team auch die Interceptors überarbeitet: Sie lassen sich nun in Form von einfachen Funktionen umsetzen und beim Einbinden des HttpClients registrieren. Außerdem besteht nun auch die Möglichkeit, Interceptors in übergeordneten Scopes zu berücksichtigen.

The post HttpClient in Angular 15, Standalone APIs und funktionale Interceptors appeared first on JavaScript & Angular Days.

]]>