Angular RouteGuard / Dynamic Navigation

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

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

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

Leave a Reply

Your email address will not be published.