Angular platformBrowserDynamic method
In the previous two posts I described how the APP_INITIALIZER could be used to delay application start up so a runtime configuration file could be loaded. The first dealt with client-side configuration, and the second with server-side configuration.
This technique works well in most scenarios where the configuration is only required for services and components after initialisation.
But what if configuration is needed during initialisation to as part of further configuration? Perhaps for example to configure a module. In that scenario, APP_INITIALIZER doesn't work because it happens too late in the bootstrap process.
For example, consider a large app, consisting of multiple modules that are initialised and loaded on demand. In such an application the configuration or just a part of the configuration, could be injected into the modules and therefore become available to all components and services hanging off those modules.
Whilst the simple example described in this post won't go as far as that, it will demonstrate how to use a partial configuration to apply runtime configuration to a logging module, using NgxLogger
.
This alternative, proposed by Tim Deschryver in this excellent post uses the platformBrowserDynamic
operation, to load and store the configuration in a token, APP_CONFIG
.
As before with the APP_INITIALIZER
technique, we rely on the 'fetch
' promise to delay execution whilst configuration is loaded. However, instead of using a factory called from app.module.ts,
we go much earlier - in main.ts
.
We provide runtime configuration to the platformBrowserDynamic
operation in main.ts
, making the configuration available almost immediately on start up. (Some lines removed for clarity and brevity - full code is at my GitHub repository.):
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { APP_CONFIG } from './app/app.config';
let configFile = "assets/config.json";
console.debug(`Executing main.ts - Loading config file from ${configFile}`);
fetch(configFile)
.then((response) =>
{
return response.json();
})
.then((config) =>
{
console.debug(config);
if (environment.production) {
enableProdMode()
}
platformBrowserDynamic([{ provide: APP_CONFIG, useValue: config }])
.bootstrapModule(AppModule)
.catch((err) => console.error(err))
})
We define the APP_CONFIG
token for AppConfig
in the app/app.config.ts
file, but because also we want to configure a separate logger module we also introduce a nested configuration type LoggerConfig
. We'll be injecting this property as a partial configuration into our logger:
import { InjectionToken } from "@angular/core";
import { LoggerConfig } from "./logger.config";
export class AppConfig {
env: string = '';
apiBaseUrl: string = ""
activeTheme: string = '';
logger: LoggerConfig | null = null;
buildRef: string = '';
}
export let APP_CONFIG = new InjectionToken('APP_CONFIG')
Of course we need to define our LoggerConfig
in the app/logger.config.ts
file:
import { NgxLoggerLevel } from "ngx-logger";
export class LoggerConfig {
browserLogLevel: NgxLoggerLevel = NgxLoggerLevel.TRACE;
serverLogLevel: NgxLoggerLevel = NgxLoggerLevel.TRACE;
serverLoggingUrl: string | undefined;
}
Now take a look at our initial implementation of app.module.ts
. Notice that there is no factory method or provider for application configuration in there. This is because the provider has already executed in main.ts
!
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule
],
providers: [
RandomImageService,
ClockService
],
bootstrap: [AppComponent]
})
export class AppModule { }
Configuring components and services
When a component or service needs configuration, it's simply injected via the token. Easy:
import { APP_CONFIG, AppConfig } from '../app.config';
...
@Injectable({
providedIn: 'root'
})
export class RandomImageService {
constructor(@Inject(APP_CONFIG) private appConfig: AppConfig, private http: HttpClient) {
}
...
}
Module configuration
Whilst components and services are easily injected with configuration, modules such as NgxLogger
(and other modules that need runtime configuration) can't use simple injection like that.
For this, we need to return to the app.module.ts
file.
In principle we can pass the entire configuration APP_CONFIG
to our NgxLogger
, but as Tim describes, we can also subdivide our configuration into smaller object tokens e.g. TOKEN_LOGGER_CONFIG
(which could be anything but in this case is a pre-defined token in our logger library, NgxLogger
.
So if we want to configure our NgxLogger
module, we need to add a specific provider for it in app.module.ts
, and provide that with just the log-specific portion of our configuration - in this case the "logger" property (which is type LoggerConfig
):
import { INGXLoggerConfig, LoggerModule, NGXLoggerConfigEngine, NgxLoggerLevel, TOKEN_LOGGER_CONFIG } from 'ngx-logger';
...
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
LoggerModule.forRoot(null,
{
configProvider:
{
provide: TOKEN_LOGGER_CONFIG,
useFactory: (config: AppConfig): INGXLoggerConfig => {
if (config.logger) {
return {
level: config.logger.browserLogLevel,
serverLogLevel: config.logger.serverLogLevel,
serverLoggingUrl: config.logger.serverLoggingUrl
}}
else {
return {
level: NgxLoggerLevel.FATAL,
serverLogLevel: NgxLoggerLevel.FATAL
}
}
},
deps: [APP_CONFIG]
}
})
],
In my simple example, the logger module is being used within the current app simply for logging, and won't be declaring any child components or services of it's own, so apart from it's own internal usage, we won't be injecting TOKEN_LOGGER_CONFIG
elsewhere, but hopefully this illustrates the principle.
Configuration
The actual configuration file itself looks pretty similar to the one used for the APP_INITIALIZER technique, with the obvious exception of an object defined for the "logger" property:
{
"env": "Dev",
"apiBaseUrl": "https://collectionapi.metmuseum.org/public/collection/v1",
"activeTheme": "solution-b",
"logger": {
"browserLogLevel": 2,
"serverLogLevel": 7,
"serverLoggingUrl": null
},
"buildRef": "1.0.0.0B-DEV"
}
At runtime of the example application, the browserLogLevel
is reflected in the highlighted text of the screenshot:
I
And that, as they say is that! In my next (and final post on Angular runtime configuration) I'll be adapting the platformBrowserDynamic
method to Angular Universal for Server-Side Rendering and Server-Side Generation.
Until then, happy coding!