6

I am developing an angular application which is a product and deployed to multiple clients. Clients will take this angular application and host it in their servers. So the Services' url will be different for the clients. I have seen solutions that talk about dev, prod. But that will not be applicable in this case. I am looking for a config file that is similar to .net config file. I have created a separate app.config.ts file but that gets built when I do ng build. I am looking for a solution where I can deploy the app to any client with out them to build it again.

import { InjectionToken } from "@angular/core";

export let APP_CONFIG = new InjectionToken("app.config");

export interface IAppConfig {
    apiEndpoint: string;
}

export const AppConfig: IAppConfig = {    
    apiEndpoint: "http://88.87.86.999/"
};
indra257
  • 66
  • 3
  • 24
  • 50
  • Will the angular app be hosted/served from the same server and port as the Services? Or will they be different? – Simply Ged Mar 29 '18 at 14:54
  • They will be different. If it is same, then I could get the hosted url easily.. :) – indra257 Mar 29 '18 at 15:19
  • The problem is the Angular app will run in the browser on the client so any config needs to be sent, or stored, on every client. I'm not sure how you would do this? Unfortunately the server address needs to be know at build time. The only thing I can think is deploy it as a Docker container and let the customer provide the server address as a parameter when starting the container? – Simply Ged Mar 29 '18 at 15:38

2 Answers2

14

What you can do it host the config file in json format in the assets folder and retrieve it dynamically. You need to make sure that you retrieve it before the app starts, that way it can be available in components/services when they need it. For that, you can use the APP_INITIALIZER Token

Step #1: put your json configuration files under src/assets/config/conf.json (json format, not ts format since there is no TS compiler in prod mode)

Step #2: Add a new config service

import {Inject, Injectable} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {Observable} from 'rxjs/Rx';
import {environment} from "../../environments/environment";

/**
 * Declaration of config class
 */
export class AppConfig
{
//Your properties here
  readonly apiEndpoint: string;
}

/**
 * Global variable containing actual config to use. Initialised via ajax call
 */
export let APP_CONFIG: AppConfig;

/**
 * Service in charge of dynamically initialising configuration
 */
@Injectable()
export class AppConfigService
{

  constructor(private http: HttpClient)
  {
  }

  public load()
  {
    return new Promise((resolve, reject) => {

      this.http.get('/assets/config/config.json').catch((error: any): any => {
        reject(true);
        return Observable.throw('Server error');
      }).subscribe((envResponse :any) => {
        let t = new AppConfig();
        //Modify envResponse here if needed (e.g. to ajust parameters for https,...)
        APP_CONFIG = Object.assign(t, envResponse);
        resolve(true);
      });

    });
  }
}

Step #3: In your main module, add this before declaring the module

/**
* Exported function so that it works with AOT
* @param {AppConfigService} configService
* @returns {Function}
*/
export function loadConfigService(configService: AppConfigService): Function 

{
  return () => { return configService.load() }; 
}

Step #4: Modify the module providers to add this

providers: [
  …

  AppConfigService,
  { provide: APP_INITIALIZER, useFactory: loadConfigService , deps: [AppConfigService], multi: true },


],

Step 5: In your code, use the config

import {APP_CONFIG} from "../services/app-config.service";

//…
return APP_CONFIG.configXXX;

Now, you can ship the app to multiple clients; each client just need to have theyr specific parameter in conf.json file

David
  • 33,444
  • 11
  • 80
  • 118
  • The only problem with this is, Client's users can easily access the conf.json file by just doing http:url/assets/config.json.. – indra257 Mar 29 '18 at 15:23
  • How is it a problem? Even if config is packaged completely into the build, it's still accessible as in the end it's just client JS code. Maybe retrieve an encrypted config in json file to make it slightly harder to see the values, but I'm not sure there is no point – David Mar 29 '18 at 16:18
  • would this be better than saving the config properties in an environments file? – MartaGalve Jul 04 '18 at 10:42
  • @bubble If it's saved in environments files, then you have to have one build per client. With this approach, you can have the same build that you deploy to each client, with just the config file that changes – David Jul 04 '18 at 12:04
  • 1
    thanks @David for this detailed explanation. however we encountered issues with using the AppConfigService while bootstrapping our app since some of the root modules require passing the settings at bootstrap time and the service only runs after bootstrapping. e.g AuthModule.forRoot(appConfig) – Yinon Aug 31 '18 at 07:32
  • What value needs to provide in `source` object in `angular-cli.ts`? – Ramesh Rajendran Oct 05 '18 at 19:07
  • Code is missing a quote in the http.get call – Norbert Huurnink Oct 10 '18 at 09:22
  • https://angular.io/api/core/APP_INITIALIZER A function that will be executed when an application is initialized. – Dipendu Paul Jun 02 '19 at 13:37
  • @David how would you unit test files that use this though? I'm having trouble as it's creating a global object rather than using DI as Services are designed to do in Angular, in fact it's probably better to do it the standard Angular way, thanks. – Christopher Grigg Jun 12 '19 at 06:42
8

For the config to be loaded initially and use it even when bootstrapping the AppModule, we used this simple solution in main.ts.

notice we queued the

platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));

inside an async function that waits for the fetching of a config file.

(async () => {

  // console.time()
  const response = await fetch('./assets/config/conf.json');
  const json = await response.json();
  Object.entries(json).forEach(([key, value]) => {
    environment[key] = value;
  });
  // console.timeEnd()

  platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));

})();

the config file is located in the assets and is replaced accordingly by the deployment job.

meaning we have environment based config:

  • config.json
  • config.qa.json
  • config.prod.json

time of fetching on for us takes about 40 milliseconds (practically nothing. uncomment the console.time to check estimations in your own projects). I doubt it will consume too much time on any project as it is fetched from a local file. good luck!

Yinon
  • 697
  • 7
  • 13
  • Small addition: if you need to pass those environment variables on to a provider in the AppModule, you can use `useFactory`. More details [here](https://stackoverflow.com/a/54473123/4337765) – Dennis Ameling Feb 01 '19 at 12:00
  • I also had variables i needed to inject even before bootstrapping. So thanks a lot for this solution. Furthermore, I like this approach as it still makes it possible to run ng build --prod etc.. – Anonymous Sep 04 '19 at 19:25