Routing Module loads before APP_INITIALIZER - javascript

I have a value that is from of a config file from static AppConfigService.
Described below:
reference code/article: https://blogs.msdn.microsoft.com/premier_developer/2018/03/01/angular-how-to-editable-config-files/
import { Injectable } from '#angular/core';
import { AppConfig } from './app-config';
import { HttpClient } from '#angular/common/http';
import { environment } from 'src/environments/environment';
#Injectable()
export class AppConfigService {
static settings: AppConfig;
constructor(private http: HttpClient) { }
load() {
console.log('is this getting fired before routing module check?');
const jsonFile = `assets/config/config.${environment.name}.json`;
return new Promise<void>((resolve, reject) => {
this.http.get(jsonFile)
.toPromise()
.then((response: AppConfig) => {
AppConfigService.settings = <AppConfig>response;
console.log(AppConfigService.settings);
resolve();
})
.catch((response: any) => {
reject(`Could not load file '${jsonFile}':
${JSON.stringify(response)}`);
});
});
}
}
This config gets loaded in my APP_INITIALIZER in the app.module.ts
providers: [
AppConfigService,
{
provide: APP_INITIALIZER,
useFactory: (appConfigService: AppConfigService) => () => {appConfigService.load() },
deps: [AppConfigService], multi: true
}
],
but my routing module, named AppRoutingModule is reading something out of my AppConfigService.settings variable which is crazy enough, UNDEFINED. My application crashes. I expect the APP_INITIALIZER to fire BEFORE AppRoutingModule but this is not the case:
Uncaught TypeError: Cannot read property 'oldUrl' of undefined
oldUrl is a property of AppConfigService.settings. I checked if AppConfigService.settings is set, it IS, properly AFTER routing module is fired but this is not what I want.
I checked some other sources for help. I used the following already as maybe a fix: https://github.com/angular/angular/issues/14615 and https://github.com/angular/angular/issues/14588
#component({})
class App {
constructor(router: Router, loginService: LoginService) {
loginService.initialize();
router.initialNavigation();
}
}
#NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(routes, {initialNavigation: false})
],
declarations: [ App ],
bootstrap: [ App ],
providers: [ Guard, LoginService ]
})
export class AppModule {
}
Unfortunately, the above solution is not fixing my problem. I also tried to put in AppModule but alas, that didn't help either.
Any help is very welcome.

I've solved my App Initialization and Routing with NgRx listening the central state to know when the system is Loaded and activating the route Guards after that.
But for a direct solution, you need to add a Route Guard checking when your service is loaded. So, add a loaded: boolean flag in your Service, and check it from a Guard like this:
https://github.com/angular/angular/issues/14615#issuecomment-352993695
This is better handled with Observables tho, and I'm wiring all with NgRx in my Apps using Facades to facilitate everything:
https://gist.github.com/ThomasBurleson/38d067abad03b56f1c9caf28ff0f4ebd
Best regards.

Related

Clean solution to ensure a singleton service is created on app initialization

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

Angular - Apollo: Client has not been defined yet

I'm using apollo client for graphql. I set up the client in AppApolloModule that I'm importing in AppModule. I'm making a query in a service which is also imported right in the AppModule. Although the service runs before the AppApolloModule runs and hence apollo is not initialized when the query is made and I get this error
Error: Client has not been defined yet
AppApolloModule
imports ....
export class AppApolloModule {
constructor(
apollo: Apollo,
httpLink: HttpLink,
private userService: UserService
) {
console.log("apollo module")
apollo.create({
link: httpLink.create({ uri: `${environment.apiBase}/graphql?${this.myService.token}`}),
cache: new InMemoryCache()
})
}
}
App Module
import { AppApolloModule } from './app.apollo.module';
import { MyService } from './services/my.service';
export class AppModule {
constructor() {
console.log("app module")
}
}
I don't get the two consoles app module and apollo module, since the service runs first, it doesn't find any initialized apollo app and thus breaks the code.
How can I make apollo run before the service or any services for that matter in an efficient and standard way?
This will solve the issue nicely:
import {NgModule} from '#angular/core';
import {HttpClientModule} from '#angular/common/http';
import {ApolloModule, APOLLO_OPTIONS} from 'apollo-angular';
import {HttpLink, HttpLinkModule} from 'apollo-angular-link-http';
import {InMemoryCache} from 'apollo-cache-inmemory';
export function createApollo(httpLink: HttpLink) {
return {
link: httpLink.create({uri: 'https://api.example.com/graphql'}),
cache: new InMemoryCache(),
};
}
#NgModule({
imports: [HttpClientModule, ApolloModule, HttpLinkModule],
providers: [
{
provide: APOLLO_OPTIONS,
useFactory: createApollo,
deps: [HttpLink],
},
],
})
class AppModule {}
The answer by #wendellmva didn't work for me. What did work was the solution suggested in this repo:
https://github.com/patricknazar/angular-lazy-loading-apollo-client
which is basically to put Apollo initialization in a separate, shared module, and include it in your main app module with forRoot().
I have the same issue an the docs from Apollo helped me. Go to 'https://www.apollographql.com/docs/angular/basics/setup/' or copy this:
import { HttpClientModule } from "#angular/common/http";
import { ApolloModule, APOLLO_OPTIONS } from "apollo-angular";
import { HttpLinkModule, HttpLink } from "apollo-angular-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
#NgModule({
imports: [
BrowserModule,
HttpClientModule,
ApolloModule,
HttpLinkModule
],
providers: [{
provide: APOLLO_OPTIONS,
useFactory: (httpLink: HttpLink) => {
return {
cache: new InMemoryCache(),
link: httpLink.create({
uri: "https://o5x5jzoo7z.sse.codesandbox.io/graphql"
})
}
},
deps: [HttpLink]
}],
})
export class AppModule {}
What worked for me was deleting the .angular folder and serving the application again.

Angular How to test a Component which requires a Location

Hello and thank you for your time!
I am learning how to use Angular and I am interested in learning how to test its Components.
Currently I am struggling because I have done the Tour of Heroes tutorial of the Angular page and I am testing the code to understand it better.
The point is that I am testing hero-details component which code is:
import {Component, OnInit, Input} from '#angular/core';
import {ActivatedRoute} from '#angular/router';
import {MyHeroService} from '../hero-service/my-hero.service';
import {Location} from '#angular/common';
import {Hero} from '../Hero';
#Component({
selector: 'app-hero-details',
templateUrl: './hero-details.component.html',
styleUrls: ['./hero-details.component.css']
})
export class HeroDetailsComponent implements OnInit {
#Input() hero: Hero;
constructor(private route: ActivatedRoute,
private myHeroService: MyHeroService,
private location: Location) {
}
ngOnInit(): void {
this.getHero();
}
getHero(): void {
const id = +this.route.snapshot.paramMap.get('id');
this.myHeroService.getHero(id)
.subscribe(hero => this.hero = hero);
}
goBack(): void {
this.location.back();
}
}
And my test tries to prove that getHero() is called after creating the hero-details component:
import {HeroDetailsComponent} from './hero-details.component';
import {ActivatedRoute} from '#angular/router';
import {MyHeroService} from '../hero-service/my-hero.service';
import {MessageService} from '../message.service';
import {Location} from '#angular/common';
import {provideLocationStrategy} from '#angular/router/src/router_module';
import {BrowserPlatformLocation} from '#angular/platform-browser/src/browser/location/browser_platform_location';
describe('heroDetails', () => {
it('should call getHero after being created', () => {
const heroDetailsComponent = new HeroDetailsComponent(new ActivatedRoute(),
new MyHeroService(new MessageService([])),
new Location(provideLocationStrategy(new BrowserPlatformLocation(['anyParameter']), '/')));
spyOn(heroDetailsComponent, 'getHero');
heroDetailsComponent.ngOnInit();
expect(heroDetailsComponent.getHero()).toHaveBeenCalled();
});
});
The difficulty I am facing is when I try to create a new Location which is a required parameter for the Hero-datail component's constructor.
The first Location's parameter is a PlatformStrategy, so then I used the provider to build it. Also, the provider needs a PlatformLocation() which looks like is abstract so then I chose the only implementation I could find which is BrowserPlatformLocation.
After all that process, the test execution says:
And the browser never ends loading the suite:
The strange thing here is that the IDE indeed finds those modules because I can navigate to them.
Also if I comment out that test, the suite works well:
Additionaly I have also read:
https://angular.io/api/common/Location
https://www.tektutorialshub.com/location-strategies-angular/
https://angular.io/tutorial/toh-pt5
How could I test it in a correct way? Where could I find more information about doing this type of tests properly? How could do this test to mock easily that Location parameter?
Thank you for reading this
Location is a built-in service, you do not need to instantiate it, just mock it:
const locationStub = {
back: jasmine.createSpy('back')
}
Then in your providers array:
providers: [ ..., {provide: Location, useValue: locationStub} ],
Then in your test just call the components goBack method, then use the Injector to get the instance of your service stub, like this:
const location = fixture.debugElement.injector.get(Location);
And then just test, that the back function has been called:
expect(location.back).toHaveBeenCalled();
This should solve the problem. This is, as far as I have seen, the best way to deal with the built-in services, you don't need to test them (Angular team did that already), just mock them and make sure they have been called correctly
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HeroDetailsComponent ],
providers: [ MyHeroService ],
imports: [ RouterTestingModule ],
providers: [{ provide: Location, useClass: SpyLocation }]
})
.compileComponents();
}));
it('should logout from application', async(() => {
const location: Location = TestBed.get(Location);
expect(location.href).toContain('blablabla url');
}));
Use SpyLocation from #angular/router/testing
If I were you, I would not bother creating tests from scratch : the CLI creates pre-made tests.
In those tests, there's a TestBed, that is used to set up a testing module for testing your component. If you used it, you would only have to import a router testing module.
This would give something like this :
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HeroDetailsComponent ],
providers: [ MyHeroService ],
imports: [ RouterTestingModule ]
})
.compileComponents();
}));
And just with that, your whole routing strategy is mocked.

Angular 4 and Tokens

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).

How to load route configuration from server before dependency injection in angular 4?

My top menu module doesn't know anything about routes and modules, that will be used for menu items before they load from api.
var routeConfig = [
{loadChildren: "./widget1.ts#Widget1Module", path: "widget1.ts"},
{loadChildren: "./widget2.ts#Widget2Module", path: "widget2.ts"},
{loadChildren: "./widget3.ts#Widget3Module", path: "widget3.ts"}
]; // this must be loaded before AppRoutingModule inject
#NgModule({
imports: [
RouterModule.forRoot(
routeConfig
)
],
exports: [ RouterModule ]
})
export class AppRoutingModule {
};
Now i just use routeConfig as global variable and make request from pure javascript before angular imports module. How to do it correctly?
As Gunter said, we can delay bootstrap method and pass some parameters like that. But it is a wrong way in my case.
In angular we can redefine routing in our application. So we can leave initialization parameter for RouterModule empty:
#NgModule({
imports: [ BrowserModule, RouterModule.forRoot([]), HttpModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
But then redefine them after web api retrieve us callback with route configuration:
export class AppComponent implements OnInit {
constructor(private router: Router, private http: Http) {
}
public isLoaded : bool = false;
ngOnInit() {
this.http.get('app/routerConfig.json')
.map((res:any) => res.json())
.subscribe(data => {
this.router.resetConfig(data);
this.isLoaded = true;
}, error => console.log(error));
}
}
Here is an example in Plunker.

Categories