Getting trapped into the Angular DI mechanism

Aristeidis Bampakos
Angular Athens
Published in
7 min readOct 12, 2020
“Tomcat mouse trap” by jronaldlee is licensed under CC BY 2.0

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:

  1. When we create a module
  2. When we create a component.
  3. When we create a lazy-loaded module.

Injectors in an Angular app are organized hierarchically:

Hierarchical injectors

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 MatDialogservice that will emit only when the dialog actually returns a value:

ui.service.ts

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 thenextmethod when the afterClosed observable emits a value.

Notice that we call the complete method of the observer in any case, to indicate that the afterClosedobservable completes properly.

Our Angular application can now use the newly createdshowDialogmethod 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:

StackBlitz demo

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:

create-pirate.component.ts

Both CreatePirateComponent and PirateService are declared in PiratesModule which is lazy-loaded when we access the default route of the application:

pirates.module.ts

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:

Provider injection error

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 ?

“Surprise” by AJC1 is licensed under CC BY-SA 2.0

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 the root injector as described in the providedIn property of the @Injectable decorator.
  • CreatePirateComponent and PirateServiceare declared in PiratesModule 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 inside UiService is part of the MatDialogModule that is also registered with the root injector.

Thus, the MatDialog service is not aware of the PirateServicebecause they exist in different injectors.

There are many ways to solve this problem, including:

  1. Set the providedIn property of UiService to any 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.
  2. The open method of the MatDialog service accepts a MatDialogConfig object as a second parameter. We can set the viewContainerRef and componentFactoryResolver properties of that object from CreatePirateComponent . In this way, we can switch the context of the injector that serves the MatDialog service and make it the same as CreatePirateComponent .

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 :)

--

--

Aristeidis Bampakos
Angular Athens

Angular Google Developer Expert — Web Development Team Lead at Plex-Earth — Award winning author — Speaker