Prior to the loading of the AppModule in Angular, make sure to load

Consider the following situation (Angular v7):

  1. Retrieve configuration settings (API server URL and Auth server URL) from an external source (JSON), prior to loading the AppModule
  2. Provide configuration to the AppModule (OAuth2 module)
  3. Compile the application with AOT

The crucial step is number 2, and it appears as follows:

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

What I have attempted so far:

  • An approach using APP_INITIALIZER. This method fails because the OAuthModule.forRoot() is triggered before the APP_INITIALIZER can fetch the external configuration JSON.
  • Loading the configuration via an async function in the main.ts into the Angular environment variables, then bootstrapping the AppModule. However, this also does not work due to the
    import { AppModule } from './app/app.module';
    statement in main.ts, which causes the AppModule to load and trigger OAuthModule.forRoot() before the external configuration is fetched (this comment confirms this behavior).
  • Dynamically loading the AppModule in main.ts, without the import statement on top. Here is the StackBlitz example provided in that comment. It does work, but 1) it disrupts lazy loading
    WARNING in Lazy routes discovery is not enabled.
    and 2) it is incompatible with AOT compiling. Nonetheless, it comes very close to meeting my requirements.

I am interested in knowing if there is another approach to ensure external configuration is loaded prior to the AppModule being loaded.

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

Answer №1

Within the Angular documentation, there is a valuable section in NgModule FAQs discussing the scenario when two modules offer the same service:

What if two modules provide the same service?

...

If NgModule A provides a service for token 'X' and imports another NgModule B that also provides a service for token 'X', the service definition from NgModule A takes precedence.

This essentially means that you can customize OAuthModuleConfig within your library's 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 {}

It is important to note the usage of useFactory over useValue to avoid any dependency issues related to the import timing of AppModule.

Answer №2

There is another approach available here. While @yurzui's solution works, it relies on the use of useFactory, which can make the code more complex and difficult to comprehend.

The necessity for useFactory arises because Angular executes the @NgModule decorators as soon as the AppModule is imported in main.ts, before the configuration is loaded. To address this issue, I chose to load the configuration even earlier by incorporating a script in the scripts section of angular.js. Here's how:

src/config/load.js:

// This file is included in the scripts section of 'angular.json' to run prior to Angular bootstrap process.
// Its purpose is to fetch application configuration from JSON files.
(() => {
  const fetchSync = url => {
    // The following code may trigger a 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/.",
    // However, we ignore this warning as we need the configuration set before Angular bootstrap process.
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, false);
    xhr.send(null);
    return JSON.parse(xhr.responseText);
  };

  // Configuration fetched is attached to the global 'window' variable for later access 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 {
  // Retrieve the configuration using the 'window.configuration' property set earlier by 'config/load.js'.
  private static value = (window as any).configuration;

  /**
   * Obtain configuration value.
   * @param path The path of the configuration value. Use '.' for nested values.
   * @param defaultValue Value returned if the specified path does not exist.
   * @example
   * const baseUrl = Configuration.get<string>('apis.github.baseUrl');
   */
  static get<T>(path: string, defaultValue?: T): T {
    return get(Configuration.value, path, defaultValue);
  }
}

Now you can utilize it like this:

OAuthModule.forRoot({
  resourceServer: {
    allowedUrls: Configuration.get('allowedUrls')
    sendAccessToken: true
  }
}),

If you encounter issues with lodash, refer to this link.

Answer №3

Aside from @yurzui's response, if you decide to run this in Ahead-Of-Time compilation (e.g. using ng build --prod), you may encounter

ERROR in Error found during template compilation of 'AppModule' Function expressions are not accepted in decorators within 'AuthModule' The issue lies within 'AuthModule' at src\app\core\auth.module.ts(29,23) It is advised to convert the function expression into an exported function.

Thus, we need to 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 {}

Answer №4

I wanted to share a solution that I successfully implemented in a project of mine a few months back.

    @NgModule({
      imports: [
        ...
        OAuthModule.forRoot({
          resourceServer: {
            allowedUrls: GetApiUrl(), // <== This value is loaded from an external endpoint (JSON)
            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 a function similar to GetApiUrl().
  2. Retrieve the Json file synchronously using XMLHttpRequest.
  3. Extract the api url from the response object.
  4. Use the GetApiUrl() function whenever you need to insert the api url.

Note: If using an external json file, provide the complete URL to the file. In my case, the json file was within the project so I used its path directly.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Issue with redirecting users back to the login page after accepting Keycloak required actions due to browser specific terms and conditions

My Keycloak version is 9.0 and my app's front end is built using Angular 8. I have enabled Terms and Conditions while setting the defaults to true. https://i.sstatic.net/URXM1.png I have customized the t&c page as desired, and it appears when u ...

How to implement Dragula and Angular to enable draggable elements to be dropped in any position within their container?

I am in the process of developing a web application using Angular, which incorporates dragula. While the current drag and drop functionality works well for moving items within a designated "bag", I am seeking to enhance it with more advanced features. Is ...

Encountering an error in Angular 13 while trying to retrieve the innerHTML of an element with the ID 'msg'

Seeking assistance to solve this issue. I have exhausted all possible methods to contact the data. (method) Document.getElementById(elementId: string): HTMLElement | null Retrieves a reference to the first object that matches the specified ID attribute v ...

Which RxJS operators necessitate unsubscription?

It can be confusing to know which operators in RxJS must be unsubscribed from to prevent subscription leaks. Some, like forkJoin, complete automatically, while others, such as combineLatest, never complete. Is there a comprehensive list or guideline availa ...

Issue with Angular framework causing UI not to update when state changes occur

UPDATE: Deciding to close this discussion for a few reasons. First off, I believe this is essentially a duplicate topic. Second, I suspect that my specific scenario may be too artificial to benefit from the solution provided. I plan on studying the data mo ...

Is it possible to loop through a subset of a collection using *ngFor?

Is it possible to iterate through a specific range of elements in a collection using *ngFor? For instance, I have a group of checkboxes with their form control name and label specified as follows: [{id: 'c1', label: 'C1'}, ...] Assum ...

What is the best way to retrieve key/value pairs from a JSON object?

After making a call to an external service, I receive a domain object in return: var domainObject = responseObject.json(); This JSON object can then be easily accessed to retrieve specific properties. For example: var users = domainObject.Users The &ap ...

I need assistance with the following issue: the type 'Observable<ArrayBuffer>' cannot be assigned to the type 'Observable<User[]>. Can anyone provide a solution for this problem?

Encountered an issue with the error message: Type 'Observable' cannot be assigned to type 'Observable<User[]>' while working on setting up Angular service.ts for implementing HTTP requests. ...

What is the process for converting language json files into different languages?

I'm currently using ngx-translate to localize an Angular app. With over 20 languages that need translation, I am in search of a tool that can efficiently translate the language json files. While I did come across a helpful website, it proved to be ti ...

Inquiry regarding the implementation of DTO within a service layer parameter

I have a query regarding the choice of service layer to use. // 1 export class SomeService{ async create(dto:CreateSomeDto) {} } or // 2 export class SomeService{ async create(title: string, content: string) {} } It appears that most individuals opt ...

The issue I'm facing with the mongoose schema.method is that the TypeScript error TS2339 is showing up, stating that the property 'myMethod' does not exist on type 'Model<MyModelI>'

I am looking to integrate mongoose with TypeScript and also want to enhance Model functionality by adding a new method. However, when I try to transpile the file using tsc, I encounter the following error: spec/db/models/match/matchModelSpec.ts(47,36): e ...

Storing Global Types in Angular: A Better Approach

Imagine I possess certain universally applicable enums, interfaces, or extensive classes. For example: export interface LogMessage { code: number; message: string; } Where would be the optimal location to store them? What is considered the best pr ...

What is the best way to adjust the Directions route Polyline to the edge of the map canvas?

I am currently working on a project that involves direction mapping, and I need the directions to be displayed on the right side of the panel when rendered. Here is what I am currently getting: https://i.sstatic.net/AMgA8.png However, I do not want the di ...

Warning: TypeScript linter alert - the no-unused-variable rule is now outdated; however, I do not have this configuration enabled

After 3 long months, I came across a warning in a project that was being refreshed today. The warning says "no-unused-variable is deprecated. Since TypeScript 2.9. Please use the built-in compiler checks instead." Oddly enough, my tsconfig.json file do ...

What is the functionality of "classnames" in cases where there are multiple classes sharing the same name?

Currently, I am reviewing some code that utilizes the npm utility classnames found at https://www.npmjs.com/package/classnames. Within the scss file, there are various classes with identical names (small and large), as shown below: .buttonA { &.smal ...

Update to Material-UI 4.8.1 - Is there a different method for defining the `component` property now?

Note: Please note that there was a bug in version 4.8.x which caused this issue. To resolve it, make sure to upgrade to version 4.9.0 or above. In the initial version 4.8.0, the following code would compile and run smoothly: <DialogContent> {/* us ...

Nested formArrays within formArrays in Angular 4

I've been working on implementing a FormArray inside another FormArray, but it doesn't seem to be functioning correctly. I also tried the solution provided in the link below, but it didn't work for me. How to get FormArrayName when the Form ...

Improving Accessibility in Angular 10 by Confining Focus within Modals

I'm currently working on updating my existing modal popup to ensure ADA compliance. I am using a user-defined handleTabKeyFocus() function to manage the keyboard Tab focus within the modal container. However, the querySelector selector always returns ...

Creating a Cordova application from the ground up, evaluating the options of using ngCordova, Ionic framework, and

With the goal of creating a multiplatform Cordova app for Android, iOS, and Windows, I have been exploring different approaches. My plan is to build an application with 4 or 5 features focused on service consumption (listing, adding, and editing items) wh ...

The checkbox in Angular 2 is failing to register as checked when the "checked

Code Snippet Displaying Radio Buttons in home.ts File <div class="col-xs-12 col-sm-3 col-md-3 radios"> <input [(ngModel)]="SEARCH_TYPE" [checked]="true" type="radio" name="SEARCH_TYPE" placeholder="Starts With" title="Starts With" tab-index= ...