In this post, Premier Developer consultant Laurie Atkinson walks through how to allow editing of your Angular configuration files after your app has been built, bundled, and deployed.
*This post was updated on 12/3/2018 to reflect the latest changes to Angular.
The Angular-CLI is the recommended way to build a production-ready app, complete with bundling, uglifying, and tree-shaking. An Angular-CLI generated application even comes with a mechanism for creating environment-specific versions. However, those configuration files are in TypeScript and do not allow editing by IT staff or automated deployment tools such as VSTS. This post provides the steps and code samples for using a JSON configuration file, which can be customized for multiple environments.
Define TypeScript interface for config settings
The use of interfaces in an Angular app provides intellisense and type-safety for your entities. For this example, refer to this sample configuration file.
app-config.model.ts
export interface IAppConfig { env: { name: string; }; appInsights: { instrumentationKey: string; }; logging: { console: boolean; appInsights: boolean; }; aad: { requireAuth: boolean; tenant: string; clientId: string; }; apiServer: { metadata: string; rules: string; }; }
Create JSON config files
A convenient place to store configuration files is under the assets folder of your project. Using the interface defined above, sample files could look as follows:
assets\config\config.dev.json
{ "env": { "name": "DEV" }, "appInsights": { "instrumentationKey": "<dev-guid-here>" }, "logging": { "console": true, "appInsights": false }, "aad": { "requireAuth": true, "tenant": "<dev-guid-here>", "clientId": "<dev-guid-here>" }, "apiServer": { "metadata": "https://metadata.demo.com/api/v1.0/", "rules": "https://rules.demo.com/api/v1.0/" } }
assets\config\config.deploy.json (Note placeholders that are replaced during deployment)
{ "env": { "name": "#{envName}" }, "appInsights": { "instrumentationKey": "#{appInsightsKey}" }, "logging": { "console": true, "appInsights": true }, "aad": { "requireAuth": true, "tenant": "#{aadTenant}", "clientId": "#{aadClientId}" }, "apiServer": { "metadata": "https://#{apiServerPrefix}.demo.com/api/v1.0/", "rule": "https://#{apiServerPrefix}.demo.com/api/v1.0/", } }
Continue to use environment.ts with Angular-CLI build
The Angular-CLI creates several TypeScript environment files in the environments folder. They will still be used, but contain only the environment name.
environments\environment.dev.json
export const environment = { name: 'dev' };
environments\environment.deploy.json
export const environment = { name: 'deploy' };
angular.json
"projects": { "my-app": { "architect": { "build": { "configurations": { "deploy": { "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.deploy.ts" } ], . . . } } }, "serve": { . . . "configurations": { "deploy": { "browserTarget": "my-app:build:deploy" }
Create a service to read config file
This service will read the correct config file and store the result in a static field in this class..
app.config.ts (Note the use of the interface defined above and config file naming convention to retrieve the appropriate file.)
import { Injectable } from '@angular/core’; import { HttpClient } from '@angular/common/http'; import { environment } from '../environments/environment'; import { IAppConfig } from './models/app-config.model'; @Injectable() export class AppConfig { static settings: IAppConfig; constructor(private http: HttpClient) {} load() { const jsonFile = `assets/config/config.${environment.name}.json`; return new Promise<void>((resolve, reject) => { this.http.get(jsonFile).toPromise().then((response : IAppConfig) => { AppConfig.settings = <IAppConfig>response; resolve(); }).catch((response: any) => { reject(`Could not load file '${jsonFile}': ${JSON.stringify(response)}`); }); }); } }
Load config file prior to app creation
Angular includes a token named APP_INITIALIZER that allows our app to execute code when the application is initialized. In the app module, use this token to invoke the load method in our config service. Since our method returns a promise, Angular will delay the initialization until the promise is resolved.
app.module.ts
import { APP_INITIALIZER } from '@angular/core'; import { AppConfig } from './app.config'; export function initializeApp(appConfig: AppConfig) { return () => appConfig.load(); } @NgModule({ imports: [ , , , ], declarations: [ . . . ], providers: [ AppConfig, { provide: APP_INITIALIZER, useFactory: initializeApp, deps: [AppConfig], multi: true } ], bootstrap: [ AppComponent ] }) export class AppModule { }
Consume the app settings throughout the application
The config settings are now available from anywhere in the application and they include type-checking provided by the interface.
export class DataService { protected apiServer = AppConfig.settings.apiServer; . . . if (AppConfig.settings.aad.requireAuth) { . . . } } export class LoggingService { . . . instrumentationKey: AppConfig.settings && AppConfig.settings.appInsights ? AppConfig.settings.appInsights.instrumentationKey : '' . . . if (AppConfig.settings && AppConfig.settings.logging) { . . . } }
Note: to build a production version of the app using an environment name other than prod, use this command:
ng build –configuration=deploy
Hi Laurie Atkinson,
I like your approach and we are using it in this specific scenario. I have a comment about naming - you are using class AppConfig - which indeed is provider of configuration settings and meanwhile - you have interface with name- IAppConfig. When I saw it for the first time I expected that this interface is for "describing" this class - its properties/functions. I agree and I like this approach to expose an...
why don’t you just save config straight in the environments files? I don’t see a point to this whole app.config thing.
Because the environment files are read at build-time, not at run-time.
If, for example, you needed to move the API server to a new IP address, in order to do that using the environment files, you'd have to make the change and then recompile and redeploy the whole app for the change to take place.
Using this method, a simple file change is all that is needed, and then restart the web app. No need...
I've been using this method for some time successfully.
I ended up creating another file (suggestion from a colleague) that had some constants, such as BASE_URL etc.. then I split into two files, one api-calls.json which had actual API calls, and another for general environment-specific app configuration (such as BASE_URL).
app-config.json:
[
Profile: "Local",
Local: { BASE_URL: "http://localhost:53324/api" },
Test : { BASE_URL: "http://test.mydomain.com/api" },
Prod: {...
I added this on Angular 6
I am trying to run this solution, but it doesen’t work. I tried a number of these kind of solutions and every each of them fails, because the response data after the promise is never defined (message: “response is not defined”).
"Note: to build a production version of the app using an environment name other than prod, use this command:
ng build –configuration=deploy"Wait, so i cant change the config file without re building the app? Then its not really a config file at all if its baked into the app at build time
This is a bad strategy and a bad pattern. A good solution would be to build the app once, and have that one build...
I have the same needs. Did you find any solutions to reload configs without rebuilding the app?
I think you are missing the point in that at deploy time this allows to add the correct configuration to any environment that you will be deploying to using some form of release variables.
It is very useful when say, deploying the same code to mutiple different Tenants (companies/enterprises). You can have release variables set up in your release definition for each environment t(tenant) you are deploying to. And all you need to do is...
Our use case mightbe simalr to Sam's. We want one build to be submitted to the app store and then allow the testers to reconfigure it to use only the development servers so they can submit test data without polluting production. This could be done through some secret key stroke or tap sequence but could also be triggered by the appeararance of another file on the device. e.g if debug.txt is found in the documents...
I think you are missing the point.
Here are some arguments on why you should avoid one build per environment:
- It slows down CI pipelines because it needs to create a build for every environment
- It increases the risk of errors/differences in different environments because the builds are separate
- It adds unnecessary information about other environments in the code
- You might, for security reasons, don’t want to show confidential information in...
Hi, I've been incorporating this functionality into my Angular 6 web app and have noticed that my config settings are not actually available anywhere in the application as mentioned. Specifically, they don't seem to be available in my injectable classes unless I call it within a class method. So for example, in something like this where ConfigService is the name of the config file read service:
@Injectable()
export class TestService {
endpoint...
I have the exact same problem. Is there nobody adressing this?
I have similar experiences.
Hi guys,
nice tutorial, thanks!
One question, where do you decide that assets\config file will used? I don’t know where you determine the values to replaces the placeholders in assets\config\config.deploy.json
Regards
The customer I worked with was using Octopus Deploy which has an option for setting up variables in a step. (https://octopus.com/docs/deployment-process/configuration-features/substitute-variables-in-files) Azure DevOps has a similar option called “Replace Tokens”: https://marketplace.visualstudio.com/items?itemName=qetza.replacetokens