Dennis Ameling
Dennis Ameling

Reputation: 737

Angular load external configuration before AppModule loads

Consider the following scenario (Angular v7):

  1. Load configuration parameters (API server URL and Auth server URL) from an external endpoint (JSON), before the AppModule is loaded
  2. Pass configuration to AppModule (OAuth2 module)
  3. Compile the app with AOT

Point 2 is key here, and looks like this:

@NgModule({
  imports: [
    ...
    OAuthModule.forRoot({
      resourceServer: {
        allowedUrls: [API_SERVER_URL], // <== we need to set the value that we loaded from the external endpoint (JSON) here
        sendAccessToken: true
      }
    }),
    ...
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule { }

What I've tried to far:

Curious to hear if someone is aware of another method to get external configuration loaded before the AppModule loads.

StackBlitz for option 3 (Load the AppModule dynamically): https://stackblitz.com/edit/angular-n8hdty

Upvotes: 13

Views: 11554

Answers (4)

Naveed Ullah Khan
Naveed Ullah Khan

Reputation: 69

I am suggesting you this simple solution as I have implemented this few months ago in my project.

    @NgModule({
      imports: [
        ...
        OAuthModule.forRoot({
          resourceServer: {
            allowedUrls: GetApiUrl(), // <== we need to set the value that we loaded from the external endpoint (JSON) here
            sendAccessToken: true
          }
        }),
        ...
      ],
      declarations: [AppComponent],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    export function GetApiUrl() {
      const jsonFile = `assets/config.dev.json`; //path to config file
      var request = new XMLHttpRequest();
      request.open('GET', jsonFile, false);  // get app settings
      request.send(null);
      const response = JSON.parse(request.responseText);
      return response.ApiUrl;
  }
  1. Step 1, Create function as I did with the name GetApiUrl().
  2. Get Json file synchronously using XMLHttpRequest.
  3. Return api url from the response object.
  4. Call the GetApiUrl() function where you want to put api url.

Note: For external json file, put the complete url to the file. In my example json file exist in my own project so I just put its path to the file.

Upvotes: 2

Maxime G&#233;linas
Maxime G&#233;linas

Reputation: 2330

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.

Upvotes: 6

Dennis Ameling
Dennis Ameling

Reputation: 737

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 {}

Upvotes: 5

yurzui
yurzui

Reputation: 214175

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.

Upvotes: 21

Related Questions