I have an Angular application where I want to mount a bunch of variables from a JSON file into a configService that the angular app gets from its own web server, so I can inject it in a container as a configMap.
Then I want this service and the config member property of the service to be accessible throughout the application.
This is what I have as of now:
@Injectable({
providedIn: 'root'
})
export class ConfigService {
configUrl = '/config/cfg.json';
public config: Config | undefined;
constructor(private httpClient: HttpClient) {
}
getConfig(): Observable<Config> {
return this.httpClient.get<Config>(window.location.origin + this.configUrl)
.pipe(
catchError(this.handleError<Config>("getConfig", this.config = { apiBaseUrl: "", adminLoginUrl: "", clientLoginUrl: "" }))
)
}
getConfigObject() : Config | undefined{
if (this.config?.apiBaseUrl == undefined) {
this.getConfig().subscribe((data: Config) => this.config = {
apiBaseUrl: data.apiBaseUrl,
adminLoginUrl: data.adminLoginUrl,
clientLoginUrl: data.clientLoginUrl
})
}
return this.config;
}
//error handling omitted
}
When calling subscribe on the getConfig() method, I eventually get the data but I can't use it to immediately afterward call a function on that apiBaseUrl, it's then "undefined". When I try to call getConfigObject() it doesn't return the object, at least not at that moment in time.
What I want to do is to use this config.apiBaseUrl when I call my backend api. So I can do this in another service/component/module:
@Injectable({
providedIn: 'root'
})
export class InvoiceService {
config: Config | undefined;
invoices: InvoiceItem[] | undefined;
confSub!: Subscription;
invSub!: Subscription;
//Idea 1, set the local config object in the constructor, does not work:
constructor(private configSvc: ConfigService, private httpClient: HttpClient) {
this.config = this.configSvc.getConfigObject()
}
getInvoices(): Observable<InvoiceItem[]> {
this.config = this.configSvc.config
console.log(this.config?.apiBaseUrl + "api/invoices");
//This above log results in just "api/invoices" in the console so the apiBaseUrl isn't loaded yet.
return this.httpClient.get<InvoiceItem[]>(this.config?.apiBaseUrl + "api/invoices")
.pipe(
catchError(this.handleError("getInvoices", this.invoices = []))
);
}
//idea 2: Get the configObject in the ngOnInit() (which I have, maybe wrongfully, added to a service) by calling the method that returns an observable.
ngOnInit(): void {
this.confSub = this.configSvc.getConfig()
.subscribe({
next: (data: Config) => this.config = data,
error: (err: Error) => console.log("Could not get config, " + err)
});
}
I somehow got it working yesterday by nesting the calls to configSvc.getConfig() and invoiceService.getInvoices() but that resulted in spamming the invoices endpoint and isn't a solution that I'd expect to be using.
According to the angular documentation, an injectable service is supposed to be a singleton which makes me expect that the ngOnInit() method calling this.getConfig() would populate the instance variable "config" with values and then this instance would be accessible and usable by injecting it in the constructor as private configSvc: ConfigService
and then by calling this.configSvc.config.apiBaseUrl
but that doesn't work either, it returns undefined.
I'm using the latest stable angular version.
What am I missing?
Edit: I am aware that inside:
this.confSub = this.configSvc.getConfig()
.subscribe({
next: (data: Config) => {
//HERE
this.config = data,
error: (err: Error) => console.log("Could not get config, " + err)
});
the config data is accessible and I could, in theory, call the backend to get invoices but I can't then return that as an observable from the getInvoices function. This doesn't work:
return this.configSvc.getConfig()
.subscribe({
next: (data: Config) => {
this.config = data;
return this.httpClient.get<Config>(window.location.origin + this.configUrl)
.pipe(
catchError(this.handleError<Config>("getConfig", this.config = { apiBaseUrl: "", adminLoginUrl: "", clientLoginUrl: "" }))
)
},
error: (err: Error) => console.log("Could not get config, " + err)
});
As it would return the config observable and not the InvoiceItem[] observable. I'm going to need the backend api url in multiple places so I just want to be able to inject it and use it like this:
constructor(private httpClient: HttpClient, private configSvc: ConfigService) {}
...
return this.httpClient.get<something>(this.configSvc.config?.apiBaseUrl + "/some/endpoint")
Like a singleton object that is mounted at startup and stays alive and accessible throughout the life cycle. Nesting calls to configService every time I want to get something from a backend endpoint seems unnecessary, or I don't quite understand how this works.