Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save fernandohu/122e88c3bcd210bbe41c608c36306db9 to your computer and use it in GitHub Desktop.
Save fernandohu/122e88c3bcd210bbe41c608c36306db9 to your computer and use it in GitHub Desktop.
Reading configuration files before application startup in Angular2 final release

Reading data before application startup in Angular 2

In this demonstration I will show you how to read data in Angular2 final release before application startup. You can use it to read configuration files like you do in other languages like Java, Python, Ruby, Php.

This is how the demonstration will load data:

a) It will read an env file named 'env.json'. This file indicates what is the current working environment. Options are: 'production' and 'development';

b) It will read a config JSON file based on what is found in env file. If env is "production", the file is 'config.production.json'. If env is "development", the file is 'config.development.json'.

All these reads will be done before Angular2 starts up the application.

It assumes you already have a working application, with a module and everything set up.

In Your Module

Open your existing module and add the following two lines to your list of providers.

import { APP_INITIALIZER } from '@angular/core';
import { AppConfig }       from './app.config';
import { HttpModule }      from '@angular/http';

...

@NgModule({
    imports: [
        ...
        HttpModule
    ],
    ...
    providers: [
        ...
        AppConfig,
        { provide: APP_INITIALIZER, useFactory: (config: AppConfig) => () => config.load(), deps: [AppConfig], multi: true }
    ],
    ...
});

The first line makes AppConfig class available to Angular2.

The second line uses APP_INITIALIZER to execute Config.load() method before application startup. The 'multi: true' is being used because an application can have more than one line of APP_INITIALIZER.

Make sure you set "HttpModule" in "imports" section if you want to make http calls using Angular2 built in Http library.

In app.config.ts

Create a class AppConfig and name the file 'app.config.ts' (you can use a name of your choice).

This is the place we will do the reading of env and config files. The data of both files will be stored in the class so we can retrieve it later.

Note that native Angular Http library is used to read the json files.

import { Inject, Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Rx';

@Injectable()
export class AppConfig {

    private config: Object = null;
    private env:    Object = null;

    constructor(private http: Http) {

    }

    /**
     * Use to get the data found in the second file (config file)
     */
    public getConfig(key: any) {
        return this.config[key];
    }

    /**
     * Use to get the data found in the first file (env file)
     */
    public getEnv(key: any) {
        return this.env[key];
    }

    /**
     * This method:
     *   a) Loads "env.json" to get the current working environment (e.g.: 'production', 'development')
     *   b) Loads "config.[env].json" to get all env's variables (e.g.: 'config.development.json')
     */
    public load() {
        return new Promise((resolve, reject) => {
            this.http.get('env.json').map( res => res.json() ).catch((error: any):any => {
                console.log('Configuration file "env.json" could not be read');
                resolve(true);
                return Observable.throw(error.json().error || 'Server error');
            }).subscribe( (envResponse) => {
                this.env = envResponse;
                let request:any = null;

                switch (envResponse.env) {
                    case 'production': {
                        request = this.http.get('config.' + envResponse.env + '.json');
                    } break;

                    case 'development': {
                        request = this.http.get('config.' + envResponse.env + '.json');
                    } break;

                    case 'default': {
                        console.error('Environment file is not set or invalid');
                        resolve(true);
                    } break;
                }

                if (request) {
                    request
                        .map( res => res.json() )
                        .catch((error: any) => {
                            console.error('Error reading ' + envResponse.env + ' configuration file');
                            resolve(error);
                            return Observable.throw(error.json().error || 'Server error');
                        })
                        .subscribe((responseData) => {
                            this.config = responseData;
                            resolve(true);
                        });
                } else {
                    console.error('Env config file "env.json" is not valid');
                    resolve(true);
                }
            });

        });
    }
}

See that we used resolve() in all scenarios because we don't want the application to crash if any problem is found in the configuration files. If you prefer, you can set error scenarios to reject().

In env.json

This is the place you will configure the current development environment. Allowed values are 'development' and 'production'.

{
    "env": "development"
}

You may add this file to .gitignore to your convenience.

In config.development.json

This is the place you will configure development config variables. You can add as many variables you want in this JSON file.

{
    "host": "localhost"
}

You may add this file to .gitignore to your convenience.

In config.production.json

This is the place you will write production config variables. You can add as many variables you want in this JSON file.

{
    "host": "112.164.12.21"
}

You may add this file to .gitignore to your convenience.

In Any Angular2 class

Example of how we read the values previously loaded from both files. In this case, we are reading the 'host' variable from config file and 'env' from the env file.

import { AppConfig } from './app.config';

export class AnyClass {
    constructor(private config: AppConfig) {
        // note that AppConfig is injected into a private property of AnyClass
    }
    
    myMethodToGetHost() {
        // will print 'localhost'
        let host:string = config.get('host');
    }
    
    myMethodToGetCurrentEnv() {
        // will print 'development'
        let env: string = config.getEnv('env');
    }
}
@ortichon
Copy link

@tkir
I've changed your solution so it can work on arrays instead of . separated strings:

Config Service:

  public getConfig(key: any) {
    if (!Array.isArray(key)) {
      return this.config[key];
    }
    let res: any = this.config;
    key.forEach(k => res = res[k]);
    return res;
  }

Component:

this.config.getConfig('host'); // for root item
this.config.getConfig(['parent', 'child']); // for nested

@Someone92
You should use this.config.getConfig('host')

@smitelij
Copy link

There is a much simpler way if you're just looking to set global environment properties (host address for example):

http://tattoocoder.com/angular-cli-using-the-environment-option/

@saadiadel
Copy link

when i did as your example, i got this error : Configuration file "env.json" could not be read.
in fact i put this file in same level as .gitignore file in my directory project.
Thank you for your help.

@wethinkagile
Copy link

wethinkagile commented Oct 8, 2017

@smitelij Angular-CLI Environments are before Build, this example are before startup aka runtime! Also your links is horribly outdated, there is official support on GitHub for Build-Time Environments.

@wethinkagile
Copy link

Unhandled Promise rejection: Cannot read property 'host' of null ; Zone: <root> ; Task: Promise.then ; Value: TypeError: Cannot read property 'host' of null
    at AppConfig.webpackJsonp.../../../../../src/app/app.config.ts.AppConfig.getConfig (app.config.ts:19)

@JoseMejia96
Copy link

JoseMejia96 commented Nov 3, 2017

Yes, i am also trying to use APP_INITIALIZER to load config file in tests but its not working. How did you manually call config.load in beforeEach().
I am getting an error "Cannot read property load of undefined"

I have the same issue with unit test. Did someone find the solution?
@scottseeker How did you call config.load ? could you please give us an example?

@Priyesha
Copy link

I am able to get the external json file in my unit test by mocking the AppConfig. Here is what i have done -
First get the JSON file in your unit test : let mockConfig = require('../../assets/config.json');

Then mock the getConfig function to access the keys and values like this :
class MockAppConfig {
getConfig(key) {
return mockConfig[key];
}
}

You can also mock other functions whichever is required.
@Chuvisco88, @scottseeker, @Carpediemy, @ridj87 I am able to run my unit test with this. You can also try this if it helps anyone.

@okta123
Copy link

okta123 commented Nov 27, 2017

@Priyesha, could you please elaborate your example with the mockConfig, maybe a complete example, thank you

@xeax
Copy link

xeax commented Nov 28, 2017

Possible error:
case 'default':
must be:
default:

@deejbee
Copy link

deejbee commented Dec 1, 2017

In my situation i'm trying to apply the contents of the config file I have loaded to the standard environments/environment import that has already been loaded (with blank fields):

public load() {

    this.http.get('assets/config.json').toPromise().then(allConfigs => {

        //lookup the config using the host
        let specificConfig = allConfigs.json().find((item) => item.host === window.location.hostname);

        if (specificConfig === undefined) {
            //try loading the one marked as isDefault (this should be the production config)
            let defaultProductionConfig = allConfigs.json().find((item) => item.isDefault === true);

            if (defaultProductionConfig !== undefined)
                specificConfig = defaultProductionConfig;
        }
        
        //now override environment, one property at a time.
        //We can't just copy the whole object to ennvironment object since its defaint as Constant
        Object.assign(environment, specificConfig);

    });    
}

The problem is when I try to read the environment.someThing setting from a service more than one service. The first service seems to have the correct value in environment.someThing but when I try to use another service, someThing is blank again.

Anyone else tried this approach of overwriting the standard `envionments/environment' in this way?

@rpeyfuss
Copy link

rpeyfuss commented Jan 26, 2018

If you are using Angular 5, the AppConfig needs some corrections for it to work:

  1. Http injection in the constructor needs to be changed to HttpClient which is imported from angular/common/http;
  2. change the this.http.get... to this.http.get < any >('env.json')
  3. remove the map function after the http.get call
  4. in the if (request) remove the map function
  5. the json files need to be in the directory /src/app/config

@bhupal4all
Copy link

bhupal4all commented Feb 16, 2018

I have created the Config service as suggested, Config service is loaded during bootstrap (add Log Config service). But Page is coming as Empty. If remove APP_INITIALIZER line, then the page is loading.

Did anyone face this problem?

I got the solution, Silly me. I forgot to set resolve(true) at the end of the Config Service.

@BBaysinger
Copy link

I'm getting "Configuration file "env.json" could not be read", but I'm not even seeing an HTTP request for it. Huh?

@rubenkuipers
Copy link

rubenkuipers commented Apr 16, 2018

@anyones and @geekrumper I'm struggling with the same problem of variables being undefined in the constructor of other injectable services. Have you already found a better solution for this?

@m00zi
Copy link

m00zi commented May 25, 2018

add @Injectable({ providedIn: 'root' }) in the service itself, and no need to add it in app.module.ts

@lucasklaassen
Copy link

While using this alongside HTTP_INTERCEPTORS where in you are using the configuration inside of the HTTP Interceptor it appears that the interceptor loads before this and does not have access to the "environment variables" Has anyone found a workaround for this? @skgyan

@shantanu-dutta
Copy link

I have modified the app.config.ts code a bit to make it work in Angular 6 using RxJS 6. I have created an repo for this. To include the json files using ng serve, see angular.json file. For app.config.ts, look inside src/app folder. The service call to AppConfig can be found inside src/app/counter/order-sheet.

@schmorrison
Copy link

I was having an issue in the following example, where the AngularFireModule.initializeApp(config) was being called with an undefined config object. It would seem the imports are being executed before the factory provided to 'APP_INITIALIZER' was being is resolved.

  @NgModule({
    declarations: [
      AppComponent,
    ],
    imports: [
      ...,
      AngularFireModule.initializeApp(firebaseConfig),
    ],
    providers: [
      ConfigService,
      {
        deps: [ConfigService],
        multi: true,
        provide: APP_INITIALIZER,
        useFactory: configProvider,
      },
    ],
    bootstrap: [AppComponent]
  })
  export class AppModule { }

This resulted in AngularFireModule.initializeApp(firebaseConfig) returning an error related to 'not providing projectId'.
What I did instead was add a script in head of the index.html that would be parsed before the Angular files.

<head>
.........

<script type="text/javascript" src="/assets/config/firebase.config.js"></script>

.............
</head>
<body>
  <app-root></app-root>
<script type="text/javascript" src="runtime.js"></script><script type="text/javascript" src="polyfills.js"></script><script type="text/javascript" src="styles.js"></script><script type="text/javascript" src="vendor.js"></script><script type="text/javascript" src="main.js"></script></body>
</html>

And inside the firebase.config.js:

var firebaseConfig = {
    firebase: {
        apiKey: "<API KEY>",
        authDomain: "<AUTH DOMAIN>",
        databaseURL: "<DATABASE URL>",
        projectId: "<PROJECT ID>",
        storageBucket: "<STORAGE BUCKET>",
        messagingSenderId: "<SENDER ID>"
    }
};

Then finally in my NgModule:

declare var firebaseConfig: FirebaseConfig;
@NgModule({
.......
imports: [
........
     AngularfireModule.initializeApp(firebaseConfig),
........
],
.......
})

@pchiarato
Copy link

pchiarato commented Feb 25, 2019

@skgyan @lucasklaassen I'm getting the same error you were, how did you fix the cyclic dependency issue?

Thanks.

@jeevanandamjobs
Copy link

jeevanandamjobs commented Apr 1, 2019

This works for me. Modified version, making synchronous HTTP request

export class AppConfig {
static Settings: IAppConfig;
constructor() {

}
load() {
const jsonFile = 'assets/config/config.json';
return new Promise((resolve, reject) => {
var response = this.GetSync(jsonFile);
if (response && response != null) {
AppConfig.Settings = JSON.parse(response);
resolve(true);
}
else
reject(Could not load file '${jsonFile}': ${JSON.stringify(response)});
});
}

public GetSync(url: string): any {
const xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", url, false);
xmlhttp.overrideMimeType('application/json');

xmlhttp.send();
if (xmlhttp.status == 200) {
  return xmlhttp.responseText;
}
else {
  return null;
}

}
}

providers: [
AppConfig,
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
deps: [AppConfig],
multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule {

}

export class AppComponent implements OnInit {

constructor(

) {

}

async ngOnInit() {

 if (!AppConfig.Settings.AppConfig.production) {
 
}

@kavinda1995
Copy link

kavinda1995 commented May 16, 2019

@skygan @lucasklaassen @pchiarato and anyone who using using http interceptor.

Follow this way.
https://stackoverflow.com/a/49013534/6686446

The way this is happening is Http Interceptors sit between the HttpClient interface and the HttpBackend. So creating a HttpClient using HttpBackend is bypassing the http interceptor

@dabbid
Copy link

dabbid commented Aug 16, 2019

@schmorrison, I was stuck on the same problem (I've got several configurable modules), I found an alternative way to load a configuration file before bootstrap:

// src/shared/config/config.model.ts
export interface ExtendedWindow extends Window {
  appConfig: Config;
}

interface Config {
  someProperty: string;
  someOtherProperty: boolean;
  modules: any;
}
// main.ts
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { ExtendedWindow, Config } from './src/shared/config/config.model';

fetch('./config.json')
  .then((response: Response) => response.json())
  .then((config: Config) => {
    (window as ExtendedWindow).appConfig = config;
    platformBrowserDynamic()
      .bootstrapModule(AppModule, {})
      .catch(err => console.log(err));
  });
// src/shared/config/config.provider.ts
import { InjectionToken } from '@angular/core';
import { ExtendedWindow, Config } from './config.model';

export const APP_CONFIG: InjectionToken<Config> = new InjectionToken<Config>('AppConfig');

const appConfigFactory = (): Config => (window as ExtendedWindow).appConfig;

export const appConfigProvider: any = { provide: APP_CONFIG, useFactory: appConfigFactory };

export const configurableModuleFactory = (name: string) => (appConfig: Config): any => appConfig.modules[name];
// src/app.module.ts
import { NgModule } from '@angular/core';
// both configurable modules exposes a configuration injection token
// so I can define a factory provider for these (https://angular.io/guide/dependency-injection-providers#factory-providers)
import { FooConfigurableModule, FOO_CONFIGURABLE_MODULE_CONFIGURATION } from '@scope/foo';
import { BarConfigurableModule, BAR_CONFIGURABLE_MODULE_CONFIGURATION } from '@scope/bar';

import { AppComponent } from './app.component';
import { APP_CONFIG, appConfigProvider, configurableModuleFactory } from './shared/config/config.provider';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    FooConfigurableModule.withConfig(null),
    BarConfigurableModule.withConfig(null),
  ],
  providers: [
    appConfigProvider,
    {
      provide: FOO_CONFIGURABLE_MODULE_CONFIGURATION,
      useFactory: configurableModuleFactory('foo'),
      deps: [APP_CONFIG],
    },
    {
      provide: BAR_CONFIGURABLE_MODULE_CONFIGURATION,
      useFactory: configurableModuleFactory('bar'),
      deps: [APP_CONFIG],
    },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

@ayyash
Copy link

ayyash commented Aug 20, 2019

@dabbid oh wow, may be we're over killing it? may be environment rebuild isn't such a bad idea after all :)

@dabbid
Copy link

dabbid commented Aug 20, 2019

Believe me @ayyash, if I could have done without runtime config, I'd be happy... But it's imposed by my company's context.

@ayyash
Copy link

ayyash commented Aug 20, 2019

@schmorrison that is a very easy way to do it, unfortunately, it doesn't work on ssr. Unless you rewrite the firebaseConfig in server.ts

@vitaliidasaev
Copy link

vitaliidasaev commented Jun 18, 2020

There is example here:
How to initialize msal configurations using APP_INITIALIZER
AzureAD/microsoft-authentication-library-for-js#1403

@sforsandeep
Copy link

Sorry to ask a dumb question... because I am just a beginner in Angular. Is it same in Angular 11 or any easier solution already integrated ??

@ayyash
Copy link

ayyash commented May 17, 2021

I don't think there is anything new to how to initialize config from server in angular 11, but then again, may be i curled into a comfort zone and didn't try

@mohitp22
Copy link

I am also reading config.json in my web component to get API endpoint since path of config json is relative it’s taking path of application using my web component
I already tried by passing @input variable to webcomponent but since it’s changing the way currently other application using it I can’t use this approach

need suggestions
Thanks

@ayyash
Copy link

ayyash commented Jul 24, 2021

By the way, in Angular 12, what changed is that now you no longer need a promise, you can return an observable directly from load function

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment