February 7, 2025

Angular Signals Teil 5 – Schritt für Schritt zu Signal Directives

Click here for the English version of this blog post.

Angular 17: Arbeite besser und schneller mit Signal Directives

Angular unterscheidet zwischen Komponenten-, Struktur- und Attribut-Direktiven. In diesem Beitrag werden wir uns auf Struktur-Direktiven konzentrieren und Dir Schritt für Schritt zeigen, wie Du mit Signal Directives einfacher und effektiver arbeiten kannst. Für einen kurzen Refresh zu Angular Signals, ihrer Funktionsweise und ihren Vorteilen schau doch in die anderen Artikel unserer Angular Signals Reihe:

Als Ausgangspunkt für den Artikel haben wir die UserRoleRequired-Direktive, welche dazu dient, den Zugriff auf bestimmte Bereiche einer Angular-Anwendung basierend auf Benutzerrollen zu steuern. Diese ermöglicht es, eine granulare Zugriffskontrolle zu implementieren und sicherzustellen, dass nur autorisierte Benutzer auf sensible oder eingeschränkte Inhalte zugreifen können.

Zuerst wollen wir uns die Direktive an sich anschauen und den traditionellen und bereits bekannten Ansatz vorstellen. Danach bauen wir den Code Stück für Stück auf den Ansatz mit Input Signals um.
Im ersten Schritt nutzen wir inject() anstatt dem herkömmlichen Konstruktor. Im zweiten Schritt wird die Input-Funktion genutzt, welche den @Input Decorator und Lifecycle-Hook ngOnChanges() ersetzt. Damit wir uns nicht manuell um eine Subscription kümmern müssen, wandeln wir im dritten Schritt Observables mit toSignal() zu Signals um. Zu guter Letzt kombinieren wir die Signals noch mit effect().

Die einzelnen Schritte und Zwischenstände kannst Du hier in StackBlitz nachverfolgen oder selber ausprobieren.

Wenn Du mehr über den Umgang mit Signals und Angular im Detail erfahren möchtest, besuche doch einen unserer Angular-Kurse:

UserRoleRequired-Direktive: Traditioneller Ansatz

Im traditionellen Ansatz einer Struktur-Direktive in Angular verwenden wir in der Regel einen klassischen Mechanismus, um Benutzerdaten abzurufen und auf Änderungen zu reagieren. In unserem Fall haben wir eine Direktive, die überprüft, ob der Benutzer die erforderliche Rolle hat, um bestimmten Inhalt anzuzeigen. Es werden die Daten von einem Input und einem Observable kombiniert.

Der Ausgangscode basiert auf dem klassischen ngOnChanges()-Lifecycle-Hook, um auf Änderungen an den Eingabewerten zu reagieren und entsprechende Aktionen zu unternehmen. Das Subscriben an Observables, die den Benutzerstatus überwachen, und das manuelle Aufräumen mit ngOnDestroy() ist ebenfalls typisch für diesen Ansatz.

Signals bieten eine bessere Möglichkeit, mit reaktiven Daten zu arbeiten, ohne die Notwendigkeit von manuellem Management von Subscriptions oder komplexen Lifecycle Methoden. Außerdem ermöglichen sie eine direktere und reaktionsschnellere Art der Datenbindung.
In den nächsten Schritten werden wir die Direktive so anpassen, dass wir Signals verwenden können, um den Code zu vereinfachen und die Wartbarkeit zu verbessern. Der Code wird dadurch auch deutlich übersichtlicher werden.

Schritt 1: Einsatz von inject()

In Angular ist inject() eine Funktion, die es ermöglicht, Abhängigkeiten direkt in eine Klasse zu injizieren, ohne den traditionellen Konstruktormechanismus zu verwenden. Es bietet eine elegantere und flexiblere Möglichkeit, Abhängigkeiten zu verwalten, ohne den Konstruktor mit vielen Parametern zu überladen. Statt die Abhängigkeiten über den Konstruktor zu übergeben, was den Code schnell unübersichtlich machen kann, wird mit inject() eine saubere Trennung zwischen der Logik und den Abhängigkeiten geschaffen. Jede Abhängigkeit wird direkt an der Stelle deklariert, wo sie gebraucht wird.

Mehr Informationen findest Du auch in der Dokumentation zu inject() von Angular.

Im traditionellen Ansatz haben wir den Code so aufgebaut, dass wir den LoginService über den Konstruktor der Direktive injizieren. Dabei wird der Service im Konstruktor empfangen, was jedoch bei vielen Abhängigkeiten schnell unübersichtlich wird. Der Konstruktor muss alle benötigten Dienste aufnehmen und verwalten, was zu einer höheren Komplexität führt.

Im neuen Ansatz verwenden wir inject(), um die Abhängigkeiten direkt in die Klasse zu holen. Dies führt zu einer klareren Trennung der Logik und den benötigten Services. Im Fall unserer Direktive wird der LoginService nun direkt mit der inject()-Funktion zur Verfügung gestellt, ohne dass er über den Konstruktor übergeben werden muss.

Weitere Beispiele zur Abänderung vom Konstruktor zu inject() und warum Du inject() verwenden solltest findest Du hier.

Schritt 2: Verwendung der Input-Funktion statt @Input und ngOnChanges

In Angular haben wir traditionell die @Input-Dekoratoren und den ngOnChanges()-Lifecycle-Hook verwendet, um Eingabewerte von außen in eine Komponente oder Direktive zu reichen und darauf zu reagieren. Dieser Ansatz funktioniert gut, wird aber durch die Einführung von Signals und der Input-Funktion deutlich vereinfacht und verbessert.

Im klassischen Ansatz wird eine Eingabe in einer Direktive mit dem @Input-Dekorator definiert, und Änderungen werden über den ngOnChanges()-Hook verarbeitet. In unserem Fall wird der @Input-Dekorator verwendet, um die requiredUserRole zu deklarieren, und jede Änderung an diesem Wert wird im ngOnChanges()-Hook verarbeitet.

Der obige Code wird durch die Verwendung von input() und Signals so verändert, dass ein Großteil der Funktion wegfällt und der Code damit viel übersichtlicher wird.

Mit der Einführung von Signals können wir die input-Funktion verwenden, um Eingabewerte direkt zu deklarieren, ohne einen separaten Lifecycle-Hook wie ngOnChanges() zu benötigen. Die input-Funktion macht die Deklaration von Eingabewerten einfacher und die Reaktivität wird automatisch übernommen.

Mehr zur input-Funktion kannst Du in der Dokumentation von Angular nachlesen.

Schritt 3: Umwandlung des Observable mit toSignal()

Im traditionellen Ansatz arbeiten wir oft mit Observables, um asynchrone Daten zu handhaben, wie zum Beispiel Benutzerinformationen, die von einem Backend-Dienst abgerufen werden. Eine häufige Methode ist die Verwendung von getUser(), die ein Observable zurückgibt, an welches dann subscribed werden muss, um auf die Daten zuzugreifen. Mit der Einführung von Signals in Angular können wir diese Observables jedoch in Signals umwandeln. Die Umwandlung von Observables in Signals bedeutet, dass wir uns nicht mehr selbst um die Subscriptions kümmern müssen.

userRoleFromLogin wird hier aus dem Observable getUser(), mit Hilfe der toSignal()-Funktion, in ein Signal umgewandelt. Das sorgt dafür, dass das Signal automatisch aktualisiert wird, wenn sich das zugrunde liegende Observable ändert. Dadurch entfällt die Notwendigkeit, manuell Streams zu subscriben oder Subscriptions zu verwalten. Gleichzeitig bleibt die deklarative und reaktive Handhabung der Daten bestehen. Anstatt auf das manuelle subscriben und Verwalten von Streams angewiesen zu sein, können wir nun direkt mit reaktiven Signals arbeiten.

Mehr zur toSignal()-Funktion und wie man Observables ersetzen kann, kannst Du in der Dokumentation von Angular nachlesen. Oder auch im zweiten Beitrag unser Blogreihe.

Schritt 4: Signals mit effect kombinieren

Ein weiteres Konzept im neuen Ansatz ist die Kombination von Signals mit der effect()-Funktion, die automatisch auf Signaländerungen reagiert und entsprechende Aktionen ausführt. Dies verbessert weiterhin die Reaktivität und das Event-Handling im Vergleich zu traditionellen Methoden.

Ein effect wird bei jeder Änderung seiner Signal-Abhängigkeiten ausgeführt. Während der Ausführung verfolgt er dynamisch, welche Signals gelesen wurden, und wird nur bei Änderungen dieser Werte erneut ausgelöst. Dadurch passt sich der Zustand der Anwendung effizient und automatisch an. Zusätzlich laufen effects immer mindestens einmal und werden asynchron während der Änderungsdetektion ausgeführt.

Mehr zu effect kannst Du natürlich auch wieder in der Dokumentation von Angular finden.

In unserem letzten Schritt wird effect() verwendet, um automatisch auf Änderungen der Signals userRole und userRoleFromLogin zu reagieren. Wenn userHasSufficientPermission wahr ist, wird die View mit createEmbeddedView erstellt. Andernfalls wird sie mit clear() entfernt. Damit haben wir den gesamten Reaktivitätsmechanismus in einem effect-Block zusammengefasst, der bei jeder relevanten Änderung die View anpasst.

UserRoleRequired-Direktive: Signal Ansatz

Und das wars! Nach diesen vier einfach Schritten haben wir erfolgreich die UserRoleRequired-Direktive auf Signals umgestellt:

Hier siehst Du nochmal den vollständigen Code, der alle vorherigen Verbesserungen beinhaltet. Durch die Verwendung von inject(), der input()-Funktion für die Eingabewerte, die Umwandlung von Observables mit toSignal() und der Kombination von Signals mit effect, haben wir den Code deutlich vereinfacht und gleichzeitig die Reaktivität und Wartbarkeit verbessert.

Probiere es selber aus: Hands-On mit Signal Directives

In unserem interaktiven StackBlitz-Beispiel kannst Du Dir die einzelnen Schritte und Zwischenstände im Code nochmal in Ruhe ansehen und selber probieren. Jeder Schritt ist dabei eine eigene Datei, damit auch hier der Code übersichtlich bleibt.

Was kommt als nächstes?

Mit Angular v19 gab es auch Neuigkeiten in Bezug auf Signals, die wir Dir natürlich nicht vorenthalten wollen. Unsere Blogreihe geht also weiter und im nächsten Artikel stellen wir Dir linkedSignals vor.

Terminübersicht der nächsten Angular Schulungen

Related Posts

theCodeCampus Autor Marc Dommröse

Marc Dommröse
Developer at thecodecampus </>

My name is Marc. I'm currently studying Computer Science at university, with one of my favorite topics being frontend development.


Leave a Reply

Add code to your comment in Markdown syntax.
Like this:
`inline example`

```
code block
example
```

Your email address will not be published.