Dependency injection in Angular libraries

Useful patterns for parameterizing Angular modules

Matěj ChalkFront-end engineer | FlowUp

article image

When using multiple Angular projects in your codebase (e.g. when using an Nx monorepo), it is important to maintain a clean dependency tree. One major anti-pattern is for a library to import from your app (causes a circular dependency and your library is no longer generic). However, you often need your library to have access to some app-specific details. These may be provided in two ways:

  1. Pass in a parameter on a per-usage basis (i.e. for every occurrence of the library's component/pipe/directive in a template, or every invocation of the library's service/function). This is trivial, but not suitable for all cases - see next point.

  2. Inject parameters globally when importing the library. This is much more elegant when a parameter is static within your app, but it is not so obvious how to best implement it. Therefore, this will be the focus of the rest of the article.

Parameterizing libraries imported in AppModule

This section is for libraries whose module is imported once in the root module. Typically, this is the case for libraries that export a provider (i.e. a service that can then be injected throughout the app), or an NgRx feature module (state management for a store slice).

Example - SEO service

In this example, we have a library that exports an SEOModule, providing an SEOService that can be used to update meta tags on route changes. It also takes care of setting all the OpenGraph and Twitter tags, and is able to provide some generic defaults (e.g. determining URL for og:url). Since this is something multiple apps can take advantage of, we want this library to be generic. But the library needs to be provided some app-specific information, e.g. a title for the website, canonical URL, or Facebook App ID. This configuration is global by nature, so it's best to provide it in the AppModule. The convention used for such cases in external libraries is for the library module to have a static forRoot method, where the necessary parameters can be passed in.

SEO library usage

The library module then has to take care of providing these values internally so that its services etc. have access to them. This can be done via dependency injection - the library module's forRoot return value will be of type ModuleWithProviders (built-in type from @angular/core) in order to provide these values as internal dependency injection tokens. The library implementation looks something like this:

SEO library implementation

Parameterizing libraries imported in component modules

The forRoot solution is convenient for libraries that are imported once in the root module. However, if a library is exporting a UI element (component/pipe/directive), it is a common pattern to import the library's module only in the modules of components that are using it (to take advantage of lazy loading). Having to remember to import the module via forRoot (with the exact same values each time) would be quite tiresome and error-prone. A better solution in this case is to also export the library's DI token, and instruct library users to provide the token in the AppModule, in the understanding that this will then apply globally to all library module imports throughout the app.

Example - Relative date pipe with i18n support

In our next example, we have a library with a RelativeDatePipeModule, which exports a pipe for formatting dates based on how long ago they occurred. This pipe receives a date as input, compares it to the current date and then returns a string like '5 hours ago' or 'yesterday at 4 PM'. A key feature of this pipe is internationalization, so that it's also usable for different languages. It would be incovenient to have to pass in a language parameter for each usage (either as a secondary parameter or when importing the pipe's module). Instead, we would like apps to be able to integrate it like this:

Relative date pipe library usage

The internal library implementation is then very similar to the previous example.

Relative date pipe library implementation

Conclusion

Angular's dependency injection mechanism provides an elegant solution for parameterizing Angular libraries when importing them. The forRoot convention should be preferred when suitable, but a looser approach can also be used (just make sure it's well documented). It's also important to differentiate between required and optional dependencies - missing parameter errors should be reported as clearly as possible, but for most parameters it is better if the library can fall back on sensible defaults.

Can you feel the flow?

Drop us a note

hello@flowup.cz
We're based in Brno

Šumavská 519/35

Brno

602 00

Czech Republic

map