Consider the following scenario (Angular v7):
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.
In addition to @yurzui's answer, if you try this in AOT (e.g. ng build --prod
), you will get
ERROR in Error during template compile of 'AppModule' Function expressions are not supported in decorators in 'AuthModule' 'AuthModule' contains the error at src\app\core\auth.module.ts(29,23) Consider changing the function expression into an exported function.
so we create an exported function for the factory:
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';
export function oAuthConfigFactory() : OAuthModuleConfig {
return {
resourceServer: {
allowedUrls: [environment.servers.apiServer],
sendAccessToken: true
}
}
}
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule,
OAuthModule.forRoot(),
],
providers: [
{
provide: OAuthModuleConfig,
useFactory: oAuthConfigFactory
}
],
bootstrap: [AppComponent]
})
export class AppModule {}
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.