I have an angular 4 app that is broken down into different feature. So I have a shopping feature with all its components and billing feature with all its component.
However, I have an issue with configuration. I would like to share configuration between multiple feature of the app. My configuration is different for each feature so I created a base config class and inherit from it.
However in my app.module is did a registration such as below.
{ provide: BillingModuleConfig, useClass: ConfigService },
{ provide: ShoppingConfig, useClass: ConfigService }
{
provide: APP_INITIALIZER,
useFactory: configServiceFactory,
deps: [ShoppingConfig],
multi: true
}
However, this does not work for the BillingModuleConfig. Which implies when I inject BIllingConfig in the section I need it from I cannot get the config setting. Is there a reason why this would not work? My ConfigServiceFactory is the an external function that load the config file as below.
export function configServiceFactory(config: ConfigService) {
return () => config.load();
}
The config.load method loads all the configuration into a class that inherits from both ShoppingConfig and BillingConfig.
The config.load implementation is
import { Injectable, OnInit } from '#angular/core';
import { Http } from '#angular/http';
import { BillingModuleConfig} from '#razor/Billing';
import { ShoppingConfig } from '#razor/shopping';
import { environment } from './../../environments/environment';
import { HttpClient } from '#angular/common/http';
abstract class Settings implements BillingModuleConfig, ShoppingConfig {
public ShoppingAPIUrl: string;
public BillingApiUrl: string;
}
#Injectable()
export class ConfigService implements Settings {
constructor(private http: HttpClient) {}
public load() {
return new Promise((resolve, reject) => {
this.http
.get<Settings>(environment.configFileName)
.subscribe((config: AppSettings) => {
this.ShoppingAPIUrl= config.ShoppingAPIUrl;
this.BillingApiUrl= config.BillingApiUrl;
resolve(true);
});
});
}
}
Considering that config.load assigns fetched configuration to ConfigService class instance, it will work only for provider token that was specified as a dependency for APP_INITIALIZER, which is ShoppingConfig.
In order for the configuration to be fetched in BillingModuleConfig, it should be specified, too:
{
provide: APP_INITIALIZER,
useFactory: configServiceFactory,
deps: [ShoppingConfig],
multi: true
},
{
provide: APP_INITIALIZER,
useFactory: configServiceFactory,
deps: [BillingModuleConfig],
multi: true
}
However, this will result in two requests. To avoid this, the most simple way is to use ConfigService everywhere. If BillingModuleConfig and ShoppingConfig were introduced because the configuration is currently combined but supposed to be divided later, they can reuse a common instance:
ConfigService,
{ provide: BillingModuleConfig, useExisting: ConfigService },
{ provide: ShoppingConfig, useExisting: ConfigService },
{
provide: APP_INITIALIZER,
useFactory: configServiceFactory,
deps: [ConfigService],
multi: true
}
Related
I want to ensure a singleton service is created on application boot. I could add it as injection parameter to my AppComponent and not use it at all, but that looks a bit dirty. Right now I'm going with this solution:
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '#angular/core';
import { NavigationService } from './navigation.service';
#NgModule()
export class NavigationServiceModule {
public static forRoot(): ModuleWithProviders<NavigationServiceModule> {
return {
ngModule: NavigationServiceModule,
providers: [
{
provide: APP_INITIALIZER,
deps: [NavigationService],
multi: true,
useFactory: () => () => { }
}
]
}
}
}
But don't really love it, too. Any ideas how this could be achieved best?
Just use the default service setup and don't add it to the providers array - only one instance will be created (unless you explicitly provide it outside of constructors)
#Injectable({
providedIn: 'root'
})
export class NavigationService
I tried to implement dynamic configuration as can be seen in this post.
Everything works in JiT compiler, but I get
ERROR in Error during template compile of 'environment'
Function calls are not supported in decorators but 'Environment' was called.
when trying to build with the AoT compiler.
This is my environment.ts (note class Environment is exported):
export class Environment extends DynamicEnvironment {
public production: boolean;
constructor() {
super();
this.production = false;
}
}
export const environment = new Environment();
I would still like to use the environment in the standard way some.component.ts:
import { environment } from '../environments/environment';
console.log(environment.config.property);
Don't. Seriously, stay away from those two files (environment.ts and environment.prod.ts). Those are NOT about the DevOps meaning of the word "environment", they are about debug constants.
If you need to know if you're running a debug build, import isDevMode:
import { isDevMode } from '#angular/core';
If you need dynamic configuration, just read a Json from somewhere or have the server side inject it as a script tag, then read it directly or via Dependency Injection (it's not that hard to do).
But don't mess with those files. Trust me, you'll thank me later ;)
Solved this by creating config.module.ts and config.service.ts. Config module declares providers:
#NgModule({
providers: [
ConfigService,
{
provide: APP_INITIALIZER,
useFactory: (appConfigService: ConfigService) => () => appConfigService.loadAppConfig(),
deps: [ConfigService],
multi: true,
},
],
})
export class ConfigModule {}
Usage of config service in some.component.ts:
#Component(...)
export class SomeComponent {
constructor(private configService: ConfigService) { }
private myMethod() {
console.log(this.configService.get.property);
}
}
For tests, json testing config file is imported:
import { default as appTestConfig } from '../../../../assets/app-config.test.json';
and set directly on config service:
TestBed.configureTestingModule({
...,
imports: [
ConfigModule,
...
],
providers: [
{
provide: APP_INITIALIZER,
useFactory: (appConfigService: ConfigService) => () => appConfigService.setConfig(appTestConfig),
deps: [ConfigService],
multi: true,
},
]
}).compileComponents();
I created Angular 5 project and create one factory which should provide metadata from service before application start.
The problem is that I get error:
RangeError: Maximum call stack size exceeded
at resolveNgModuleDep (core.js:10559)
at _callFactory (core.js:10649)
at _createProviderInstance$1 (core.js:10599)
at resolveNgModuleDep (core.js:10581)
at _createClass (core.js:10622)
at _createProviderInstance$1 (core.js:10596)
at resolveNgModuleDep (core.js:10581)
at NgModuleRef_.get (core.js:11806)
at new MetadataService (metadata.service.ts:23)
at _createClass (core.js:10622)
App module:
#NgModule({
declarations: [
AppComponent,
...
],
imports: [
BrowserModule,
HttpClientModule,
AppRoutingModule,
FormsModule,
FlexLayoutModule,
I18NextModule.forRoot()
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
MetadataService,
{ provide: APP_INITIALIZER, useFactory: MetadataProviderFactory, deps: [MetadataService], multi: true },
{ provide: APP_INITIALIZER, useFactory: I18NextProviderFactory, deps: [I18NEXT_SERVICE], multi: true }
],
bootstrap: [AppComponent]
})
export class AppModule { }
Metadata service:
import { Injectable, Injector } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import { HttpClient } from '#angular/common/http';
#Injectable()
export class MetadataService {
private http: HttpClient;
constructor(private injector: Injector) {
this.http = injector.get(HttpClient); // this row makes a problem
}
This row makes a problem.
this.http = injector.get(HttpClient);
I put this line avoid circular reference and I get "Maximum call stack size exceeded".
Where I am wrong ?
You can temporarily get over this by delaying your http call that you make on service init, with a simple setTimeOut (don't need any seconds). I've gone through a few open Angular threads and the main issue has to do with HttpInterceptor and circular depedenecy with the HttpClient.
I have the following two environments in my angular-cli (v1.5.1, angular v5) application:
dev
prod
Dev makes use of mock data, which I provide with an http-interceptor.
Pro makes use of a live rest api.
How do I provide the http-interceptor on dev, but not on pro?
I already tried the following, but it doesn't work:
{
provide: HTTP_INTERCEPTORS,
useFactory: () => {
if (environment.useMockBackend === true) {
return MockHttpInterceptor;
}
return false;
},
multi: true
}
In my Angular 5.2 project I used following approach.
app.module.ts
import { HttpClientModule, HTTP_INTERCEPTORS } from '#angular/common/http';
import { environment } from '../environments/environment';
import { MyInterceptor } from './my.interceptor';
const commonProviders = [/*...*/];
const nonProductionProviders = [{
provide: HTTP_INTERCEPTORS,
useClass: MyInterceptor,
multi: true
}];
#NgModule({
imports: [
HttpClientModule,
// ...
],
providers: [
...commonProviders,
...!environment.production ? nonProductionProviders : []
]
})
my.interceptor.ts
import { Injectable } from '#angular/core';
import { HttpEvent, HttpRequest, HttpInterceptor, HttpHandler } from '#angular/common/http';
import { Observable } from 'rxjs';
#Injectable()
export class MyInterceptor implements HttpInterceptor {
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
// ...
return next.handle(req);
}
}
I've come up with the following approach (this is in Angular 7), by drawing on the previous answers from #dhilt and #kemsky:
Your dev environment file
import { HTTP_INTERCEPTORS } from '#angular/common/http';
import { MyDevInterceptor} from './my-dev.interceptor';
export const ENVIRONMENT_SPECIFIC_PROVIDERS = [
{ provide: HTTP_INTERCEPTORS, useClass: MyDevInterceptor, multi: true }
];
environment.prod.ts
export const ENVIRONMENT_SPECIFIC_PROVIDERS = [];
app.module.ts
#NgModule({
declarations: [],
imports: [
HttpClientModule
],
providers: [
ENVIRONMENT_SPECIFIC_PROVIDERS
]
})
It's simple, it works a treat, and it means that your code base contains no references to anything that's not required by your environment.
The idea is to export interceptor providers from environment file, prod environment exports do-nothing interceptor or just any other dummy provider (lets name it DefaultHttpInterceptor) and dev exports MockHttpInterceptor.
dev environment: export const INTERCEPTORS = {provide: HTTP_INTERCEPTORS, ... MockHttpInterceptor}
prod environment: export const INTERCEPTORS = {provide: HTTP_INTERCEPTORS, ... DefaultHttpInterceptor}
Then you can use it like usual:
import { INTERCEPTORS } from './../environments/environment';
#NgModule({
providers : [
...
INTERCEPTORS
...
]
...
})
I have an injectable authentication service written for Angular 4. The code looks similar to the following:
auth.service.ts
import { CookieService } from 'ngx-cookie';
import { Identity } from './auth.identity';
export function authInit(authService: AuthService): () => Promise<any> {
return (): Promise<any> => authService.checkAuthenticated();
}
#Injectable()
export class AuthService {
identity: Identity;
isAuthenticated:boolean = false;
apiUrl: string = 'https://myUrl/api';
constructor(private _http: HttpClient, private _cookieService: CookieService) {
this.identity = new Identity();
}
checkAuthenticated(): Promise<any> {
return new Promise((res, rej) => {
let identity = this._cookieService.getObject('myToken');
if (!!identity) {
this.setAuthenticated(identity);
}
});
}
login(username: string, password: string) {
let creds = {
username: username,
password: password
};
this._http.post<any>(this.apiUrl + '/auth/login', creds).subscribe(data => {
this.setAuthenticated(data);
});
}
logout() {
}
private setAuthenticated(data: any) {
this._cookieService.putObject('myToken', data);
this.isAuthenticated = true;
// hydrate identity object
}
}
auth.module.ts
import { NgModule, APP_INITIALIZER } from '#angular/core';
import { CommonModule } from '#angular/common';
import { AuthService, authInit } from './auth.service';
#NgModule({
imports: [CommonModule],
providers: [
AuthService,
{
provide: APP_INITIALIZER,
useFactory: authInit,
deps: [AuthService],
multi: true
}
]
})
export class AuthModule { }
The idea is that when the app loads, I want to be able to check the local storage (cookies, sessionStorage or localStorage) to see if the value exists. (This is demonstrated by the commented if statement in the constructor.) Based on the isAuthenticated property I want to be able to show specific content.
Currently, if I uncomment the lines in the constructor, I'll get an exception document.* is not defined. I know what that means. Unfortunately, I don't know how to accomplish what I'm going for.
Keep in mind, this is a service and not a view component, so there's no ngOnInit method available.
EDITED
So I've added the factory provider as suggested. However, I'm still getting the exception: document is not defined
Thanks!
When you have a service that you need to have run before everything else might be initialized you can use the APP_INITIALIZER token (the documentation is sparse to say the least :)
The gist is that in your application providers array you add a factory provider:
{
provide: APP_INITIALIZER,
useFactory: authInit,
deps: [AuthService],
multi: true
}
Make sure to have provide set specifically to APP_INITIALIZER and the multi value to true. The authInit function is factory that returns a function that returns a promise. It has to return a promise and not an observable. It would be something like:
export function authInit(authServ: AuthService) {
return () => authServ.check();
}
The authServ.check() function is where you can put the logic you currently have commented in your service (just make sure it returns a promise as the result). Setting it up this way will let that logic run while the application loads.
Edit: Now that I take a look at the app.module.ts add the initialization of the cookie service and add the BrowserModule:
import { NgModule, APP_INITIALIZER } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
import { CommonModule } from '#angular/common';
import { CookieModule } from 'ngx-cookie';
import { AuthService, authInit } from './auth.service';
#NgModule({
imports: [BrowserModule, CommonModule, CookieModule.forRoot()],
providers: [
AuthService,
{
provide: APP_INITIALIZER,
useFactory: authInit,
deps: [AuthService],
multi: true
}
]
})
export class AuthModule { }
Also, make sure to add ngx-cookie to your systemjs.config.js (if that's what you're using as your loader).