DEV Community

Cover image for Dynamic environment variable
Marko Berger
Marko Berger

Posted on • Edited on

Dynamic environment variable

One build to rule them all!

Imagine that you have a multi-tenant product. Building your angular app for every client is a drag. I refuse to do the same build over and over again. Just to have different environment settings. So how to fix this.
I found a few posts online that helped me with this problem. In short, there are 2 different ways of doing this. One way is with making a window object dirty ( i don’t like this ). The other is a more angular way. So I will show you that way.
In both ways, the common denominator is a secret gem. APP_INITIALIZER.

So what is APP_INITIALIZER?

A function that will be executed when an application is initialized.

The official documentation says just that. Not very helpful. Right.
Let's start coding.
app-init.service.ts

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
import { environment } from 'src/environments/environment';
import { HttpClient } from '@angular/common/http';
import { EnvironmentService } from './environment.service';

@Injectable({
  providedIn: 'root'
})
export class AppInitService {

  /** config.js file that will contain out environment variables */
  private readonly CONFIG_URL = 'assets/config/config.js';
  private config$: Observable<any>;

  constructor(
    private http: HttpClient,
    private environmentService: EnvironmentService
    ) { }

    /**
     * Method for loading configuration
     */
    loadConfiguration(){
      if(this.config$ && environment.production){
        this.config$ = this.http.get(this.CONFIG_URL)
        .pipe(
          shareReplay(1)
        );
      } else {
        this.config$ = of(environment);
      }

      this.environmentService.setEnvironment(this.config$);
      return this.config$;
  }
}
Enter fullscreen mode Exit fullscreen mode

environment.service.ts

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class EnvironmentService {

  private dynamicEnv: any;

  constructor() { }
  /** Setter for environment variable */
  setEnvironment(env: Observable<any>){
    env.subscribe(data => this.dynamicEnv = { ...data});
  }
  /** Getter for environment variable */
  get environment() {
    return this.dynamicEnv;
  }
}

Enter fullscreen mode Exit fullscreen mode

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, APP_INITIALIZER } from '@angular/core';

import { AppComponent } from './app.component';
import { AppInitService } from './app-init.service';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    {
      // This is where the magic happens. NOTE we are returning an Observable and converting it to Promise
      // IMPORTANT It has to be a Promise 
      provide: APP_INITIALIZER,
      useFactory: (appInit: AppInitService) => () => appInit.loadConfiguration().toPromise(),
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

This is the basic setup for a dynamic environment. We are bypassing the environment and delegating the service to take care of our environment for us. In my example, we will put the configuration in the config.json file and in the environment.prod.ts we will set production to true.
This way app-init.service.ts will know what configuration to load. If we are in development it will load environment.ts configuration and if we are in production it will load config.json.
U can call API instead of loading config.json if you want.
IMPORTANT Be careful with interceptors. Your configuration will be undefined until config.json loads. So your interceptor (if you have them) need to ignore the first initial post (a post that angular need before initialization).

UPDATE

It was put to my attention that this post is unclear on how to implement this concept on multi-tenant applications. You have your one application build and you need to install it on different domains with there own settings. So just need to add your config.json to assets/config/ with your environment to it. That it.

GitHub logo markoberger / ng-dynamic-environment

Angular dynamic environment example

DynamicEnvironment

This project was generated with Angular CLI version 8.3.22.

Development server

Run ng serve for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files.

Code scaffolding

Run ng generate component component-name to generate a new component. You can also use ng generate directive|pipe|service|class|guard|interface|enum|module.

Build

Run ng build to build the project. The build artifacts will be stored in the dist/ directory. Use the --prod flag for a production build.

Running unit tests

Run ng test to execute the unit tests via Karma.

Running end-to-end tests

Run ng e2e to execute the end-to-end tests via Protractor.

Further help

To get more help on the Angular CLI use ng help or go check out the Angular CLI README.






Top comments (1)

Collapse
 
magicmatt007 profile image
magicmatt007

Thanks for your instructions Marko. Here's a beginner question:

What's the code snipnet to access the content of the JSON file in another component? E.g. the "apiUrl"?

Many thanks,
Matthias