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$;
}
}
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;
}
}
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 { }
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.
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)
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