App.settings - the Angular way?
Asked Answered
D

11

134

I want to add an App Settings section into my App where It will contain some consts and pre defined values.

I've already read this answer which uses OpaqueToken But it is deprecated in Angular. This article explains the differences but it didn't provide a full example , and my attempts were unsuccessful.

Here is what I've tried ( I don't know if it's the right way) :

//ServiceAppSettings.ts

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

const CONFIG = {
  apiUrl: 'http://my.api.com',
  theme: 'suicid-squad',
  title: 'My awesome app'
};
const FEATURE_ENABLED = true;
const API_URL = new InjectionToken<string>('apiUrl');

And this is the component where I want to use those consts :

//MainPage.ts

import {...} from '@angular/core'
import {ServiceTest} from "./ServiceTest"

@Component({
  selector: 'my-app',
  template: `
   <span>Hi</span>
  ` ,  providers: [
    {
      provide: ServiceTest,
      useFactory: ( apiUrl) => {
        // create data service
      },
      deps: [

        new Inject(API_URL)
      ]
    }
  ]
})
export class MainPage {


}

But it doesn't work and I get errors.

Question:

How can I consume "app.settings" values the Angular way?

plunker

NB Sure I can create Injectable service and put it in the provider of the NgModule , But as I said I want to do it with InjectionToken , the Angular way.

Divorcement answered 3/4, 2017 at 19:45 Comment(5)
You can check my answer here based in current official documentationPoly
@javier no. Your link has a problem if two providers supply the same name so you now have a problem. Entring opaquetokenDivorcement
you know [OpaqueToken is deprecated]. (angular.io/api/core/OpaqueToken) This Article talks about how to prevent name collisions in Angular ProvidersPoly
Yaeh i iknow but still the linked article is wrong.Divorcement
may be below link can be helpful for every one who likes to use new architecture of angular config schema devblogs.microsoft.com/premier-developer/…Skylar
B
70

I figured out how to do this with InjectionTokens (see example below), and if your project was built using the Angular CLI you can use the environment files found in /environments for static application wide settings like an API endpoint, but depending on your project's requirements you will most likely end up using both since environment files are just object literals, while an injectable configuration using InjectionToken's can use the environment variables and since it's a class can have logic applied to configure it based on other factors in the application, such as initial HTTP request data, subdomain, etc.

Injection Tokens Example

/app/app-config.module.ts

import { NgModule, InjectionToken } from '@angular/core';
import { environment } from '../environments/environment';

export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');

export class AppConfig {
  apiEndpoint: string;
}

export const APP_DI_CONFIG: AppConfig = {
  apiEndpoint: environment.apiEndpoint
};

@NgModule({
  providers: [{
    provide: APP_CONFIG,
    useValue: APP_DI_CONFIG
  }]
})
export class AppConfigModule { }

/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppConfigModule } from './app-config.module';

@NgModule({
  declarations: [
    // ...
  ],
  imports: [
    // ...
    AppConfigModule
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Now you can just DI it into any component, service, etc:

/app/core/auth.service.ts

import { Injectable, Inject } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

import { APP_CONFIG, AppConfig } from '../app-config.module';
import { AuthHttp } from 'angular2-jwt';

@Injectable()
export class AuthService {

  constructor(
    private http: Http,
    private router: Router,
    private authHttp: AuthHttp,
    @Inject(APP_CONFIG) private config: AppConfig
  ) { }

  /**
   * Logs a user into the application.
   * @param payload
   */
  public login(payload: { username: string, password: string }) {
    return this.http
      .post(`${this.config.apiEndpoint}/login`, payload)
      .map((response: Response) => {
        const token = response.json().token;
        sessionStorage.setItem('token', token); // TODO: can this be done else where? interceptor
        return this.handleResponse(response); // TODO:  unset token shouldn't return the token to login
      })
      .catch(this.handleError);
  }
   
  // ...
}

You can then also type check the config using the exported AppConfig.

Buyse answered 3/4, 2017 at 20:22 Comment(8)
No, but you can literally copy and paste the first part into a file, import it into your app.module.ts file, and the DI it anywhere and output it to the console. I would take longer to set this up in a plunker then it would to do those steps.Buyse
Oh I thought You already have a plunker for this :-) Thank you.Divorcement
For those who want : plnkr.co/edit/2YMZk5mhP1B4jTgA37B8?p=previewDivorcement
Does it have to be used via Interface ?Divorcement
I don't believe you need to export the AppConfig interface/class. You definitely don't need to use it when doing DI. To make this work in one file it had to be a class instead of an interface, but that doesn't matter. In fact the style guide suggest using classes instead of interfaces since it means less code and you can still type check using them. With regards to its use by the InjectionToken via generics that is something you'll want to include.Buyse
hi @mtpultz: is there a way for not having to do import { APP_CONFIG, AppConfig } from '../app-config.module'; ? For ex: if I have settings that I will use in many components, and one day I might change the file name app-config.module.ts into app-settings.module.ts is there a way for me to only need to change in 1 place and it will apply to all components? ThanksRadiology
@SeverusDark not that I know of since the import path is hardcoded. It would take only a second in your IDE to run a search and replace with the new filename.Buyse
I'm trying to inject the API endpoint dynamically using Azure's environment variables and JSON transform features, but it looks like this answer just gets the apiEndpoint from the environment file. How would you grab it from the config and export it?Inquisition
A
173

It's not advisable to use the environment.*.ts files for your API URL configuration. It seems like you should because this mentions the word "environment".

Using this is actually compile-time configuration. If you want to change the API URL, you will need to re-build. That's something you don't want to have to do ... just ask your friendly QA department :)

What you need is runtime configuration, i.e. the app loads its configuration when it starts up.

Some other answers touch on this, but the difference is that the configuration needs to be loaded as soon as the app starts, so that it can be used by a normal service whenever it needs it.

To implement runtime configuration:

  1. Add a JSON config file to the /src/assets/ folder (so that is copied on build)
  2. Create an AppConfigService to load and distribute the config
  3. Load the configuration using an APP_INITIALIZER

1. Add Config file to /src/assets

You could add it to another folder, but you'd need to tell the Angular CLI that it is an asset, by updating the angular.json configuration file. Start off using the assets folder:

{
  "apiBaseUrl": "https://development.local/apiUrl"
}

2. Create AppConfigService

This is the service which will be injected whenever you need the config value:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class AppConfigService {

  private appConfig: any;

  constructor(private http: HttpClient) { }

  loadAppConfig() {
    return this.http.get('/assets/config.json')
      .toPromise()
      .then(data => {
        this.appConfig = data;
      });
  }

  // This is an example property ... you can make it however you want.
  get apiBaseUrl() {

    if (!this.appConfig) {
      throw Error('Config file not loaded!');
    }

    return this.appConfig.apiBaseUrl;
  }
}

3. Load the configuration using an APP_INITIALIZER

To allow the AppConfigService to be injected safely, with config fully loaded, we need to load the config at app startup time. Importantly, the initialisation factory function needs to return a Promise so that Angular knows to wait until it finishes resolving before finishing startup:

import { APP_INITIALIZER } from '@angular/core';
import { AppConfigService } from './services/app-config.service';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  providers: [
    {
      provide: APP_INITIALIZER,
      multi: true,
      deps: [AppConfigService],
      useFactory: (appConfigService: AppConfigService) => {
        return () => {
          //Make sure to return a promise!
          return appConfigService.loadAppConfig();
        };
      }
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Now you can inject it wherever you need to and all the config will be ready to read:

@Component({
  selector: 'app-test',
  templateUrl: './test.component.html',
  styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit {

  apiBaseUrl: string;

  constructor(private appConfigService: AppConfigService) {}

  ngOnInit(): void {
    this.apiBaseUrl = this.appConfigService.apiBaseUrl;
  }

}

I can't say it strongly enough, configuring your API urls as compile-time configuration is an anti-pattern. Use runtime configuration.

Awfully answered 20/2, 2019 at 18:54 Comment(24)
This is exactly what I want to do however I already have an APP_INITIALIZER that makes a call to our API to see if the user is authenticated. It is injected with the base URL that I want to pull from config. How can I setup 2 APP_INITIALIZERs where one is dependent on the other?Northbound
Normally I'd agree, but this being a single-page-application the deployment story is a little different than usual and I don't think the anti-pattern is so bad here. It's not as compelling to avoid compile-time configuration in an arrangement like this. You're going to have to redeploy the application if you want to change the assets anyway. Now if you're loading something from a different server, that's another story, but truly dynamic settings wouldn't be involved in this question anyway.Applicant
I should add that we use deploy plans to bridge this gap - so technically we're doing something very similar to what you're suggesting.Applicant
Local file or different service, compile-time config shouldn't be used for an API url. Imagine if your app is sold as a product (purchaser to install), you don't want them to be compiling it, etc. Either way, you don't want to re-compile something that was built 2 years ago, just because the API url changed. The risk!!Awfully
@Bloodhound You can have more than one APP_INITIALIZER but I don't think you can easily make them dependent on one another. It sounds like you have a good question to ask, so maybe link to it here?Awfully
@MattTester - If Angular ever implements this feature it would solve our problem: github.com/angular/angular/issues/23279#issuecomment-528417026Northbound
I'm a little confused. If you're reading a static file in your assets how is this runtime configurable? Wouldn't you want to read config from a protected endpoint or something that can serve up different content after build?Perishing
@CrhistianRamirez It's from the point of view of the app: configuration is not known until runtime and the static file is outside of the build and can be set in many ways at deploy time. Static file is fine for non-sensitive config. An API or some other protected end-point is possible with the same technique, but how to authenticate to make it protected is your next challenge.Awfully
@MattTester great solution, how did you solve the the issue with publishing the site, either the existing config file is over-written, or deleted is publish if set to "Remove additional files at destination"Dinnie
@DaleK Reading between the lines, you're deploying using Web Deploy. If you're using a deployment pipeline, like Azure DevOps, then it's possible to set the config file correctly as the next step. The setting of the config is the responsibility of the deployment process/pipeline, which can override values in the default config file. Hope that clarifies.Awfully
Settings loads only one time when module is loaded first?Suprarenal
@AliceMessis Yes, the settings load once, when the application is initializing/loading/bootstrapping. I'm not 100% sure how this would work if you had a lazy loaded module define an APP_INITIALIZER, I've always done this in my "Core" module.Awfully
I tried this solution and its working for Chrome Edge etc. But not for Firefox. When I load the application in Firefox the app is not blocking until configuration is loaded. Did you face that kind of issue on Firefox @MattTester?Glavin
@MattTester I want to use a URL from the config file for an injected service of another ngrx related component in the providers list. It seems that the configuration is not loaded yet when I access a property from the configuration file in another component from the providers list. The get PROPERTY_NAME() gets called before the loadAppConfig() method. How can I solve this problem?Preventer
@Bozhinovski The last time I've used Firefox, it was fine, but I admit that it was a while ago. For the apps I work on, it hasn't been a use case. Maybe ask as new question and post the link here in the comments?Awfully
@Preventer Any other service should be able to inject the AppConfigService and use the values. However, are you saying that when registering a provider, you need to know the configured url at that point (i.e. when it's registered with Angular)? In that case, no, that won't work as has to be known at compile-time, not runtime. Did I miss the real question?Awfully
@MattTester Is it recommended to add customer specific styling attributes to AppConfigService? My config .json would then be a few hundred lines long.Soll
@MattTester I also created this question for typecasting the AppConfigService for different apps: #69973466Soll
@MattTester I did this approach and deployed the code in nginx. After updating the config.json file and then restarting nginx, it wasn't getting the updates. I'm still investigating what I'm doing wrong but I'm clueless at the moment. This works locally. It's just in the nginx that I wasn't able to make it work.Abigail
@Abigail With it working locally, it may well be caching in nginx or some proxy in between. If you use Dev Tools and look at the XHR request for the config.json file, it should help you see if it was a cached response. You can always add a cache bust query string with the current ticks, if that will verify e.g. config.json?t=112121212.Awfully
@MattTester I got it sorted out. It worked the first time. There was a wait/delay task in my jenkins pipeline that messed things up. Thank you!Abigail
@MattTester this is the best solution together with the best explanation I´ve found for this topic. Is it available on a blog or something to provide it as a reference to my team? (nothing agaisnt stackoverflow, but I would prefer a more formal format)Kirkham
@Kirkham I appreciate the feedback, thanks. No, it's not on a blog that I know of (I had a blog but couldn't find the time to do it justice between work and making courses). Maybe we should suggest to the Angular team to get this in the official docs!Awfully
How do I get apiBaseUrl into app.module. I need it there to set a manifest URLHyaloplasm
P
154

If you are using , there is yet another option:

Angular CLI provides environment files in src/environments (default ones are environment.ts (dev) and environment.prod.ts (production)).

Note that you need to provide the config parameters in all environment.* files, e.g.,

environment.ts:

export const environment = {
  production: false,
  apiEndpoint: 'http://localhost:8000/api/v1'
};

environment.prod.ts:

export const environment = {
  production: true,
  apiEndpoint: '__your_production_server__'
};

and use them in your service (the correct environment file is chosen automatically):

api.service.ts

// ... other imports
import { environment } from '../../environments/environment';

@Injectable()
export class ApiService {     

  public apiRequest(): Observable<MyObject[]> {
    const path = environment.apiEndpoint + `/objects`;
    // ...
  }

// ...
}

Read more on application environments on Github (Angular CLI version 6) or in the official Angular guide (version 7).

Plating answered 10/4, 2017 at 6:48 Comment(13)
its working fine.But while moving build it is also changed as bundle.I should change configuartion in my serve not in code after moving to productionMet
This is somewhat of an anti-pattern in normal software development; the API url is just configuration. It shouldn't take a re-build to re-configure an app for a different environment. It should be build once, deploy many times (pre-prod, staging, prod, etc).Awfully
@MattTester This is actually what is now an official Angular-CLI story. If you happen to have a better answer to this question: feel free to post it!Plating
is it configurable after ng build ?Camarillo
@Plating do you have a link to the story?Brynnbrynna
@JensBodal here you go: github.com/angular/angular-cli/wiki/…Plating
Not sure if you meant to link to the angular cli wiki but that’s where that goesBrynnbrynna
@JensBodal absolutely. The link goes to the CLI story on application environmentsPlating
Oh ok, I misread the comments. I’d agree that this lends to an anti-pattern, I thought there was a story for dynamic run time configs.Brynnbrynna
@Plating Better late than never, but I did get around to posting an alternative. Using Runtime Configuration, you can avoid the anti-pattern.Awfully
@jens-bodal In case you still need to, check out the other answers to this question which use dynamic/runtime configurationAwfully
Anti-pattern: See Matt Tester's answer for a more appropriate solution.Roti
@MattTester you are right. This can be achieved by injecting environment variables into the app and use it inside the environment.prod.ts. Can be changed at runtime, e.g. by .env files. Manual: kehrwasser.com/blog/2021/09/23/…Isoleucine
B
70

I figured out how to do this with InjectionTokens (see example below), and if your project was built using the Angular CLI you can use the environment files found in /environments for static application wide settings like an API endpoint, but depending on your project's requirements you will most likely end up using both since environment files are just object literals, while an injectable configuration using InjectionToken's can use the environment variables and since it's a class can have logic applied to configure it based on other factors in the application, such as initial HTTP request data, subdomain, etc.

Injection Tokens Example

/app/app-config.module.ts

import { NgModule, InjectionToken } from '@angular/core';
import { environment } from '../environments/environment';

export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');

export class AppConfig {
  apiEndpoint: string;
}

export const APP_DI_CONFIG: AppConfig = {
  apiEndpoint: environment.apiEndpoint
};

@NgModule({
  providers: [{
    provide: APP_CONFIG,
    useValue: APP_DI_CONFIG
  }]
})
export class AppConfigModule { }

/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppConfigModule } from './app-config.module';

@NgModule({
  declarations: [
    // ...
  ],
  imports: [
    // ...
    AppConfigModule
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Now you can just DI it into any component, service, etc:

/app/core/auth.service.ts

import { Injectable, Inject } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

import { APP_CONFIG, AppConfig } from '../app-config.module';
import { AuthHttp } from 'angular2-jwt';

@Injectable()
export class AuthService {

  constructor(
    private http: Http,
    private router: Router,
    private authHttp: AuthHttp,
    @Inject(APP_CONFIG) private config: AppConfig
  ) { }

  /**
   * Logs a user into the application.
   * @param payload
   */
  public login(payload: { username: string, password: string }) {
    return this.http
      .post(`${this.config.apiEndpoint}/login`, payload)
      .map((response: Response) => {
        const token = response.json().token;
        sessionStorage.setItem('token', token); // TODO: can this be done else where? interceptor
        return this.handleResponse(response); // TODO:  unset token shouldn't return the token to login
      })
      .catch(this.handleError);
  }
   
  // ...
}

You can then also type check the config using the exported AppConfig.

Buyse answered 3/4, 2017 at 20:22 Comment(8)
No, but you can literally copy and paste the first part into a file, import it into your app.module.ts file, and the DI it anywhere and output it to the console. I would take longer to set this up in a plunker then it would to do those steps.Buyse
Oh I thought You already have a plunker for this :-) Thank you.Divorcement
For those who want : plnkr.co/edit/2YMZk5mhP1B4jTgA37B8?p=previewDivorcement
Does it have to be used via Interface ?Divorcement
I don't believe you need to export the AppConfig interface/class. You definitely don't need to use it when doing DI. To make this work in one file it had to be a class instead of an interface, but that doesn't matter. In fact the style guide suggest using classes instead of interfaces since it means less code and you can still type check using them. With regards to its use by the InjectionToken via generics that is something you'll want to include.Buyse
hi @mtpultz: is there a way for not having to do import { APP_CONFIG, AppConfig } from '../app-config.module'; ? For ex: if I have settings that I will use in many components, and one day I might change the file name app-config.module.ts into app-settings.module.ts is there a way for me to only need to change in 1 place and it will apply to all components? ThanksRadiology
@SeverusDark not that I know of since the import path is hardcoded. It would take only a second in your IDE to run a search and replace with the new filename.Buyse
I'm trying to inject the API endpoint dynamically using Azure's environment variables and JSON transform features, but it looks like this answer just gets the apiEndpoint from the environment file. How would you grab it from the config and export it?Inquisition
G
15

Poor man's configuration file:

Add to your index.html as first line in the body tag:

<script lang="javascript" src="assets/config.js"></script>

Add assets/config.js:

var config = {
    apiBaseUrl: "http://localhost:8080"
}

Add config.ts:

export const config: AppConfig = window['config']

export interface AppConfig {
    apiBaseUrl: string
}
Garica answered 23/3, 2020 at 9:41 Comment(2)
Seriously, +1 for boiling down a solution into it's most basic of components and still maintaining type consistency.Agouti
This allows configuration to be set from the server host application, example an MVC view.Bimetallism
E
13

I found that using an APP_INITIALIZER for this doesn't work in situations where other service providers require the configuration to be injected. They can be instantiated before APP_INITIALIZER is run.

I've seen other solutions that use fetch to read a config.json file and provide it using an injection token in a parameter to platformBrowserDynamic() prior to bootstrapping the root module. But fetch isn't supported in all browsers and in particular WebView browsers for the mobile devices I target.

The following is a solution that works for me for both PWA and mobile devices (WebView). Note: I've only tested in Android so far; working from home means I don't have access to a Mac to build.

In 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 { APP_CONFIG } from './app/lib/angular/injection-tokens';

function configListener() {
  try {
    const configuration = JSON.parse(this.responseText);

    // pass config to bootstrap process using an injection token
    platformBrowserDynamic([
      { provide: APP_CONFIG, useValue: configuration }
    ])
      .bootstrapModule(AppModule)
      .catch(err => console.error(err));

  } catch (error) {
    console.error(error);
  }
}

function configFailed(evt) {
  console.error('Error: retrieving config.json');
}

if (environment.production) {
  enableProdMode();
}

const request = new XMLHttpRequest();
request.addEventListener('load', configListener);
request.addEventListener('error', configFailed);
request.open('GET', './assets/config/config.json');
request.send();

This code:

  1. kicks off an async request for the config.json file.
  2. When the request completes, parses the JSON into a Javascript object
  3. provides the value using the APP_CONFIG injection token, prior to bootstrapping.
  4. And finally bootstraps the root module.

APP_CONFIG can then be injected into any additional providers in app-module.ts and it will be defined. For example, I can initialise the FIREBASE_OPTIONS injection token from @angular/fire with the following:

{
      provide: FIREBASE_OPTIONS,
      useFactory: (config: IConfig) => config.firebaseConfig,
      deps: [APP_CONFIG]
}

I find this whole thing a surprisingly difficult (and hacky) thing to do for a very common requirement. Hopefully in the near future there will be a better way, such as, support for async provider factories.

The rest of the code for completeness...

In app/lib/angular/injection-tokens.ts:

import { InjectionToken } from '@angular/core';
import { IConfig } from '../config/config';

export const APP_CONFIG = new InjectionToken<IConfig>('app-config');

and in app/lib/config/config.ts I define the interface for my JSON config file:

export interface IConfig {
    name: string;
    version: string;
    instance: string;
    firebaseConfig: {
        apiKey: string;
        // etc
    }
}

Config is stored in assets/config/config.json:

{
  "name": "my-app",
  "version": "#{Build.BuildNumber}#",
  "instance": "localdev",
  "firebaseConfig": {
    "apiKey": "abcd"
    ...
  }
}

Note: I use an Azure DevOps task to insert Build.BuildNumber and substitute other settings for different deployment environments as it is being deployed.

Enhanced answered 28/5, 2020 at 4:35 Comment(2)
Thank you @Glenn, this is just what I needed.Fetich
Thanks, although there is an issue. configListener() should be configListener(response: any) and you should parse const configuration = JSON.parse(response.target.responseText) because there is no this.responseText. An to make it clear, you can actually use @Inject(APP_CONFIG) private config: IConfig in a constructor of any class (component, service) to get access to the config values.Considerate
D
8

Here's my solution, loads from .json to allow changes without rebuilding

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

@Injectable()
export class ConfigService {

    private config: any;

    constructor(private location: Location, private http: Http) {
    }

    async apiUrl(): Promise<string> {
        let conf = await this.getConfig();
        return Promise.resolve(conf.apiUrl);
    }

    private async getConfig(): Promise<any> {
        if (!this.config) {
            this.config = (await this.http.get(this.location.prepareExternalUrl('/assets/config.json')).toPromise()).json();
        }
        return Promise.resolve(this.config);
    }
}

and config.json

{
    "apiUrl": "http://localhost:3000/api"
}
Doubletime answered 28/2, 2018 at 17:46 Comment(6)
The problem with this approach is that config.json is open to the world. How would you prevent somebody to type www.mywebsite.com/assetts/config.json?Searcy
@AlbertoL.Bonfiglio you configure the server not to allow access from outside to config.json file (or place it in a directory which has no public access)Externalization
This is my favourite solution too, but still to concerned about the security risks.Taegu
Please, can u help me to get it right? How is it more risky than traditional for angular environments? The full content of environments.prod.ts after ng build --prod will be at some .js file at some point. Even if obfuscated, the data from environments.prod.ts will be in clear text. And as all .js files, it will be available on end user machine.Monocyte
@AlbertoL.Bonfiglio Because an Angular app is by nature a client application, and JavaScript will be used for passing around data and configuration, there should be no secret configuration used in it; all secret configuration definitions should be behind an API layer where a user's browser or browser tools cannot access it. Values like the base URI of an API are ok for the public to access because the API should have its own credentials and security based on a user logging in (bearer token over https).Chatham
Angular apps should not use resources (embedded or external) with sensitive information. An API endpoint is not that kind and is always a protected resource.Bimetallism
B
5

Quite a few articles are recommending that you get Angular config settings using an AppConfigService such as this one.

But I found that sometimes this just didn't work.

It was simpler, and more reliable to have a "config.json" file, then create a class which just read it in, and returned a value, eg my config file would look like this:

{
  "appName": "Mike's app",
  "version": "1.23.4",
  "logging_URL" : "https://someWebsite.azurewebsites.net/logs"
}

And I'd access the values using this:

import config from '../../assets/config.json';

@Injectable({
    providedIn: 'root'
})
export class AppConfigService {
    get appName() {
        return config.appName; 
    }
    get appVersion() {
        return config.version; 
    }
    get loggingUrl() {
        return config.logging_URL; 
    }
}

(A few months later...)

After patting myself on the back for making a simpler Angular solution, I realised this has a big drawback. If you use an AppConfigService, and you're using CI/CD, then you can get your build process to update the config .json file, and the Angular app will use these settings.

With my version, yes, it's simpler, but there is no config setting file to overwrite. For automated build processes, this might not be desirable.

Bernoulli answered 10/8, 2021 at 11:37 Comment(1)
I like this method, as it easier to integrate and does not show another http request and its values in the network tab.Deci
G
1

Here's my two solutions for this

1. Store in json files

Just make a json file and get in your component by $http.get() method. If I was need this very low then it's good and quick.

2. Store by using data services

If you want to store and use in all components or having large usage then it's better to use data service. Like this :

  1. Just create static folder inside src/app folder.

  2. Create a file named as fuels.ts into static folder. You can store other static files here also. Let define your data like this. Assuming you having fuels data.

__

export const Fuels {

   Fuel: [
    { "id": 1, "type": "A" },
    { "id": 2, "type": "B" },
    { "id": 3, "type": "C" },
    { "id": 4, "type": "D" },
   ];
   }
  1. Create a file name static.services.ts

__

import { Injectable } from "@angular/core";
import { Fuels } from "./static/fuels";

@Injectable()
export class StaticService {

  constructor() { }

  getFuelData(): Fuels[] {
    return Fuels;
  }
 }`
  1. Now You can make this available for every module

just import in app.module.ts file like this and change in providers

import { StaticService } from './static.services';

providers: [StaticService]

Now use this as StaticService in any module.

That's All.

Garganey answered 23/8, 2018 at 11:31 Comment(1)
Good solution since you don't have to recompile. Environment is like hard-coding it in the code. Nasty. +1Skean
A
1

We had this problem years ago before I had joined and had in place a solution that used local storage for user and environment information. Angular 1.0 days to be exact. We were formerly dynamically creating a js file at runtime that would then place the generated api urls into a global variable. We're a little more OOP driven these days and don't use local storage for anything.

I created a better solution for both determining environment and api url creation.

How does this differ?

The app will not load unless the config.json file is loaded. It uses factory functions to create a higher degree of SOC. I could encapsulate this into a service, but I never saw any reason when the only similarity between the different sections of the file are that they exist together in the file. Having a factory function allows me to pass the function directly into a module if it's capable of accepting a function. Last, I have an easier time setting up InjectionTokens when factory functions are available to utilize.

Downsides?

You're out of luck using this setup (and most of the other answers) if the module you want to configure doesn't allow a factory function to be passed into either forRoot() or forChild(), and there's no other way to configure the package by using a factory function.

Instructions

  1. Using fetch to retrieve a json file, I store the object in window and raise a custom event. - remember to install whatwg-fetch and add it to your polyfills.ts for IE compatibility
  2. Have an event listener listening for the custom event.
  3. The event listener receives the event, retrieves the object from window to pass to an observable, and clears out what was stored in window.
  4. Bootstrap Angular

-- This is where my solution starts to really differ --

  1. Create a file exporting an interface whose structure represents your config.json -- it really helps with type consistency and the next section of code requires a type, and don't specify {} or any when you know you can specify something more concrete
  2. Create the BehaviorSubject that you will pass the parsed json file into in step 3.
  3. Use factory functions to reference the different sections of your config to maintain SOC
  4. Create InjectionTokens for the providers needing the result of your factory functions

-- and/or --

  1. Pass the factory functions directly into the modules capable of accepting a function in either its forRoot() or forChild() methods.

-- main.ts

I check window["environment"] is not populated before creating an event listener to allow the possiblilty of a solution where window["environment"] is populated by some other means before the code in main.ts ever executes.

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { configurationSubject } from './app/utils/environment-resolver';

var configurationLoadedEvent = document.createEvent('Event');
configurationLoadedEvent.initEvent('config-set', true, true);
fetch("../../assets/config.json")
.then(result => { return result.json(); })
.then(data => {
  window["environment"] = data;
  document.dispatchEvent(configurationLoadedEvent);
}, error => window.location.reload());

/*
  angular-cli only loads the first thing it finds it needs a dependency under /app in main.ts when under local scope. 
  Make AppModule the first dependency it needs and the rest are done for ya. Event listeners are 
  ran at a higher level of scope bypassing the behavior of not loading AppModule when the 
  configurationSubject is referenced before calling platformBrowserDynamic().bootstrapModule(AppModule)

  example: this will not work because configurationSubject is the first dependency the compiler realizes that lives under 
  app and will ONLY load that dependency, making AppModule an empty object.

  if(window["environment"])
  {
    if (window["environment"].production) {
      enableProdMode();
    }
    configurationSubject.next(window["environment"]);
    platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.log(err));
  }
*/
if(!window["environment"]) {
  document.addEventListener('config-set', function(e){
    if (window["environment"].production) {
      enableProdMode();
    }
    configurationSubject.next(window["environment"]);
    window["environment"] = undefined;
    platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.log(err));
  });
}

--- environment-resolvers.ts

I assign a value to the BehaviorSubject using window["environment"] for redundancy. You could devise a solution where your config is preloaded already and window["environment"] is already populated by the time any of your Angular's app code is ran, including the code in main.ts

import { BehaviorSubject } from "rxjs";
import { IConfig } from "../config.interface";

const config = <IConfig>Object.assign({}, window["environment"]);
export const configurationSubject = new BehaviorSubject<IConfig>(config);
export function resolveEnvironment() {
  const env = configurationSubject.getValue().environment;
  let resolvedEnvironment = "";
  switch (env) {
 // case statements for determining whether this is dev, test, stage, or prod
  }
  return resolvedEnvironment;
}

export function resolveNgxLoggerConfig() {
  return configurationSubject.getValue().logging;
}

-- app.module.ts - Stripped down for easier understanding

Fun fact! Older versions of NGXLogger required you to pass in an object into LoggerModule.forRoot(). In fact, the LoggerModule still does! NGXLogger kindly exposes LoggerConfig which you can override allowing you to use a factory function for setup.

import { resolveEnvironment, resolveNgxLoggerConfig, resolveSomethingElse } from './environment-resolvers';
import { LoggerConfig } from 'ngx-logger';
@NgModule({
    modules: [
        SomeModule.forRoot(resolveSomethingElse)
    ],
    providers:[
        {
            provide: ENVIRONMENT,
            useFactory: resolveEnvironment
        },
        { 
            provide: LoggerConfig,
            useFactory: resolveNgxLoggerConfig
        }
    ]
})
export class AppModule

Addendum

How did I solve the creation of my API urls?

I wanted to be able to understand what each url did via a comment and wanted typechecking since that's TypeScript's greatest strength compared to javascript (IMO). I also wanted to create an experience for other devs to add new endpoints, and apis that was as seamless as possible.

I created a class that takes in the environment (dev, test, stage, prod, "", and etc) and passed this value to a series of classes[1-N] whose job is to create the base url for each API collection. Each ApiCollection is responsible for creating the base url for each collection of APIs. Could be our own APIs, a vendor's APIs, or even an external link. That class will pass the created base url into each subsequent api it contains. Read the code below to see a bare bones example. Once setup, it's very simple for another dev to add another endpoint to an Api class without having to touch anything else.

TLDR; basic OOP principles and lazy getters for memory optimization

@Injectable({
    providedIn: 'root'
})
export class ApiConfig {
    public apis: Apis;

    constructor(@Inject(ENVIRONMENT) private environment: string) {
        this.apis = new Apis(environment);
    }
}

export class Apis {
    readonly microservices: MicroserviceApiCollection;

    constructor(environment: string) {
        this.microservices = new MicroserviceApiCollection(environment);
    }
}

export abstract class ApiCollection {
  protected domain: any;

  constructor(environment: string) {
      const domain = this.resolveDomain(environment);
      Object.defineProperty(ApiCollection.prototype, 'domain', {
          get() {
              Object.defineProperty(this, 'domain', { value: domain });
              return this.domain;
          },
          configurable: true
      });
  }
}

export class MicroserviceApiCollection extends ApiCollection {
  public member: MemberApi;

  constructor(environment) {
      super(environment);
      this.member = new MemberApi(this.domain);
  }

  resolveDomain(environment: string): string {
      return `https://subdomain${environment}.actualdomain.com/`;
  }
}

export class Api {
  readonly base: any;

  constructor(baseUrl: string) {
      Object.defineProperty(this, 'base', {
          get() {
              Object.defineProperty(this, 'base',
              { value: baseUrl, configurable: true});
              return this.base;
          },
          enumerable: false,
          configurable: true
      });
  }

  attachProperty(name: string, value: any, enumerable?: boolean) {
      Object.defineProperty(this, name,
      { value, writable: false, configurable: true, enumerable: enumerable || true });
  }
}

export class MemberApi extends Api {

  /**
  * This comment will show up when referencing this.apiConfig.apis.microservices.member.memberInfo
  */
  get MemberInfo() {
    this.attachProperty("MemberInfo", `${this.base}basic-info`);
    return this.MemberInfo;
  }

  constructor(baseUrl: string) {
    super(baseUrl + "member/api/");
  }
}
Agouti answered 6/11, 2020 at 19:13 Comment(0)
I
0

I find this Angular How-to: Editable Config Files from Microsoft Dev blogs being the best solution. You can configure dev build settings or prod build settings.

Idolah answered 11/9, 2020 at 10:59 Comment(1)
That article is good, but you must build the app for each environment, which really goes against the 12 factor app notion. So if you had 'test' and then needed a 'automation' environment, you would need build a whole other copy to deploy. It would be better to pass that in to the app you had already build. But what makes some of this a challenge is that HTML, JS and CSS are basically served up statically.Aforethought
A
0

I found the 'Angular way' and 'React way' and all of that not very good - not following 12 factor app principles. Statically compiling environmenet settings into the app is too limiting. This is a general symtom of all the web frameworks that are essentially static content.

You can essentially have the config be a template that is edited at the start of the app. If running in a container such as a docker container, this article https://pumpingco.de/blog/environment-variables-angular-docker/ shows how to use envsubst on the entry point command to fix this static compile issue on the fly via environment variable. With this, it takes and writes variables into the 'production' Angualar config effectively.

This way you can have a 12 factor style app without having to know all the environments ahead of time and compiled in. It always runs in a 'production' mode but those values are different depending on container startup args.

Aforethought answered 17/8, 2022 at 21:34 Comment(4)
Randy, a link to a solution is welcome, but please ensure your answer is useful without it: add context around the link so your fellow users will have some idea what it is and why it is there, then quote the most relevant part of the page you are linking to in case the target page is unavailable. Answers that are little more than a link may be deleted.Wobbly
Was this flagged as too terse?Aforethought
Apart from the link you describe only what is achieved not how. The context you should provide is about "how" not only about "what".Wobbly
I am sorry, I thought that 'fix this static compile issue on the fly via environment variable' was the how 'summary'. I will add a little more.Aforethought

© 2022 - 2024 — McMap. All rights reserved.