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