February 7, 2025

Angular Signals Part 5 – Step by step to Signal Directives

Klicke hier für die deutsche Version des Blog-Posts.

Angular 17: Work better and faster with Signal Directives

Angular distinguishes between component, structure and attribute directives. In this article, we will focus on structure directives and show you step by step how you can work with Signal Directives more easily and effectively. For a quick refresher on Angular Signals, how they work and their benefits, check out the other articles in our Angular Signals series:

As a starting point for the article, we have the UserRoleRequired directive, which is used to control access to certain areas of an Angular application based on user roles. This makes it possible to implement granular access control and ensure that only authorized users can access sensitive or restricted content.

First, let’s take a look at the directive itself and introduce the traditional and already known approach. Then we will convert the code piece by piece to the approach with input signals.
In the first step, we use inject() instead of the traditional constructor. In the second step, we use the input function, which replaces the @Input Decorator and lifecycle hook ngOnChanges(). In the third step, we convert observables to signals with toSignal() so that we don’t have to worry about a subscription manually. Last but not least, we combine the signals with effect().

You can follow the individual steps and intermediate results here in StackBlitz or try them out yourself.

If you would like to learn more about using Signals and Angular in detail, why not attend one of our Angular courses?

UserRoleRequired directive: Traditional approach

In the traditional approach of a structure directive in Angular, we usually use a classic mechanism to retrieve user data and react to changes. In our case, we have a directive that checks whether the user has the required role to display certain content. The data from the input and an observable are then combined.

The output code is based on the classic ngOnChanges() lifecycle hook to respond to changes in the input values and take appropriate actions. Subscribing to observables that monitor the user status and manual cleanup with ngOnDestroy() is also typical of this approach.

Signals offer a better way to work with reactive data without the need for manual management of subscriptions or complex lifecycle methods. They also enable a more direct and responsive way of binding data.
In the next steps, we will adapt the directive so that we can use signals to simplify the code and improve maintainability. This will also make the code much clearer.

Step 1: Use of inject()

In Angular, inject() is a function that allows you to inject dependencies directly into a class without using the traditional constructor mechanism. It provides a more elegant and flexible way to manage dependencies without overloading the constructor with many parameters. Instead of passing the dependencies via the constructor, which can quickly make the code confusing, inject() creates a clean separation between the logic and the dependencies. Like this we declare each dependency directly at the point where we need it.

You can also find more information in the Angular inject() documentation.

In the traditional approach, we have structured the code in such a way that we inject the LoginService via the constructor of the directive. But this quickly becomes confusing if there are many dependencies. The constructor has to receive and manage all the required services, which leads to greater complexity.

In the new approach, we use inject() to get the dependencies directly into the class. This leads to a clearer separation of the logic and the required services. In the case of our directive, the LoginService is now made available directly with the inject() function, without having to be passed via the constructor.

You can find more examples of how to change the constructor to inject() and why you should use inject() here.

Step 2: Use of the Input-function instead of @Input and ngOnChanges

In Angular, we have traditionally used the @Input decorators and the ngOnChanges() lifecycle hook to pass input values from the outside into a component or directive and react to them. This approach works well, but is greatly simplified and improved with the introduction of Signals and the Input function.

In the classic approach, an input is defined in a directive with the @Input decorator, and changes are processed via the ngOnChanges() hook. In our case, the @Input decorator is used to declare the requiredUserRole, and any change to this value is processed in the ngOnChanges() hook.

The code above is changed by using input and signals in such a way that a large part of the function is omitted, making the code much clearer.

With the introduction of Signals, we can use the input function to declare input values directly without needing a separate lifecycle hook like ngOnChanges(). The input function makes the declaration of input values easier and the reactivity is automatically applied.

You can read more about the input function in the Angular documentation.

Step 3: Conversion of the observable with toSignal()

Traditionally, we often work with observables to handle asynchronous data, such as user information retrieved from a backend service. A common method is to use getUser(), which returns an observable that must then be subscribed to in order to access the data. However, with the introduction of Signals in Angular, we can convert these Observables into Signals. Converting observables into signals means that we no longer have to worry about subscriptions ourselves.

userRoleFromLogin is converted from the getUser() observable into a signal using the toSignal() function. When the underlying observable changes we can ensures that the signal is automatically updated. This eliminates the need to manually subscribe streams or manage subscriptions. At the same time, the declarative and reactive handling of the data is retained. Instead of relying on manually subscribing and managing streams, we can now work directly with reactive signals.

You can read more about the toSignal() function and how to replace observables in the Angular documentation. Or in the second post in our blog series.

Step 4: Combine Signals with effect

Another concept in the new approach is the combination of signals with the effect() function, which automatically reacts to signal changes and executes corresponding actions. This further improves reactivity and event handling compared to traditional methods.

An effect is executed every time its signal dependencies change. During execution, it dynamically tracks which signals have been read and is only triggered again if these values change. As a result, the state of the application adapts efficiently and automatically. In addition, effects always run at least once and are executed asynchronously during change detection.

You can find out more about effect in the Angular documentation.

In our last step, effect() is used to automatically react to changes in the userRole and userRoleFromLogin signals. If userHasSufficientPermission is true, the view is created with createEmbeddedView. Otherwise, it is removed with clear(). We have thus summarized the entire reactivity mechanism in an effect block that adjusts the view for every relevant change.

UserRoleRequired directive: Signal approach

And that’s it! After these four simple steps, we have successfully changed the UserRoleRequired directive to Signals:

Here you can see the complete code again, which includes all the previous improvements. By using inject(), the input() function for the input values, the conversion of observables with toSignal() and the combination of signals with effect, we have significantly simplified the code and at the same time improved reactivity and maintainability.

Try it out for yourself: Hands-On with Signal Directives

In our interactive StackBlitz example, you can take another look at the individual steps and intermediate states in the code and try them out for yourself. Each step is a separate file so that the code remains clear here too.

What’s next?

With Angular v19, there was also news regarding Signals, which we naturally don’t want to withhold from you. So our blog series continues and in the next article we will introduce you to linkedSignals.

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.