Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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');
    }
}
@patrickkee

This comment has been minimized.

Copy link

@patrickkee patrickkee commented Nov 5, 2016

It seems this only works for fetching data funfrom the backend over http. What about loading data from a global js variable in the browser?

@azulay7

This comment has been minimized.

Copy link

@azulay7 azulay7 commented Nov 29, 2016

Hi,
does anyone succeed to install it on angular-cli project?

@skgyan

This comment has been minimized.

Copy link

@skgyan skgyan commented Nov 30, 2016

I am using http interceptor and while using this approach getting cyclic dependency. do you have any reference for the same?

@aryasunny

This comment has been minimized.

Copy link

@aryasunny aryasunny commented Dec 1, 2016

How would your load function in app.config.ts would come to know that where your env.json is located.
Ideally it should not be part of my source code. How would you mention that this file is located at some location external to the source code ??

In-fact I get compilation errors on switch (envResponse.env) because code is not able to find this env.json.

@jolmos

This comment has been minimized.

Copy link

@jolmos jolmos commented Dec 2, 2016

@aryasunny: Your compile errors are caused by the compiler knowing nothing about the type of env. Add:

class EnvData {
   env: string;
}

Over
@Injectable()

and specify that envResponse is an EnvData with:
subscribe( (envResponse: EnvData)
instead of
subscribe( (envResponse)

@kvotheyr

This comment has been minimized.

Copy link

@kvotheyr kvotheyr commented Dec 16, 2016

Thanks for great article. I'm stuck with this error

Unhandled Promise rejection: No provider for AppConfig! 

Any help would be appreciated.

@armno

This comment has been minimized.

Copy link

@armno armno commented Jan 17, 2017

@Carpediemy I got the same error. It was that I forgot to add AppConfig itself into providers list.

@NgModule({
  ...
  providers: [
    ...
    AppConfig, // <---- this line
    { provide: APP_INITIALIZER, useFactory: (config: AppConfig) => () => config.load(), deps: [AppConfig], multi: true }
    ...
  ]
});
@PooperPig

This comment has been minimized.

Copy link

@PooperPig PooperPig commented Jan 18, 2017

I get an error
ERROR in Error encountered resolving symbol values statically. Function calls are not supported. Consider replacing the function or lambda with a reference to an exported function (position 33:46 in the original .ts file), resolving symbol AppModule in /app.module.ts

Help!

@ddramone

This comment has been minimized.

Copy link

@ddramone ddramone commented Jan 18, 2017

@PooperPig functions and lambdas are not supported anymore in useFactory. You have to rewrite your code:

// Add this function
function initConfig(config: AppConfig){
 return () => config.load() 
}

@NgModule({
  ...
  providers: [
    ...
    AppConfig, 
    { provide: APP_INITIALIZER,
       useFactory: initConfig, // And use it here
       deps: [AppConfig], 
       multi: true } 
    ...
  ]
});
@Antarian

This comment has been minimized.

Copy link

@Antarian Antarian commented Jan 22, 2017

Every envResponse.env after assigning this.env = envResponse; should be changed to this.getEnv('env') its better than create new EnvData class I think.

I also added in second subscribe type any. Instead of .subscribe((responseData) => { I have .subscribe((responseData: any) => {

Spent one day to get it up and running, learn much about http in Angular2. But still really useful gist. Thanks

@dbautistav

This comment has been minimized.

Copy link

@dbautistav dbautistav commented Jan 25, 2017

I'm stuck with a similar case using this strategy. I want to use the fetched configuration to inject it into an external library which I added into module's imports. How can I achieve that?

This is my AppModule:

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

and this my ConfigService:

export function configProvider(cs: ConfigService) {
  return () => cs.load();
}

@Injectable()
export class ConfigService {
  public firebaseConfig;

  constructor(private http: Http) { }

  load(): Promise<FirebaseConfig> {
    const url: string = "https://somewhere.over/the/rainbow/firebaseConfigKeys.json";
    // I've updated this part of the code and actually returning the intended promise, but still doesn't work    :(
    const promise = this.http.get(url).map(res => res.json()).toPromise();
    promise.then(keys => { this.firebaseConfig = keys; });
    return promise;
  }
}

Any ideas? Thanks in advance.

@anyones

This comment has been minimized.

Copy link

@anyones anyones commented Jan 26, 2017

Although my AppConfig instance is successfully populated upon App initialization via the APP_INITIALIZER approach you suggested (as verified in the debugger, the response is correctly retrieved and config['host'] is defined), I'm not getting any data in AnyClass. The AppConfig instance I'm getting from the injector is empty, i.e. populated with the default values (null in this case). Any idea what I'm doing wrong? Also, could you please explain how concrete object allocation is handled by the injector? In your example, how does the injector know that you want the concrete object you loaded via APP_INITIALIZER injected into your AnyClass constructor (instead of just any new object of that class), where is the connection?
Thanks!

@elendil-software

This comment has been minimized.

Copy link

@elendil-software elendil-software commented Feb 2, 2017

@anyones
I noticed the same problem. I found that when I loaded pages with a component/service that need the appConfig, it was not set.
My mistake was :

constructor(private _http: Http, config: AppConfig) { this._apiEndpoint = config.apiEndpoint; }

With this code, the AppConfig was used before to be fully initialized, and this._apiEndpoint set with an empty default value.

I replaced this code with :

constructor(private _http: Http, private config: AppConfig) { }

And I'm just using this.config.apiEndpoint where I need it.

hope this helps.

@kvotheyr

This comment has been minimized.

Copy link

@kvotheyr kvotheyr commented Feb 9, 2017

I'm able to get this working however my unit tests fail with 'Cannot read property of null'.
I have added the provider to test as well

beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ LoginComponent ],
      imports: [ReactiveFormsModule, RouterTestingModule, HttpModule],
      providers: [AuthService,
        SettingsService,
        AppConfig,
        {
            provide: APP_INITIALIZER,
            useFactory: initConfig,
            deps: [AppConfig],
            multi: true
        },
        ]
    })
    .compileComponents();
  }));

Any Idea? Thanks in advance

@fazanki

This comment has been minimized.

Copy link

@fazanki fazanki commented Feb 10, 2017

APP_INITIALIZER used to work fine for me until I updated Angular to version 2.4.1.
My components and services are getting initialised before my ConfigServices using APP_INITIALIZER provider.

Does anybody else is experiencing the same problem?

providers: [ ConfigService, { provide: APP_INITIALIZER, useFactory: (config:ConfigService) => () => { return config.load(); }, deps: [ConfigService, Http], multi: true }, ],

@anyones

This comment has been minimized.

Copy link

@anyones anyones commented Feb 14, 2017

I ´ve checked my code several times, but I can´t get my mistake.
I still have the problem that my AppConfig instance is successfully populated upon App initialization via the APP_INITIALIZER approach.
BUT I'm not getting any data in AnyClass. The AppConfig instance I'm getting from the injector is empty, i.e. populated with the default values (null in this case).
Meanwhile I updated Angular 2 from version 2.0.0-rc5 to version 2.4.6 but the problem still persists.
Does anyone see my mistake?

AppModule:

//Add this function
function initConfig(config: AppConfig){
    return () => config.load()
}

@NgModule({
  imports:      [ BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ],
  providers: [HttpModule, AppConfig, { provide: APP_INITIALIZER, useFactory: initConfig, deps: [AppConfig], multi: true }]  
})


export class AppModule { }

AppConfig:

@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];
    }

    /**
     *   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('./app/conf/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('app/conf/config.' + envResponse["env"] + '.json');
                    } break;

                    case 'development': {
                        request = this.http.get('app/conf/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) => {
                            debugger;
                            this.config = responseData;
                            resolve(true);
                        });
                } else {
                    console.error('Env config file "env.json" is not valid');
                    resolve(true);
                }
            });

        });
    }
}

AnyClass:

@Injectable()
export class AnyClass {
    constructor(private http: Http, private config: AppConfig) {
        this.config = config; // <-- 'config' IS NULL!!!
    }...
	
	
	
	...	
	let host:string = this.config.getConfig('host'); // <-- 'this.config' IS NULL, because it is set to null in the constructor.

Thank you in advance!

@Sparkomatic

This comment has been minimized.

Copy link

@Sparkomatic Sparkomatic commented Feb 16, 2017

I got the example working just as descried. My problem is now, instead of using this.http.get and returning a promise, I am trying to do a similar thing using AngularFire with my method returning this.af.database.list('path') which is a FirebaseListObservable. My data is always undefined. Any ideas?

@skirdey

This comment has been minimized.

Copy link

@skirdey skirdey commented Feb 22, 2017

for the most recent version of angular, this works:

function initConfig(config: AppConfig){
    return () => config.load()
}

change to

**export** function initConfig(config: AppConfig){
    return () => config.load()
}

and place it right above @NgModule decorator in the app.module.ts,

also, adjust providers to look like

... AppConfig,   { provide: APP_INITIALIZER, useFactory: initConfig, deps: [AppConfig], multi: true } ... 
@geekrumper

This comment has been minimized.

Copy link

@geekrumper geekrumper commented Feb 27, 2017

Like @anyones is mentioning, the values aren't set when the constructor of a service is being called. So instead of setting the config value inside the constructor of a service once (like putting up the pieces for a "basePath"), you have to get them on each service method. You end up with a lot of redundant code to get x amount of configuration variables. Thanks to @elendil-software for the workaround.

For e.g. my basePath contains three pieces: protocol | hostname | port (and one of my service classes contains 21 methods, you can do the math)

const protocol = this._config.getConfig(PROTOCOL);
const host = this._config.getConfig(HOST);
const port = this._config.getConfig(PORT);

this.basePath = `${protocol}://${host}:${port}`;

I could emit basePath and put the values directly, but it's still an overhead...

here is a console log, which is chronically showing what is happening:
config_load

Don't get me wrong, I really like this approach and I'm really thankful for the effort to get such a clean solution. I'm just sharing my experience.

@brianlittmann

This comment has been minimized.

Copy link

@brianlittmann brianlittmann commented Feb 27, 2017

To get this working, I had to add some custom copy config to copy the *.json files to the www/ dir.

Create or update copy.config.js in your root dir:

module.exports = {
	copyEnvConfig: {
		src: ['env.json', 'config.*.json'],
		dest: '{{WWW}}'
	}
}

Then in package.json, add:

"config": {
	"ionic_copy": "./copy.config.js"
}
@philipjohnfob

This comment has been minimized.

Copy link

@philipjohnfob philipjohnfob commented Mar 1, 2017

I followed the steps mentioned in this gist. But when I add the AppConfig in the constructor, I get the following error in console.

EXCEPTION: Uncaught (in promise): Error: DI Error
Error: DI Error
    at NoProviderError.ZoneAwareError
.
.
.
Unhandled Promise rejection: No provider for AppConfig! ; Zone: angular ; Task: Promise.then ; Value:

My constructor looks like below.

  constructor(
    private http: Http,
    private cookieService: CookieService,
    private config: AppConfig) {
      this.authUrl = "https://test/test";
  }

I added the AppConfig in @NgModule providers section in the following order.

    AppConfig,
    { provide: APP_INITIALIZER, useFactory: (config: AppConfig) => () => config.load(), deps: [AppConfig], multi: true },
    CookieService,
    AuthService,

I see that the load() method is successfully executed. I see the config data while debugging. But I m not able to inject the AppConfig class into my service.
Any ideas how I can resolve this issue?

@philipjohnfob

This comment has been minimized.

Copy link

@philipjohnfob philipjohnfob commented Mar 1, 2017

NVM.
The issue was because of a silly error.
I gave the import in app.module.ts as
import { AppConfig } from './config//app.config';
instead of
import { AppConfig } from './config/app.config';

The // caused the issue. Between, thanks for this brilliant approach.

@Chuvisco88

This comment has been minimized.

Copy link

@Chuvisco88 Chuvisco88 commented Mar 8, 2017

I have the same problem as @Carpediemy.
When doing tests I get Cannot read propery 'SomeProperty' of null.
Do we need to call the load function manually somehow or how to establish the config in testing?

@h4mit

This comment has been minimized.

Copy link

@h4mit h4mit commented Mar 9, 2017

Hi, thank's for solution, but how to use getConfig in @NgModule file ?

@TRAHOMOTO

This comment has been minimized.

Copy link

@TRAHOMOTO TRAHOMOTO commented Mar 19, 2017

+1 many thx for solution, very helpful! Especially { provide: APP_INITIALIZER, useFactory: ...

@scottseeker

This comment has been minimized.

Copy link

@scottseeker scottseeker commented Apr 9, 2017

@Carpediemy @Chuvisco88 ever find a solution? It seems the APP_INITIALIZER doesn't work properly in tests. We had to just manually call config.load() in a beforeEach()...

@Priyesha

This comment has been minimized.

Copy link

@Priyesha Priyesha commented Apr 11, 2017

@scottseeker 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".

@angular2bb

This comment has been minimized.

Copy link

@angular2bb angular2bb commented Apr 18, 2017

I have the same problem as @anyones and the @geekrumper solution does not work for me.
I am wondering how to get the data load from the service that is APP_INITIALIZER in my component. As i found that when i add
constructor( private config: AppConfig) , the AppConfig is constucted once again so the data load from APP_INITIALIZER is lost.

Appreciated that if anyone can help with this.

@benjaminu

This comment has been minimized.

Copy link

@benjaminu benjaminu commented Apr 30, 2017

@dbautistav: Did you ever get an answer to your question please? I'm in the same situation as you were/are.

I'm stuck with a similar case using this strategy. I want to use the fetched configuration to inject it into an external library which I added into module's imports. How can I achieve that?

This is my AppModule:

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

export function configProvider(cs: ConfigService) {
return () => cs.load();
}

@Injectable()
export class ConfigService {
public firebaseConfig;

constructor(private http: Http) { }

load(): Promise {
const url: string = "https://somewhere.over/the/rainbow/firebaseConfigKeys.json";
// I've updated this part of the code and actually returning the intended promise, but still doesn't work :(
const promise = this.http.get(url).map(res => res.json()).toPromise();
promise.then(keys => { this.firebaseConfig = keys; });
return promise;
}
}
Any ideas? Thanks in advance.

@crh225

This comment has been minimized.

Copy link

@crh225 crh225 commented May 4, 2017

@benjaminu what if you change you app moduel to look like

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    ...,
    AngularFireModule.initializeApp(firebaseConfig),
  ],
  providers: [
ConfigService,  //this is what I had to add
    {
      deps: [ConfigService],
      multi: true,
      provide: APP_INITIALIZER,
      useFactory: configProvider,
    },
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
@sarahAwad

This comment has been minimized.

Copy link

@sarahAwad sarahAwad commented May 6, 2017

I'm stuck with this error, in the load method, it can't reach the file env.json file nor the development.json file
{ _body: " <h…", status: 404, ok: false, statusText: "Not Found", headers: Object, type: 2, url: "http://localhost:4200/app/config/de…" }

@kvotheyr

This comment has been minimized.

Copy link

@kvotheyr kvotheyr commented May 8, 2017

@Chuvisco88, @scottseeker, @Priyesha were you able to find a solution for app initializer to work in unit tests?

@pushkalb123

This comment has been minimized.

Copy link

@pushkalb123 pushkalb123 commented Jun 5, 2017

@PooperPig: I am getting the same error and (ddramone) solution didnot work for me. Is the issue solved.
@h4mit : how to use getConfig in @NgModule file ? Did you get answer to this.

@creineq

This comment has been minimized.

Copy link

@creineq creineq commented Jun 7, 2017

It takes too long to load using
import { Observable } from 'rxjs/Rx';
Too many files inside Rx folder. So I did this to improve loading time.
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/catch'

@Jensiator

This comment has been minimized.

Copy link

@Jensiator Jensiator commented Jun 7, 2017

This made my load function to work

public load() {
        let promise =this.http.get('url').map(res => res.json()).toPromise();
        promise.then(config =>  this.validation = config);
        return promise;
    };
@tkir

This comment has been minimized.

Copy link

@tkir tkir commented Aug 2, 2017

Hi, thank for great tutorial!
If I use config deeper then one level, getConfig(key) returns undefined.
My solution:
public get(key: any) { let res:any=this.config; key.split('.') .forEach(k=>res=res[k]); return res; }

@deepakanuraags

This comment has been minimized.

Copy link

@deepakanuraags deepakanuraags commented Aug 10, 2017

why cant we simple have static variable in a class => so no injectable is required,simply import the class and read the static variable which will be same across whole of app,why do we have to do this much of code doing appinitializer and stuff,someone please explain so i can get a better understanding.

@Chili82

This comment has been minimized.

Copy link

@Chili82 Chili82 commented Aug 25, 2017

Unit test for Config

import { MockBackend, MockConnection } from '@angular/http/testing';
import { BaseRequestOptions, ConnectionBackend, Http, HttpModule, Response, ResponseOptions, RequestOptions } from '@angular/http';
import { async, fakeAsync, inject, TestBed } from '@angular/core/testing';
import { Config, MockConfig } from './config';

describe('Config', () => {
    beforeEach(() => {
      TestBed.configureTestingModule({
        imports: [HttpModule],
        providers: [Config, MockConfig,
            {
                provide: Http, useFactory: (backend: ConnectionBackend, defaultOptions: BaseRequestOptions) => {
                return new Http(backend, defaultOptions);
              }, deps: [MockBackend, BaseRequestOptions]
              },
          {provide: ConnectionBackend, useClass: MockBackend},
          {provide: MockBackend, useClass: MockBackend},
          {provide: BaseRequestOptions, useClass: BaseRequestOptions}],
      });
    });


it('should Config ...', fakeAsync(inject([Config, MockBackend], (conf: Config, mockBackend: MockBackend) => {
    expect(conf).toBeTruthy();
     conf.load();
     conf._config = { 'host': 'http://localhost'};
     conf._env = { 'env': 'development'};
    const host = conf.get('host');
    const env = conf.getEnv('env');
    expect(host).toEqual('http://localhost');
    expect(env).toEqual('development');

    let res: any;
    mockBackend.connections.subscribe(c => {
        const response = new ResponseOptions({ body: { env: 'development' }});
        c.mockRespond(new Response(response));
      });
      conf.load().then((response) => {
        res = response;
      });
})));

it('should not Be Config ...', fakeAsync(inject([Config, MockBackend], (conf: Config, mockBackend: MockBackend) => {
    let res: any;
    mockBackend.connections.subscribe(c => {
        const response = new ResponseOptions({ body: { envs: '123developments123' }});
        c.mockRespond(new Response(response));
      });
      conf.load().then((response) => {
        res = response;
      });
})));

it('should production Config ...', fakeAsync(inject([Config, MockBackend], (conf: Config, mockBackend: MockBackend) => {
    let res: any;
    mockBackend.connections.subscribe(c => {
        const response = new ResponseOptions({ body: { env: 'production' }});
        c.mockRespond(new Response(response));
      });
      conf.load().then((response) => {
        res = response;
      });
})));

it('should default Config ...', fakeAsync(inject([Config, MockBackend], (conf: Config, mockBackend: MockBackend) => {
    let res: any;
    mockBackend.connections.subscribe(c => {
        const response = new ResponseOptions({ body: { env: 'default' }});
        c.mockRespond(new Response(response));
      });
      conf.load().then((response) => {
        res = response;
      });
})));

// tslint:disable-next-line:eofline
});
@ridj87

This comment has been minimized.

Copy link

@ridj87 ridj87 commented Aug 31, 2017

@Chuvisco88, @scottseeker, @Priyesha @Carpediemy, where any of you able to find a solution for this problem ?

@Someone92

This comment has been minimized.

Copy link

@Someone92 Someone92 commented Sep 5, 2017

Property 'get' does not exist on type 'AppConfig'. <-- I get this error when running the following code.
let host: string = this.config.get('host');

This works perfectly
let env: string = this.config.getEnv('env');

If i run
let host: string = this.config['config']['host'])
i can access the config file and the host parameter.

Appreciate all the help.

@sudhakar-sekar

This comment has been minimized.

Copy link

@sudhakar-sekar sudhakar-sekar commented Sep 11, 2017

@scottseeker Yes, i am 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 null".

@ortichon

This comment has been minimized.

Copy link

@ortichon ortichon commented Sep 12, 2017

@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

This comment has been minimized.

Copy link

@smitelij smitelij commented Sep 14, 2017

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

This comment has been minimized.

Copy link

@saadiadel saadiadel commented Sep 19, 2017

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.

@stevek-pro

This comment has been minimized.

Copy link

@stevek-pro stevek-pro 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.

@stevek-pro

This comment has been minimized.

Copy link

@stevek-pro stevek-pro commented Oct 8, 2017

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

This comment has been minimized.

Copy link

@JoseMejia96 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

This comment has been minimized.

Copy link

@Priyesha Priyesha commented Nov 24, 2017

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

This comment has been minimized.

Copy link

@okta123 okta123 commented Nov 27, 2017

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

@xeax

This comment has been minimized.

Copy link

@xeax xeax commented Nov 28, 2017

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

@deejbee

This comment has been minimized.

Copy link

@deejbee 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

This comment has been minimized.

Copy link

@rpeyfuss 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

This comment has been minimized.

Copy link

@bhupal4all 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

This comment has been minimized.

Copy link

@BBaysinger BBaysinger commented Mar 16, 2018

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

@rubenkuipers

This comment has been minimized.

Copy link

@rubenkuipers 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

This comment has been minimized.

Copy link

@m00zi m00zi commented May 25, 2018

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

@lucasklaassen

This comment has been minimized.

Copy link

@lucasklaassen lucasklaassen commented May 31, 2018

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

@nlern

This comment has been minimized.

Copy link

@nlern nlern commented Sep 1, 2018

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

This comment has been minimized.

Copy link

@schmorrison schmorrison commented Sep 20, 2018

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

This comment has been minimized.

Copy link

@pchiarato 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

This comment has been minimized.

Copy link

@jeevanandamjobs 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

This comment has been minimized.

Copy link

@kavinda1995 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

This comment has been minimized.

Copy link

@dabbid 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

This comment has been minimized.

Copy link

@ayyash 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

This comment has been minimized.

Copy link

@dabbid 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

This comment has been minimized.

Copy link

@ayyash 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

This comment has been minimized.

Copy link

@vitaliidasaev vitaliidasaev commented Jun 18, 2020

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

For those who looks about msal-angular MsalHttpInterceptor.
I've made own implementation of MsalInterceptor to overcome issues with protectedResourceMap.
This is my custom code block:

// #region own workaround in order not to put every api endpoint url to settings
if (!scopes && req.url.startsWith(this.settingsService.apiUrl)) {
  scopes = [this.auth.getCurrentConfiguration().auth.clientId];
}
// #endregion

// If there are no scopes set for this request, do nothing.
if (!scopes) {
  return next.handle(req);
}

PS: Vote for wildcard in protectedResourceMap AzureAD/microsoft-authentication-library-for-js#1776

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.