About my blog

I write about the technical and non-technical aspects of software development

How it works

Microsoft ASP.NETASP.Net
BlogEngine.NET BlogEngine.NET
Azure DevOpsAzure DevOps

Contact info

 Email
 Contact

Follow me

Prod-20240407.1

Angular platformBrowserDynamic method

Scenarios where configuration will be injected only into services and components work well with the APP_INITIALIZER technique. If however we need to configure an application consisting of multiple modules, with each requiring configuration, even partial configuration, then an alternative is necessary.

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!


You Might Also Like


Would you like to share your thoughts?

Your email address will not be published. Required fields are marked *

Comments are closed