Getting trapped into the Angular DI mechanism
The Angular framework includes a top-notch mechanism for providing dependencies to an Angular application, called the Angular dependency injection or simply Angular DI.
It is a core feature of the Angular framework and allows us to inject dependencies in various Angular artifacts, such as components and directives. Dependencies are usually Angular services, but we can use any object that can be considered injectable from the framework.
The Angular DI is a powerful and fundamental concept of the Angular framework. It hides much of the complexity when it comes to creating and using dependencies in an Angular app, a daunting process if we try to do it manually ourselves. But with great power comes great responsibility. We need to be careful when using Angular DI; otherwise, we may fall into a trap.
In this article, we will see how we can easily get caught in a pitfall while trying to use Angular DI in the most efficient way inside an Angular app.
Dependency injectors
Central to the Angular DI mechanism is the injector, responsible for managing dependencies in an Angular app. It creates, maintains, and serves dependencies to Angular artifacts that require them, such as components and directives or even services. The Angular artifact does not need to know anything about the dependency, except for just asking it!
An Angular app has at least a main injector, called the root
injector. It can also have other injectors that are created in the following cases:
- When we create a module
- When we create a component.
- When we create a lazy-loaded module.
Injectors in an Angular app are organized hierarchically:
In the previous image, when any of the A2/A3 components require a service, it will first ask its own component injector whether it provides the service or not. If the component injector provides the service, it will return an instance to the component that asked it. Otherwise, it will propagate the request to the parent component, A1. If the parent component does not also provide the service, it will forward the request further up to the tree, in module A, until it reaches the root injector.
When a service is not provided by an injector, even by the root, the Angular framework will throw an error.
The Angular DI mechanism is governed by rules that can easily be violated if we are not careful, and we may finally end up in trouble. In the following section, we will see such a case using the Dialog component of the Angular Material library.
Using Angular Material Dialog component
The Angular Material is a library that contains a rich set of UI components based on Material Design techniques. It is a collection of well-crafted components that are built using the Angular framework in mind. One of these components is the Dialog component that allows us to render an Angular component inside a modal popup window.
To use an Angular Material Dialog component in an Angular app, we need to import MatDialogModule
. It contains all necessary directives and services for working with dialogs in an Angular app. It also exports the MatDialog
service that we can use to display an Angular component as a modal window:
constructor(private dialog: MatDialog) { }showDialog() {
this.dialog.open(MyComponent).afterClosed().subscribe(result => {
if (result) {
// do something
}
});
}
In the previous snippet, we use the MatDialog
service to open MyComponent
by calling the open
method of the service and passing the class of the component as a parameter. We also subscribe to the afterClosed
observable so that we will get notified when the dialog is closed. The afterClosed
observable can emit a result, optionally, when the dialog is closed.
A typical dialog contains two types of buttons that have a different meaning:
1. A submit button that takes further action using data from the dialog, such as submitting a form to a back-end API or responding positively to a question.
2. A close/cancel button that closes the dialog without taking any further action.
So, it is obvious that we need to check the value of theresult
property from the afterClosed
observable to understand the outcome of the user’s action. Large enterprise Angular applications usually have more than one dialog in their codebase. The application code will become cluttered and difficult to test if we write conditional code to check the result of each one. Not to mention that it violates the Do not Repeat Yourself (DRY) principle! Can we do better?
Yassss! In the following section, we will see how to create an Angular service for reusing some of the functionality of the Angular Material Dialog component.
Creating a reusable dialog service
We can create an Angular service that encapsulates the business logic of the MatDialog
service. In a nutshell, we will create a wrapper observable around the open
method of the MatDialog
service that will emit only when the dialog actually returns a value:
In the previous snippet, we create the showDialog
method that accepts the type of component we want to display in a dialog as a parameter. Internally, it creates a new observable using the Observable
constructor and calls the open
method of the MatDialog
service. The observer
object of the new observable calls thenext
method when the afterClosed
observable emits a value
.
Notice that we call the
complete
method of theobserver
in any case, to indicate that theafterClosed
observable completes properly.
Our Angular application can now use the newly createdshowDialog
method to interact with the Angular Material Dialog component. All of it?? No! There is an exception where we can’t use this approach. In the following section, we will learn about this case by providing an example using lazy loaded modules.
Dependency injection in lazy loaded modules
Consider the following Angular application that displays a component with a single button. The button is used to open another Angular component as a modal dialog:
It uses the showDialog
method of UiService
that we saw earlier in the previous section. The CreatePirateComponent
is the component that we want to display in the dialog and injects another Angular service,PirateService
, through its constructor:
Both CreatePirateComponent
and PirateService
are declared in PiratesModule
which is lazy-loaded when we access the default route of the application:
Now let’s try to click the Show dialog button and check if the CreatePirateComponent
is displayed properly. When we click the button, nothing happens. If we open the Console window to debug the application, we will see some errors. Let’s focus on the following error message:
It indicates that our Angular application cannot find a suitable injector that provides PirateService
. How is that even possible since we have already provided PirateService
with PiratesModule
?
Well, we did provide the service. But we do not know which injector tried to serve it to our component. Let’s try to demystify things a bit:
UiService
is provided from theroot
injector as described in theprovidedIn
property of the@Injectable
decorator.CreatePirateComponent
andPirateService
are declared inPiratesModule
which is lazily loaded. Recall from the Dependency injectors section that a lazily loaded module creates its own injector.- Additionally, the
MatDialog
service that is used insideUiService
is part of theMatDialogModule
that is also registered with theroot
injector.
Thus, the MatDialog
service is not aware of the PirateService
because they exist in different injectors.
There are many ways to solve this problem, including:
- Set the
providedIn
property ofUiService
toany
so that each component that uses the service gets its own instance. You can learn more about this technique in https://indepth.dev/angulars-root-and-any-provider-scopes/ from Santosh Yadav. - The
open
method of theMatDialog
service accepts aMatDialogConfig
object as a second parameter. We can set theviewContainerRef
andcomponentFactoryResolver
properties of that object fromCreatePirateComponent
. In this way, we can switch the context of the injector that serves theMatDialog
service and make it the same asCreatePirateComponent
.
These are some of the most well-known techniques for mitigating such an issue. I am sure there will be other alternatives according to the architecture of each Angular app.
Conclusion
In this article, we saw some of the basics of the Angular DI mechanism and how injectors play an important role in an Angular app. We learned how an injector can be created and how they are organized in a hierarchical form.
We learned about the Dialog component of the Angular Material library and how we can display Angular components inside a modal window. We tried to create a reusable dialog infrastructure for an Angular app and saw the difficulties we faced when multiple injectors took part.
The Angular DI framework is a powerful mechanism that requires special attention on how we use it. Even the most experienced Angular developers may be well deceived and fall into a trap. So, next time you inject a service and get an error, try to understand how the injector behaves before creating a workaround.
Thanks for reading! Let me know your feedback in the comments below :)