I have several applications that do exactly this.
I've built a utility library for my applications that includes this.
First, I have a "Configuration" class. The json configuration file gets loaded from the server and mapped to an instance of this class:
export class Configuration {
[key: string]: any;
}
Then, there's the ConfigurationService, that is responsible for loading the configuration file:
import {APP_INITIALIZER, Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs/Observable';
import {AsyncSubject} from 'rxjs/AsyncSubject';
import 'rxjs/observable/throw';
import {Configuration} from './configuration';
// synchronous version of the initializer - the app initialization will wait for the configuration to load
export function configurationServiceInitializerFactory(configurationService: ConfigurationService): Function {
// a lambda is required here, otherwise `this` won't work inside ConfigurationService::load
return () => configurationService.load(Synchronicity.Sync);
}
// async version of the initializer - the app initialization will proceed without waiting for the configuration to load
export function asyncConfigurationServiceInitializerFactory(configurationService: ConfigurationService): Function {
// a lambda is required here, otherwise `this` won't work inside ConfigurationService::load
return () => {
configurationService.load(Synchronicity.Async);
return null;
};
}
export const enum Synchronicity {
Sync,
Async,
Unknown
}
@Injectable()
export class ConfigurationService {
private synchronicity: Synchronicity = Synchronicity.Unknown;
// the observable from the (load) http call to get the configuration
private httpObservable: Observable<Configuration>;
// the error (if any) that occurred during the load
private loadError;
private loadAttempted = false;
private hasError = false;
private loaded = false;
// Observable that makes the config available to consumers when using async initialization
private loadSubject = new AsyncSubject<Configuration>();
// the configuration
private configuration: Configuration;
constructor(private http: HttpClient) {
}
public hasLoadError(): boolean {
return this.hasError;
}
public isLoadead(): boolean {
return this.loaded;
}
// use this when you have initialized with the (synchronous) configurationServiceInitializerFactory
public getConfig(): Configuration {
if(!this.loadAttempted) {
throw new Error('ConfigurationService.getConfig() - service has not been iniialized yet');
}
if(this.synchronicity === Synchronicity.Async) {
throw new Error('ConfigurationService.getConfig() - service has been iniialized async - use getConfigurationObserable()');
}
if(this.hasError) {
throw this.loadError;
}
if(!this.loaded) {
throw new Error('ConfigurationService.getConfig() - service has not finished loading the config');
}
return this.configuration;
}
// use this when you have initialized with the asyncCnfigurationServiceInitializerFactory
public getConfigObservable(): Observable<Configuration> {
// if neither init function was used, init async
if (!this.loadAttempted) {
this.load(Synchronicity.Async);
}
return this.loadSubject;
}
// the return value (Promise) of this method is provided via the APP_INITIALIZER Injection Token,
// so the application's initialization will not complete until the Promise resolves.
public load(synchronicity: Synchronicity): Promise<Configuration> {
if (!this.loadAttempted) {
this.loadAttempted = true;
this.synchronicity = synchronicity;
this.httpObservable = this.http.get<Configuration>('config/ui-config.json'); // path is relative to that for app's index.html
this.httpObservable.subscribe(
config => {
this.configuration = config;
this.loadError = undefined;
this.hasError = false;
this.loadSubject.next(this.configuration);
this.loadSubject.complete();
this.loaded = true;
},
error => {
this.loadError = error;
this.hasError = true;
this.loadSubject.error(error);
this.loadSubject.complete();
}
);
return this.httpObservable.toPromise();
}
}
}
As you can see, this service gets the configuration from a relative path, config/ui-config.json. The path is relative to the index.html file that was loaded to bootstrap the application. You need to arrange for the server to return the configuration file from that location.
The service will be hooked into Angular's initialization sequence (code to follow). It can be done either synchronously or asynchronously with respect to the app's initialization.
If you use the 'synchronous' method, then app initialization will pause while the json file is loaded. The upside to this is that once the app finishes initialization, the configuration is known to be available. The downside is the potentially long pause during initialization, where your user is looking at a blank page.
If you use the 'asynchronous' method, then the app initialization will only kick off the request for the config file, but will not pause to wait for that request to complete. Upside: quick (normal) initialization. Downside: you get an Observable of the Configuration instead of a Configuration, so you need to flatMap (mergeMap) over that Observable everywhere that you need the configuration.
Here's how it gets hooked into the app initialization, in app.module:
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
// etc.
],
providers: [
ConfigurationService,
{ provide: APP_INITIALIZER, useFactory: asyncConfigurationServiceInitializerFactory, deps: [ConfigurationService], multi: true },
],
bootstrap: [AppComponent]
})
export class AppModule {
}
That's an example of the async config. For sync, just use configurationServiceInitializerFactory instead of asyncConfigurationServiceInitializerFactory
Again, if you use the synchronous version, you can just inject the ConfigurationService into your services, and call it's getConfig() method.
If you use the async version, you still inject the ConfigurationService into your services, but then you need to do something like this:
getSomething(): Observable<Something> {
return this.configurationService.getConfigObservable().mergeMap(config =>
this.http.get<Something>(`${config.serviceRoot}/something`)
);
}
Edit: Oh, I almost forgot, I did a blog post on this a while back, and that has a little more detail. It's at https://chariotsolutions.com/blog/post/12-factor-ish-configuration-of-angular-applications/
And there's a complete example on GitHub at https://github.com/rfreedman/angular-configuration-service