Use RxJS to Push Configuration to Angular Libraries
Angular libraries are much better with configuration using RxJS, interfaces, and typed configuration.
A working solution is available on Github at:
A repository to demonstrate how an Angular application can provide libraries with configuration. …
The Angular Workspace provides the ability to have multiple library and application projects in a single environment. Although this capability has been available since version 6, I find that many developers and teams are not taking full advantage of shared libraries. Having the ability to share and reuse code in the form of libraries adds to the effectiveness of your development team.
Think of the many shared libraries that would benefit your Angular applications. Here is a list of different library types that are common to applications.
- cross-cutting concerns
There are many opportunities to create libraries for our Angular applications. However, one of the difficulties is getting the configuration to these libraries. This post will demonstrate how an Angular application can provide configuration to Libraries in an Angular Workspace environment. The focus will be on providing configuration to libraries. The Logging and ErrorHandling libraries volunteered to participate in this demonstration. However, the process and patterns could be used for any type of library in the Workspace.
What We Get from Angular
Angular applications have an environment context that provides configuration items targeting specific environments for the application. Angular libraries in the Workspace cannot access the environment context. Applications reference and use libraries — and Angular Libraries may reference and use other libraries. If libraries had access to the environment context it would cause a circular dependency.
One of the main tenets of developing reusable libraries is to keep them generic enough to be reused by many applications and libraries. Therefore, the next evolution of the target library is to create a configuration for elements that can vary. Therefore, libraries need configuration.
Note: Not all library projects need configuration, but when they do, they prefer it pushed.
Libraries Need Configuration Too
The target Angular application is the source of the configuration and has the responsibility to provide that configuration to the specified library items. Since the environment context is part of the Application’s domain, the library cannot reference and use this for its configuration.
environmentcontext is an object with data elements. It is not strongly typed in terms of its schema. However, the members of the environment context are typed - however, there is not defined object type.
No need to worry, we can use some simple patterns and create a strongly-typed mechanism for library configuration.
Provide Strongly-Typed Configuration to Angular Libraries
Here is what needs to happen to enable configuration for libraries.
- each library will define its own configuration schema or object type
- each library will define an interface for the structure of the specific configuration
- each library will subscribe to publish events of configuration
- library configuration will be pushed to libraries
- each library has the responsibility to validate the configuration provided
- each library has the responsibility to define default values for any required items.
- each application will retrieve the configuration
- a service will provide the mechanism to publish configuration to library subscribers.
The linchpin in this scenario will be a new Configuration service that will act as a mediator between applications with configuration and the libraries that need that configuration. It is following an inversion of control pattern where libraries cannot retrieve or instantiate their own configuration — it will be provided to them. Applications will use the new ConfigurationService to publish configuration.
The main goal is to use a push strategy. The libraries should not pull or request their own configuration. It is not their responsibility to have such knowledge. Therefore, we will let the libraries react to published events of configuration when applications provide it.
Create a new library in your Angular Workspace. It will be used to mediate the transfer of configuration information from applications to libraries with configuration requirements.
ng generate library configuration --publishable --unit-test-runner=jest
Add a new ConfigurationService to the library. This is a very straight-forward service. It does one thing and it does it very well. It provides the configuration from the application to any subscribers. The target subscribers in our context are libraries. However, the service could also be used by other modules, components, and services within the application.
settings$is a ReplaySubject that buffers a set of values and emits the values immediately to subscribers
- the ReplaySubject will buffer or keep the one configuration available to emit to all subscribers no matter when or where they subscribe.
- a ConfigurationContext is injected into the constructor of the service.
The ConfigurationContext is just a container class for anything that can be provided to the ConfigurationModule and made available to members of that module, like the ConfigurationService. We could pass the actual configuration directly, but I like to keep my options open for future opportunities.
Notice that the
settings$ ReplaySubject is of type
IConfiguration. We are using an interface-driven approach to allow the interfaces to define the types for each library configuration. The
IConfiguration provides the container type for all configuration members.
Providing the Configuration to Libraries
If you are wondering how the ConfigurationContext is provided to the ConfigurationModule, the library’s module has a static
forRoot() method to allow applications to provide the library with the specified configuration.
The ConfigurationContext is just a class to define the members of the context. Currently, it only has a config property. The members of this class are available to the ConfigurationModule when they are set as one of the
providers of the module. Basically, the
forRoot() allows the module to have the input setup as a provider - the ConfigurationContext is now available for dependency injection. The service now has direct access to this input because it is injected into the constructor of the ConfigurationService.
The ConfigurationService in the ConfigurationModule contains a public ReplaySubject. The
ConfigurationService is injected into the constructor of the service. This allows the LoggingService to subscribe to configuration events from the ConfigurationService. When the configuration is emitted, this service will use the provided value (i.e., IConfiguration ).
log() method allows the LoggingService to emit a new LogEntry item. The LogWriters configured by the application will respond by sending the specified log entry to the target repository and/or the console. To recap, the LoggingService only does (2) things:
- uses the
LoggingConfigprovided by the ConfigurationService to set up some of the logging information
LogEntryitems and publishes them to subscribed log writers.
Library Configuration Recipe
This is a functional logging service that can be used by any number of applications and also by other Angular libraries. This is an important part of the library configuration recipe.
- a ConfigurationService is injected into the service constructor.
- the service subscribes to the ReplaySubject for configuration events.
- the service handles a configuration value emitted by the ReplaySubject.
- the service can validate and provide any required default values based on the configuration
Note: You can learn more about the log writers by reviewing the code in the GitHub repository. https://github.com/angularlicious/configuration-for-libraries
The actual configuration for the application should be consistent and typed. It should also be environment specific. Therefore, the application should have a configuration defined for each application environment. Use the same convention of the environment constant.
The AppConfig shown below shows the implementation of an application configuration. This configuration is typed and is based on interfaces. Since it is a Typescript object, there is no need to load the configuration using HTTP or importing a JSON file.
Note: Originally, the solution used JSON files for the configuration. The application imported the JSON file or supplied the path to the file to an HTTP GET call. This could work well in a scenario where you would like to replace the JSON file during the CI/Build process for specific environments. Keep important or private information secure!
Now, each environment (i.e., development, stage, or production) can target the specified configuration and make it available using the
appConfig property of the environment object.
The application’s configuration is now ready. The ConfigurationModule is imported and the application’s configuration is provided to the module using the static
forRoot() method. This module has the responsibility to load the library services and to provide the configuration.
Conclusion: (Bonus: Plug-in Services)
Notice that the module is using the
APP_INITIALIZER to initialize the log writers. What are log writers? Well, the LoggingService has the responsibility to provide an endpoint for the application to send a log message and other details. The writers have the responsibility to write/send the message to a target. The ConsoleWriter simply logs events to the browser's console. You can view the log messages in the developer tools console. This works great for development. However, in non-production environments, the log items should be stored in a centralized repository.
The LogglyWriter writes the log messages to Loggly — a cloud-based provider. This implementation uses Loggly for a centralized repository of application logs. There are several cloud-based solutions to choose for centralized logging. A centralized repository allows you to monitor, filter, and analyze log events for your Angular applications.
All of this made possible using Angular Libraries and typed Configuration from the application. More information about Angular Workspace and the Angularlicious Podcast at https://angularlicio.us — or on Twitter @angularlicious.