Issue
I have an application where my navigation bar changes based off what ‘area’ of the product they are in. I am using Angulars Route Guards to make sure that their access is checked so they can only hit the routes they have access to. This works great!
Within my app-routing-module.ts
I am (trying) to be smart and leverage the ActivatedRouteSnapshot to obtain all the child links and then build out a navigation be for it. What I would like to do, is also use the Route Guard to decide if the child link should even be displayed or not.
// Guard
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { environment } from '../../environments/environment';
import { MeService } from '../shared/services/me.service';
@Injectable()
export class AdminGuard implements CanActivate {
constructor(private _meService: MeService) {}
async canActivate(
next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
const user = await this._meService.getCurrentUser();
if (user && user.isUserAdminForCompany) {
return true;
} else {
return false;
}
}
}
// Routes
export const routes: Routes = [
{ path: '', redirectTo: 'route1', pathMatch: 'full' },
{ path: 'route1', component: MyComponent,
children: [
{ path: '', redirectTo: 'overview', pathMatch: 'full' },
{ path: 'overview', component: Overview },
{ path: 'specs', component: Specs, canActivate: [ AdminGuard ] }
]
}
];
So, once someone hits MyComponent
, I fetch the child routes and make a navbar out of it. Is it possible to have some kind of directive or some sort to leverage the AdminGuard
on the /spec
path to hide the URL if the AdminGuard
returns false? Since some/more of my guards require some sort of async call to the server or some other service dependency, I can’t just simply call guard.canActivate
within an *ngIf
or something.
I’m pretty sure it doesn’t exist, but it seems like need some kind of setup like this:
<a [routerLink]="child.path" [canActivate]="child.guards">{{child.name}}</a>
UPDATE
I ended up just opening a GitHub Feature Request on the angular repo. This functionality doesn’t seem to be present (in an out of the box way). Until a better solution is found, I’m just going to make a custom directive that will run the logic in the Guards to evaluate if something should be exposed or not.
https://github.com/angular/angular/issues/25342
Solution
Here’s what I eventually went with. Since there isn’t any ‘out of the box’ way to leverage guards for what I wanted to do, I just made a custom directive.
The one thing I’ll note about this solution is that I hate two of the following things (which I’ll change eventually).
-
If your Guards have anything that does a redirect, you’ll have to change it so the Guard only returns true/false. If it redirects the page on Guard failure, then the directive will just end up redirecting you instead of just hiding the element
-
this._elementRef.nativeElement.style.display = hasAccess ? 'block' : 'none';
There’s a better solution to this than just doing a simple hide. It should act like*ngIf
where it doesn’t even render the element at all unless it evaluates to true.
Implementation:
<div appGuard [guards]="myGuardsArray">Something you want to hide .... </div>
Directive:
import { Directive, ElementRef, Injector, Input, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
@Directive({
selector: '[appGuard]'
})
export class GuardDirective implements OnInit {
@Input() guards: any[];
private readonly _elementRef: ElementRef;
private readonly _activatedRoute: ActivatedRoute;
private readonly _router: Router;
private readonly _injector: Injector;
constructor(_elementRef: ElementRef, _activatedRoute: ActivatedRoute, _router: Router,
_injector: Injector) {
this._elementRef = _elementRef;
this._activatedRoute = _activatedRoute;
this._router = _router;
this._injector = _injector;
}
async ngOnInit(): Promise<void> {
const canActivateInstances = this.guards.map( g => this._injector.get(g));
const results = await Promise.all(canActivateInstances.map( ca => ca.canActivate(this._activatedRoute.snapshot, this._router.routerState.snapshot)));
const hasAccess = results.find( r => !r) === false ? false : true;
this._elementRef.nativeElement.style.display = hasAccess ? 'block' : 'none';
}
}
UPDATE
A simple solution to figure out how to handle redirects or not:
async canActivate(
next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
const user = await this._meService.getCurrentUser();
const result = user && user.isUserAdminForCompany;
if (next.routeConfig && next.routeConfig.canActivate.find( r => r.name === 'NameOfGuard') && !result) {
window.location.href = `${environment.webRoot}/sign-in`;
}
return result;
}
Answered By – mwilson
Answer Checked By – Gilberto Lyons (AngularFixing Admin)