I'm trying to test an Angular Resolver which accesses children routes param.
My guard works fine but I cannot create an unit test easily because I cannot create an ActivatedRouteSnapshot with children routes (read only property).
My resolver
#Injectable({
providedIn: 'root'
})
export class MyResolverGuard implements Resolve<string> {
constructor() {
}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): string {
return route.firstChild.paramMap.get('my-param');
}
}
My test :
it('should resolve chilren route params', () => {
guard = TestBed.get(MyResolverGuard);
const route = new ActivatedRouteSnapshot();
// Cannot assign children because it's a read only property
route.children = [...];
const myResolverParams = guard.resolve(route, null);
});
Is there other any other ways than using mock ActivatedRouteSnapshot ?
Does my approach to test guard is good ?
Thanks for sharing your strategy.
Related
I'm experiencing strange behavior of services injected to a component that is loaded dynamically. Consider the following service
#Injectable({
providedIn: 'root'
})
export class SomeService {
private random = Math.random() * 100;
constructor() {
console.log('random', this.random);
}
}
The service is added as a dependency to two components. First component is a part of lazy-loaded module. While second one is loaded dynamically. The following service loads modules with dynamic component
export const COMPONENT_LIST = new InjectionToken<any>('COMPONENT_LIST');
export const COMPONENT_TYPE = new InjectionToken<any>('COMPONENT_TYPE');
#Injectable({
providedIn: 'root'
})
export class LoaderService {
constructor(
private injector: Injector,
private compiler: Compiler,
) { }
getFactory<T>(componentId: string): Observable<ComponentFactory<T>> {
// COMPONENT_LIST is passed through forRoot() from the module that declares first component
const componentList = this.injector.get(COMPONENT_LIST);
const m = componentList.find(m => m.componentId === componentId);
const promise: Promise<ComponentFactory<T>> = (!m) ? null :
m.loadChildren
.then((mod: any) => {
return this.compiler.compileModuleAsync(mod);
})
.then((mf: NgModuleFactory<any>) => {
const mr: NgModuleRef<any> = mf.create(this.injector);
const type: Type<T> = mr.injector.get<Type<T>>(COMPONENT_TYPE); // DYNAMIC_COMPONENT is provided in loaded module
return mr.componentFactoryResolver.resolveComponentFactory<T>(dynamicComponentType);
});
return from(promise);
}
}
In the same module I declare the following component (to place component that is loaded dynamically) and directive
#Component({
selector: 'dynamic-wrapper',
template: `<ng-container dynamicItem></ng-container>`
})
export class DynamicWrapperComponent implements AfterViewInit, OnDestroy {
#Input() itemId: string;
#Input() inputParameters: any;
#ViewChild(DynamicItemDirective)
private dynamicItem: DynamicItemDirective;
private unsubscribe$: Subject<void> = new Subject();
constructor(
private loaderService: LoaderService
) { }
ngAfterViewInit(): void {
this.loaderService.getComponentFactory(this.itemId).subscribe((cf: ComponentFactory<any>) => {
this.dynamicItem.addComponent(cf, this.inputParameters);
});
}
}
...
#Directive({
selector: '[dynamicItem]'
})
export class DynamicItemDirective {
constructor(protected viewContainerRef: ViewContainerRef) { }
public addComponent(cf: ComponentFactory<any>, inputs: any): void {
this.viewContainerRef.clear();
const componentRef: ComponentRef<any> = this.viewContainerRef.createComponent(cf);
Object.assign(componentRef.instance, inputs);
// if I do not call detectChanges, ngOnInit in loaded component will not fire up
componentRef.changeDetectorRef.detectChanges();
}
}
SomeService is defined in a separate module that is imported in both lazy-loaded module with first component and dynamically loaded module.
After both components are initialed I see output of console.log('random', this.random) with two different numbers in console, despite providedIn: 'root' in decorator. What is the reason for such a strange behavior?
Lazy loaded modules won't react to providedIn: 'root' as one might expect.
This option will push services to the AppModule(root module) only if the module that they are imported in is not lazy loaded.
Why do you see different random numbers?
Because the module that SomeService is defined in is initiated twice! (feel free to put console.log in the constructor of this module) - therefore the service is initiated twice (I encountered this issue myself in my project lazy load platform).
Why the lazy module that contain SomeService is initiated twice?
loadChildren or moduleFactory.create(this.injector) don't hold a cache of lazy loaded modules. They initiate a module and attach its injector as a leaf to the input injector.
If the module that contains SomeService was created from moduleFactory.create - you can add a cache layer to return the cached initiated lazy loaded module.
For example:
private lazyLoadedModulesCache: Map<Type<any>, NgModuleRef<any>>;
The url looks like this:
http://localhost:4200/room/RECd4teOsdsro9YRcOMX/chat
I'm trying to extract the id part (RECd4teOsdsro9YRcOMX)
I tried the following:
chatRoomUid: string;
constructor(
private route: ActivatedRoute
) {}
ngOnInit() {
this.chatRoom$ = this.route.parent.parent.params.pipe(
tap(params => this.chatRoomUid = params.chatRoomUid),
switchMap(params => {
if (!params.chatRoomUid) return of(null);
this.chatRoomUid = params.chatRoomUid;
})
);
console.log(this.chatRoomUid); // returns undefined
}
How can I extract the id from the url and save it to my variable chatRoomUid?
Route:
{ path: 'room/:roomUid', loadChildren: () => import('#chatapp/pages/room/room.module').then(m => m.RoomModule) },
Edit: Added the routes
You're console.loging in a different context.
Remember Observables are asynchronous, thus you'd have to move console.log inside switchMap.
However, just produce a new Observable
chatRoomUid$: Observable<string>;
...
this.chatRoomUid$ = this.route.params.pipe(
map(params => params['roomUid'])
);
You can define your route like this
{path: 'room/:chatRoomUid/chat', component: ChatComponent}
Then in your component simply
import {ActivatedRoute} from '#angular/router';
constructor(private route:ActivatedRoute){}
ngOnInit(){
this.route.params.subscribe( params =>
console.log(params['chatRoomUid']);
)
}
I created a component that holds a loader:
#Component({
selector: 'app-loader',
templateUrl: './loader.component.html',
styleUrls: ['./loader.component.scss'],
providers: [LoaderService]
})
export class LoaderComponent implements OnInit {
showloader: boolean = false;
constructor (private loader_service:LoaderService) {}
ngOnInit () {
this.loader_service.showLoaderEventChange.subscribe(state => {
console.log(state); // Nothing prints !!
this.showloader = state;
})
}
}
and I have a service that I call from another component, and the call does work:
public grabJsonData (enviroment: string, language: string) {
this.loader_service.showLoader(true);
}
It uses the LoaderService:
#Injectable({
providedIn: 'root'
})
export class LoaderService {
showLoaderEventChange = new Subject<any>();
constructor() { }
showLoader(state){
this.showLoaderEventChange.next(state);
}
}
The loader component uses the LoaderService, listens for showLoaderEventChange to pass next().
But when I do this.showLoaderEventChange.next(state) it does not catch anything in the subscribe function.
Did I miss something?
Its because you are not using same instance of service for your components since you are injecting it in root as well as in Loader Component.
Try removing Providers : [LoaderService] from your Loadercomponent.
I'm not sure about Angular 6 since I haven't tested it out yet but in Angular 5 you have to create an observable to observe it from your Subject as such :
showLoaderEventChangeEmitter = new Subject<any>();
showLoaderEventChangeEvent$ = this.showLoaderEventChangeEmitter.asObservable();
Then use showLoaderEventChangeEmitter.next(); to trigger events and listen to them with showLoaderEventChangeEvent$.subscribe()
I'm using route guards, specifically the canActivate() method, but Angular is calling ngOnInit() of my root AppComponent before canActivate is called.
I have to wait on some data in canActivate before the AppComponent can render it in the template.
How can I do this?
I was dealing with such cases, and here is what I usually do:
1. I create a Resolver service (which implements Resolve interface). It allows you to get all necessary data before activating the route:
import { Injectable } from '#angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '#angular/router';
import { DataService } from 'path/to/data.service';
#Injectable()
export class ExampleResolverService implements Resolve<any> {
constructor(private _dataService: DataService) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<any> {
return this._dataService.anyAsyncCall()
.then(response => {
/* Let's imagine, that this method returns response with field "result", which can be equal to "true" or "false" */
/* "setResult" just stores passed argument to "DataService" class property */
this._dataService.setResult(response.result);
})
.catch(err => this._dataService.setResult(false););
}
}
2. Here is how we can deal with AuthGuard, which implements CanActivate interface:
import { Injectable } from '#angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '#angular/router';
import { DataService } from 'path/to/data.service';
#Injectable()
export class AuthGuard implements CanActivate {
constructor(private _dataService: DataService) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
/* "getResult" method operates with the same class property as setResult, it just returns the value of it */
return this._dataService.getResult(); // will return "true" or "false"
}
}
3. Then you can include the Resolver and the AuthGuard to your routes config, here is just a part (the structure of routes can differ, here is an example with activating the parent component):
const routes: Routes = [
{
path: 'app',
component: AppComponent,
resolve: {
result: ExampleResolverService // your resolver
},
canActivate: [AuthGuard], // your AuthGuard with "canActivate" method
children: [...] // child routes goes inside the array
}
];
How it works
When you're navigating to /app, the ExampleResolverService starts, makes API call and stores the necessary part of response to class property in DataService via setResult method (it's the usual setter). Then, when the resolver finished the work, it's time for our AuthGuard. It gets stored result from DataService via getResult method (it's the usual getter), and returns this boolean result (our AuthGuard expects boolean to be returned, and the route will be activated if it returns true, and will not be activated if it returns false);
It's the simplest example without any additional operations with data, the logic is more complex usually, but this skeleton should be enough for basic understanding.
For me, I listened for ROUTE_NAVIGATED events in my app component like below
I am using ngrx/router-store to be able to listen to these router actions.
// app.component.ts
public ngOnInit(): void {
// grab the action stream
this.actions$.pipe(
// Only pay attention to completed router
ofType(ROUTER_NAVIGATED),
// Now I can guarantee that my resolve has completed, as the router has finsihed
// Switch
switchMap(() => {
// Now switch to get the stuff I was waiting for
return this.someService.getStuff();
})
// Need to subscribe to actions as we are in the component, not in an effect
// I suppose we should unsubscribe, but the app component will never destroy as far as I am aware so will always be listening
).subscribe();
So, I am loosing my mind over this
I have a page with many components... but for some reason I am having problems with one...
it is for mains search in the header of the page... for debugging purposes I stripped it down to bare minimum, and still doesn't work
This is my search component
import { Component, OnInit } from '#angular/core';
import { ROUTER_DIRECTIVES } from '#angular/router';
import { Router, ActivatedRoute } from '#angular/router';
#Component({
selector: 'main-search',
template: `<div></div>`,
})
export class MainSearch implements OnInit {
private sub: any;
constructor(private route: ActivatedRoute){
}
ngOnInit(){
this.sub = this.route.params.subscribe(params => {
console.log('PARAMS FROM MAIN SEARCH', params);
});
}
}
as you can see, I am trying to log the params from the URL (f.e. http://localhost:8080/indices;search=test)
NOT populating
I have a similar component with exact behaviour (subscribing to params onInit...
this.sub = this.route.params.subscribe(params => {
console.log('PARAMS FROM INDICES: ', params);
})
And that one actually logs the bloody params!
From console:
PARAMS FROM MAIN SEARCH Object {} => main-search.ts?8502:24
Angular 2 is running in the development mode. Call enableProdMode() to enable the production mode. => lang.js?c27c:360
null => index.service.ts?0bf5:40
FROM API => index.service.ts?0bf5:49
PARAMS FROM INDICES: Object {search: "test"} => indicesList.component.ts?5ff1:63
The weird thing is that only the mainsearch gets logged to the console before Angular2 disclaimer
What could be the issue that main-search doesn't get the params?
I think you need to use the ActivatedRoute.
This should work in your case:
constuctor(
private _activatedRoute: ActivatedRoute,
) {}
ngOnInit()
this._activatedRoute.params.subscribe(params => console.log(params));
}
The thing is your 'main-search' is a few components deep and the router params observable emits params from the root url. Whereas the ActivatedRoute emits params from the current route.