Angular load external configuration before AppModule loads
Angular documentation has a great chapter called NgModule FAQs which contains the following section:
What if two modules provide the same service?
...
If NgModule A provides a service for token 'X' and imports an NgModule B that also provides a service for token 'X', then NgModule A's service definition "wins".
In other words, you can override OAuthModuleConfig for your library in AppModule:
main.ts
(async () => {
const response = await fetch('https://api.myjson.com/bins/lf0ns');
const config = await response.json();
environment['allowedUrls'] = config.apiBaseURL;
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
})();
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { OAuthModule, OAuthModuleConfig } from 'angular-oauth2-oidc';
import { HttpClientModule } from '@angular/common/http';
import { environment } from '../environments/environment';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule,
OAuthModule.forRoot(),
],
providers: [
{
provide: OAuthModuleConfig,
useFactory: () => ({
resourceServer: {
allowedUrls: [environment['allowedUrls']],
sendAccessToken: true
}
})
}
],
bootstrap: [AppComponent]
})
export class AppModule {}
Note that we should also use useFactory
instead of useValue
so we don't depend on when AppModule
is imported.
Another option here. @yurzui answer works, but it requires the use of useFactory
which make the code harder to understand.
useFactory
is required because Angular @NgModule
decorators will be executed as soon as the AppModule
is imported in main.ts
and so the configuration isn't loaded yet.
So I decided to load the configuration even before that by adding a script in the scripts section of angular.js
. Here's how:
src/config/load.js:
// This file is added to the scripts section of 'angular.json' so it can run before Angular bootstrap process.
// It responsability is to load app configuration from JSON files.
(() => {
const fetchSync = url => {
// The code below will log the following warning: "[Deprecation] Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help, check https://xhr.spec.whatwg.org/.",
// but since we want the configuration to be set before Angular bootstrap process, we ignore this warning.
const xhr = new XMLHttpRequest();
xhr.open('GET', url, false);
xhr.send(null);
return JSON.parse(xhr.responseText);
};
// We attach the fetched configuration to the 'window' global variable to access it later from Angular.
window.configuration = {
...fetchSync('config/config.base.json'),
...fetchSync('config/config.local.json'),
};
})();
angular.json:
// ...
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
// ...
"assets": [
// ...
"src/config/config.base.json",
"src/config/config.local.json"
],
"scripts": ["src/config/load.js"],
// ...
src/config/configuration.ts:
import get from 'lodash/get';
export class Configuration {
// We get the configuration from the 'window.configuration' property which as been set earlier by 'config/load.js'.
private static value = (window as any).configuration;
/**
* Get configuration value.
* @param path The path of the configuration value. Use '.' for nested values.
* @param defaultValue The returned value if the given path doesn't exist.
* @example
* const baseUrl = Configuration.get<string>('apis.github.baseUrl');
*/
static get<T>(path: string, defaultValue?: T): T {
return get(Configuration.value, path, defaultValue);
}
}
Then you can use:
OAuthModule.forRoot({
resourceServer: {
allowedUrls: Configuration.get('allowedUrls')
sendAccessToken: true
}
}),
See this if you have problem with lodash.