App.settings - the Angular way? - javascript
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.
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:
Add a JSON config file to the /src/assets/ folder (so that is copied on build)
Create an AppConfigService to load and distribute the config
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.
If you are using angular-cli, 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).
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.
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:
kicks off an async request for the config.json file.
When the request completes, parses the JSON into a Javascript object
provides the value using the APP_CONFIG injection token, prior to bootstrapping.
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.
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
}
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"
}
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.
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 :
Just create static folder inside src/app folder.
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" },
];
}
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;
}
}`
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.
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
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
Have an event listener listening for the custom event.
The event listener receives the event, retrieves the object from window to pass to an observable, and clears out what was stored in window.
Bootstrap Angular
-- This is where my solution starts to really differ --
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
Create the BehaviorSubject that you will pass the parsed json file into in step 3.
Use factory functions to reference the different sections of your config to maintain SOC
Create InjectionTokens for the providers needing the result of your factory functions
-- and/or --
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/");
}
}
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.
If running in a 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.
Related
How to inject service inside a module function NestJS
I'm using pino-logger in my NestJS project to log the activities in my application, and I'm logging the object along with ReqId so I can trace the whole activity inside one request. I'd like to use the same "ReqId" in another place as well, but I'm unsure of how to move it outside of the module, so for that, I'm thinking to save that generated ReqId into the CacheManager but not sure how to inject CacheManager class inside genReqId function. Please look over the code below. app.module.ts #Module({ imports: [ LoggerModule.forRoot({ pinoHttp: { genReqId: (req: any) => { // I'm thinking to use CacheManager here but I'm not sure how to inject CacheManager class here return req.headers.req_id || uuid(); // from here it generate the request ID and I want to export this ID and use in side an another class }, base: undefined, quietReqLogger: true, timestamp: false, }, }), ], }) export class AppModule {}
you need To create sharable service and import it Imports #Injectable() export class RequestIdService { private reqId: string; setRequestId(reqId: string) { this.reqId = reqId; } getRequestId() { return this.reqId; } } than import it to logger module imports: [ LoggerModule.forRoot({ pinoHttp: { genReqId: (req: any) => { this.requestIdService.setRequestId(req.headers.req_id || uuid()); return this.requestIdService.getRequestId(); }, base: undefined, quietReqLogger: true, timestamp: false, }, }), ], providers: [RequestIdService], ``` use that service by import { RequestIdService } from './request-id.service'; this.requestIdService.getRequestId()
How can I separate the firebase credentials from the production build that I get in Angular 2+? [duplicate]
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.
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: Add a JSON config file to the /src/assets/ folder (so that is copied on build) Create an AppConfigService to load and distribute the config 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.
If you are using angular-cli, 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).
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.
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: kicks off an async request for the config.json file. When the request completes, parses the JSON into a Javascript object provides the value using the APP_CONFIG injection token, prior to bootstrapping. 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.
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 }
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" }
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.
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 : Just create static folder inside src/app folder. 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" }, ]; } 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; } }` 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.
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 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 Have an event listener listening for the custom event. The event listener receives the event, retrieves the object from window to pass to an observable, and clears out what was stored in window. Bootstrap Angular -- This is where my solution starts to really differ -- 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 Create the BehaviorSubject that you will pass the parsed json file into in step 3. Use factory functions to reference the different sections of your config to maintain SOC Create InjectionTokens for the providers needing the result of your factory functions -- and/or -- 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/"); } }
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.
If running in a 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.
StaticInjectorError[HttpClent]: Function/class not supported
I am trying to inject the HttpClientModule manually which runs outside independent(may be!) from app. Before static injector i was using reflective injector and code was working fine but now that reflective injector has been deprecated and I want to update my code with static injector. //appInjector.ts export class AppInjector { private static _instance: AppInjector = new AppInjector(); private _injector; constructor() { console.log('app-injector'); AppInjector._instance = this; this._injector = ReflectiveInjector.resolveAndCreate([ ...[getAnnotations(HttpClientModule)[0].providers], MY_HTTP_DEPENDENT_PROVIDERS ]); static getInstance(): AppInjector { return AppInjector._instance; } get(cls: any): any { return this._injector.get(cls); } } //someFile.ts const translate = AppInjector.getInstance().get(TranslateResource); refer This Post for annotation fn. Now when i am trying to use Http client with static inject it gives error: StaticInjectorError[HttpClent]: Function/class not supported //app module #NgModule({ imports: [], declarations: [], providers: [], entryComponents: [App] }) export class AppModule { ngDoBootstrap(app) { console.log('bootstrapping'); app.bootstrap(App); } so if i log it will log app-injector and then bootstrapping.
StaticInjector is supposed to be a replacement for ReflectiveInjector that doesn't require Reflect API. getAnnotations is low-level hack and it likely won't work with StaticInjector in its current state. Also, getAnnotations is incompatible with AOT by design. It's preferable to create an injector for a module the way it is supposed to be done by the framework, i.e. a module should be bootstrapped. Since there is no component to bootstrap, ngDoBootstrap hook should be specified. By default, bootstrap process is asynchronous. If this is not a problem, initialization promise can be chained to get module instance. An example: #NgModule({ imports: [BrowserModule, HttpClientModule] }) export class MyHttpModule { static httpClient?: HttpClient; httpClient?: HttpClient; constructor(private _injector: Injector) {} ngDoBootstrap() { MyHttpModule.httpClient = this.httpClient = this._injector.get(HttpClient); } } platformBrowserDynamic().bootstrapModule(MyHttpModule) .then((myHttpModule: NgModuleRef<MyHttpModule>) => { // HttpClient instance is available here const httpClient = myHttpModule.instance.httpClient; httpClient.get('/foo', { responseType: 'text'}).subscribe(); }) .catch(err => console.error(err)); This approach is compatible with both JIT and AOT (which is good for using HttpClient apart from Angular because this lowers the footprint significantly) out of the box. Otherwise custom synchronous bootstrap routine can be performed instead. This is possible because HttpClient doesn't require asynchronous initialization. An example: #NgModule({ imports: [BrowserModule, HttpClientModule] }) export class MyHttpModule { static httpClient?: HttpClient; constructor(public _injector: Injector) { MyHttpModule.httpClient = this._injector.get(HttpClient); } ngDoBootstrap() {} } const platform = platformBrowserDynamic(); const compiler = platform.injector.get(CompilerFactory).createCompiler(); const moduleFactory = compiler.compileModuleSync(MyHttpModule); platform.bootstrapModuleFactory(moduleFactory) .catch(err => console.error(err)); const httpClient = MyHttpModule.httpClient; httpClient.get('/foo').subscribe(); This will work in JIT, but AOT can't be efficiently handled by Angular CLI in the code above. The code involves the compiler, which isn't needed in AOT compilation mode (that's its purpose). In order to use AOT, it should be compiled with ngc compiler and a separate entry point that uses module factories should be created. Bootstrap routine becomes even simpler, because it doesn't involve compiler, something like: ... import { platformBrowser } from '#angular/platform-browser-dynamic'; import { AppModuleNgFactory } from '<path to aot>/src/app/my-http-module.ngfactory'; const platform = platformBrowser(); platform.bootstrapModuleFactory(AppModuleNgFactory) .catch(err => console.error(err)); const httpClient = MyHttpModule.httpClient; httpClient.get('/foo').subscribe();
Angular 2 Snackbar - Global Duration Config
I can set the duration of a snackbar message like so let config = new MdSnackBarConfig(); config.duration = 5000; this.snackBar.open(element.text, 'OK', config); However I need to set the duration for multiple snackbars and would rather not have to pass in the config each time. Can I somehow set a global duration config? Thanks!
I know this post is from a few years ago but for future reference i'm going to answer this anyway. Hoping I help someone who comes across this post like I did. You can now inject the MatSnackBar with default options for modules by using the providers in your #NgModule: import { MatSnackBarModule, MAT_SNACK_BAR_DEFAULT_OPTIONS } from '#angular/material'; #NgModule({ declarations: [], imports: [ MatSnackBarModule ], exports: [ MatSnackBarModule ], providers: [ { provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, useValue: { duration: 2500 } } ] }) Source: Material Angular doc's
What we've done is included an external app.config.ts file at the module level and include it where we need it. This is an example of what's in the file. export class Config { static apiUrl = "api/"; static token = ""; static snackBarDurration = 5000; ...... } Then, all you have to do is declare it in your module and then import it in the component or service in which you want to use it. Here is an example. import { Injectable } from "#angular/core"; import { Config } from "../../app.config"; #Injectable() export class SnackBarService { .... // declare your snackbar here constructor() { } showSnackBar(elementText: string){ let config = new MdSnackBarConfig(); config.duration = Config.snackBarDurration; // Here is your change this.snackBar.open(elementText, 'OK', config); } }
I know it's not solution for a global configuration, but to make invocation more compact I declared config in params: this.snackBar.open(elementText, 'OK', { duration: 3000 });
Using generated Swagger TypeScript Rest Client in Angular2
I am working on a Meteor web application using Angular 2 and TypeScript. For using a REST API, I have generated client code with Swagger Codegen. Unfortunately there is no sample on GitHub, how to use the rest client. I have an angular 2 view component which is injecting an Angular 2 service (ProductTreeService) and the generated API (both marked as "#Injectable"): #Component({ selector: 'panel-product-selection', template, providers: [ProductTreeService, UserApi], directives: [ProductTreeComponent] }) export class PanelProductSelectionComponent { private categoriesProductsTree: Collections.LinkedList<CategoryTreeElement>; private productTreeService: ProductTreeService; private userApi: UserApi; constructor(productTreeService: ProductTreeService, userApi: UserApi) { this.productTreeService = productTreeService; this.userApi = userApi; } ngOnInit(): void { //... } } While the angular service only is accessable and all is working fine, the application is crashing when I inject UserApi. The only difference between UserApi and ProductTreeService is the constructor: The service has no constructor parameters, the generated Api class has: constructor(protected http: Http, #Optional()#Inject(BASE_PATH) basePath: string) { if (basePath) { this.basePath = basePath; } } So how can I inject the generated API?
After further researches I got the solution. The constructor of the rest client UserApi is expecting an HTTP provider as following: constructor(protected http: Http, #Optional()#Inject(BASE_PATH) basePath: string) { if (basePath) { this.basePath = basePath; } } Coming from Java, I thought maybe this provider has to be initialized somehow in the constructor which is injecting this API. In actual fact, this initialization has to be done in the NgModule containing the injecting component, as described in this thread. This solution worked for me.
I would use the environment.ts file with a property for the url: export const environment = { production: false, basePath: 'https://virtserver.swaggerhub.com/user/project/1.0.0' }; And then in the app.module you provide the config to the service: providers: [ { provide: BASE_PATH, useValue: environment.basePath }, ], This works with Swagger generated code. Hope that helps! (updated to Angular6+) https://blog.angulartraining.com/how-to-manage-different-environments-with-angular-cli-883c26e99d15
Old question but ... that's how you should do it: providers: [ { provide: API_BASE_URL, // or BASE_PATH useFactory: () => { return environment.apiBaseUrl // or return just "your base url as text" } }]