Making full use of Angular providers — Part 1
Providers and dependency injection are one of the most critical and unique features of Angular. They allow you to organize your code in ways that may not come intuitively at first, but provide a level maintainability that can’t be found in other web development frameworks. No matter how advanced of an Angular user we are or how complicated our application is, chances are we could still improve its architecture by declaring more providers.
This article is the first one in a series whose goal is to explore extracting component code into separate, re-usable, providers to provide better maintainability.
Wait, back up for a second, what’s a provider?
A provider is an object declared to Angular so that it can be injected in the constructor of your components, directives and other classes instantiated by Angular. Ok, that’s still pretty abstract, but you’re probably familiar with services. A service is a particular type of provider that is declared with its class name, as you can see in the Angular tutorial. You declare it either in your NgModule
or in a specific component simply by adding providers: [MyService]
to your configuration metadata.
There are many ways to declare providers by value, alias, factory, … You can find all of them in this section of the Angular documentation. Hopefully we’ll get to see most of them in this series, showing actual examples of when to use which.
Finally, one last thing to note before we move on. Providers declared for a component are only injectable within that component, and they override any provider with the same “name” (whether it’s a class name, an injection token, …) that were declared by parent components or by the module itself.
Our real-life example
In this article we’ll take a simple real-life example that I have seen many times, and show how we could improve it with an extra provider.
When writing radio buttons and checkboxes, we all know you need to link each <label>
to the corresponding <input>
otherwise clicking on the label doesn’t do anything. A common solution is to place the <input>
inside of the <label>
, but assistive technologies offer limited support for this and most style libraries out there require you to separate them anyway for styling purposes. The recommended solution is to set an id
attribute on the <input>
and to add a matching for
attribute on the label:
No surprise so far, this is HTML 101. But what happens when you have an Angular component that uses a radio or a checkbox, and you use that component all over the place in your application? You end up putting unique ids that you don’t care about everywhere, or you use some sort of unique id generator. The first case isn’t very interesting, so let’s look at the second one.
The simplest unique id generator
The easiest way to generate a unique number for each instance of a component is simply to put a counter variable in the same ES6 module (meaning the same file) as the class declaration of the component, and to increment it in the constructor of the component. The counter variable being at the root of the module, it is instantiated once and shared by all instances of the component.
Let’s take a look at it in action:
Play around with the example on StackBlitz. Inspect the elements and get familiar with the example, even if it’s unfamiliar. This will help you understand the code when we extract it into a provider below.
Note that in our example, we declared a sub-component that contains the label. Obviously this is overkill for such a simple case. In real life cases the todo might be complex enough to split into separate components, so we mimic this to showcase component communication. Here, the id
of the checkbox is passed to the label as an Input
.
Extracting the id generation as a provider
This time, I’ll show the end result first, and go over it step by step:
Let’s first look at the new provider file we created. It declares variables you might not be familiar with:
The trick of the NB_INSTANCES
at the root of the ES6 module is still the same: this variable will be instantiated a single time for the entire life of the application, and will be shared by anyone accessing this module.
Next, the InjectionToken<string>
is used in the @Inject
annotation to tell Angular which dependency to inject, and the type parameter allows Typescript to know what type of object will be injected, in our case a string
. Think of service classes as special injection tokens that inject their own type.
Finally, we configure the provider for the previous token: we’re telling Angular that when a component declares this provider for theUNIQUE_ID
token, it should use the factory function we provide to instantiate it.
The important part here is that we get a new unique id every time the provider is declared, not every time it is injected. Here is the overall process Angular goes through when using this provider:
- A
TodoComponent
is about to be instantiated. - Angular notices it declares the
UNIQUE_ID_PROVIDER
so it calls the factory function, then associates the object returned by the factory to theUNIQUE_ID
token. Since the factory was called, this incremented our counter by 1 and the id generated is a brand new one. - Angular instantiates the
TodoComponent
. It asks the injector for the object associated with theUNIQUE_ID
token and passes it to theTodoComponent
constructor. The factory isn’t called, we’re just using the id generated in step 2. - Angular instantiates the
TodoLabelComponent
. It asks the injector for the object associated with theUNIQUE_ID
token (which gets it from its parent injector, the one from theTodoComponent
) and passes it this time to theTodoLabelComponent
constructor. It’s still the same id, the factory wasn’t called again. - Another
TodoComponent
is about to be instantiated, restart from step 1. In particular, the factory will be called again in step 2, which is how we generate a new id.
From there, using this provider is very simple:
We declare the UNIQUE_ID_PROVIDER
in the component’s metadata, then use the injection token to get it in the constructor of our component. We can then inject it the exact same way in our label component:
So, what did we get out of this?
Obviously, we get a reusable unique id generator. It’s not tied to the todo component anymore, it can be used anywhere we want, all we have to do is add providers: [UNIQUE_ID_PROVIDER]
to any component that needs a unique id. This new generator is much easier to test because it’s isolated, and the instance counter trick doesn’t pollute our component classes anymore.
But most importantly, it opened a new way of communication between the sub-components. Instead of having to pass inputs from top to bottom, they can now all simply request the id in their constructor, which simplifies our templates. In our simple example the gain isn’t that significant, but when we write complex components composed of 5 different subcomponents nested on several levels, passing the id around through inputs quickly becomes cumbersome.
This becomes especially critical when dealing with sub-components that are content children and not view children, meaning when the sub-components are projected into the main component. This is a more advanced use case that we will explore in the next article of this series, I will not delve into a complex explanation but simply share “before and after” demos.
Before we use a provider, the way for the label to get the input’s id is to inject the parent component itself (which can lead to ugly cyclic dependencies), like so:
Our previous solution using a provider works exactly the same as before, not a single change is needed:
I hope this convinces you to start using providers for simple things like a 2-lines id generation. It will result in better separation of concerns, reusability and overall maintainability.
In the second part of this series, we will push further into the topic of using providers to simplify component communication and avoid endless @Input
chaining. Feel free to comment about specific providers use cases you’d like to see addressed, topic suggestions are welcome!
Thanks to the Clarity team and to Matt Hippely in particular for their help writing this article.