'Angular, load json file before AppRoutingModule loads
I want to load a json config file before App starts so I can provide some services or others depending in the config options. I loaded it using APP_INITIALIZER token and all was OK until I needed conditional routing depending on the same config options. I have tried to load the config file vía APP_INITIALIZER in a Module and import this module to AppRoutingModule and main AppModule assuring the same instance was supplied for not loading the config file twice.
@NgModule({
declarations: [],
imports: [
CommonModule
],
providers: [
ConfigService,
{
provide: APP_INITIALIZER,
useFactory: (cs: ConfigService, http: HttpClient) => cs.loadConfig(http),
deps: [ConfigService, HttpClient],
multi: true
},
]
})
export class CoreModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: CoreModule,
providers: [
ConfigService
]
};
}
}
Here is ConfigService:
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable()
export class ConfigService {
proccesId: number;
constructor() {}
loadConfig(http: HttpClient) {
return () => {
return (http.get('/assets/app-config.json') as Observable<any>)
.toPromise()
.then((config) => {
this.processId = config.processId
});
};
}
}
Here is how I import CoreModule in AppModule (is imported in the same way in the AppRoutingModule):
export function OptionServiceFactory (cs: ConfigService, injector: Injector): OptionService {
if(cs.processId === 1) {
return injector.get(OneOptionService);
} else {
return injector.get(OtherOptionService);
}
}
@NgModule({
imports: [
CoreModule.forRoot(),
],
providers: [
OneOptionService,
OtherOptionService,
{
provide: OptionService,
useFactory: OptionServiceFactory,
deps: [ConfigService, Injector]
}],
bootstrap: [AppComponent]
})
export class AppModule { }
This worked for the main AppModule, the application waits until ConfigService is loaded with the JSON config file so I'm able to use JSON populated ConfigService in the factories. But AppRoutingModule doesn't wait and therefore any operation accesing ConfigService properties fail. This is AppRoutingModule:
const routes: Routes = [];
function routesFactory(cs: ConfigService): Routes {
let rutas: Routes = [
{ path: '', redirectTo: '/same-path', pathMatch: 'full' }
];
/* This fails because processId is undefined because JSON file has not been loaded jet */
if(cs.processId === 1) {
rutas.push({ path: 'same-path', component: OneComponent });
} else {
rutas.push({ path: 'same-path', component: OtherComponent });
}
return rutas;
}
@NgModule({
imports: [
CoreModule.forRoot(),
RouterModule.forRoot(routes)
],
exports: [RouterModule],
providers: [
{ provide: ROUTES, multi: true, useFactory: routesFactory, deps: [ConfigService] }
]
})
export class AppRoutingModule { }
As a workaround I have made a syncronous http call to load json file in AppRoutingModule. Now routesFactory in AppRoutingModule is:
function routesFactory(cs: ConfigService): Routes {
let rutas: Routes = [
{ path: '', redirectTo: '/same-path', pathMatch: 'full' }
];
/* Syncronous http request */
var request = new XMLHttpRequest();
request.open('GET', '/assets/app-config.json', false);
request.send(null);
if (request.status === 200) {
let config = JSON.parse(request.responseText);
cs.processId = config.processId;
}
/* Now it doesn't fail because we loaded json syncronously */
if(cs.processId === 1) {
rutas.push({ path: 'same-path', component: OneComponent });
} else {
rutas.push({ path: 'same-path', component: OtherComponent });
}
return rutas;
}
It works, but is a discouraged practice and now I get a warning in the browser console.
[Deprecation] Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help, check https://xhr.spec.whatwg.org/.
Is there a way to load JSON file before AppRoutingModule loads?
Thanks in advance
Solution 1:[1]
Romain Manni-Bucau explains how to load and update routes programatically in his blog entry https://rmannibucau.metawerx.net/angular-ng-10-create-update-route-at-runtime.html
I reproduce here the basic steps in case the blog is not available.
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router, Routes } from '@angular/router';
import { from } from 'rxjs';
import { switchMap, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class RouteLoader {
constructor(
private client: HttpClient,
private router: Router) /* 1 */ { }
public load() {
return this.client.get('/my-routes')
.pipe(switchMap(json => this.createRoutes(json)));
}
private createRoutes(json) {
return from(json.routeSpecs).pipe( // 2
map(spec => this.toRoutes(spec)), // 3
map(routes => ([ // 4
...this.router.config,
...routes,
])),
map(newRoutes => this.router.resetConfig(newRoutes)) // 5
);
}
}
We inject the route in the service constructor,
Once we retrieved our backend model we create an observable from it (makes the DSL easier and integrates better with load() function as we'll see soon but can be done with a plain loop if you prefer),
We convert the backend route specification to actual routes (we'll explain that just after that),
We flatten the existing/bootstrap routes with the new routes,
Finally we update the router with the new routes. Note that this is also a place you can store the route you loaded and store them in the RouteLoader service. This is very very useful if you build dynamically a menu from what you fetched and it enables to inject the service in any component and dynamically create the menu (with a *ngFor for example) without having to use a global variable (as the original fetch option would have required).
providers: [ // 1 { provide: APP_INITIALIZER, useFactory: ensureRoutesExist, // 2 multi: true, deps: [HttpClient, RouteLoader], // 3 } ], bootstrap: [AppComponent] }) export class AppModule { }
1.We register a custom provider with the token APP_INITIALIZER (ensure to set multi to true since you can get multiple initializers per application),
2.We bind its factory to a custom method,
3.We bind the factory parameters - which is a plain function in this case and requires this explicit injection binding.
export function ensureRoutesExist( // 1
http: HttpClient,
routeLoader: RouteLoader) {
return () => routeLoader.load() // 2
.toPromise(); // 3
}
The factory is a plain function which returns a function returning a promise, it takes as parameters the services/providers we bound in the application module during the provider registration,
We want to wait for the route loading so we call the load function to execute it during application initialization,
We convert our observable to a promise to ensure angular waits for this logic before finishing to start.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | mattinsalto |
