Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular: how to call router.navigate() relative to target route in a RouteGuard?

I have an existing project developed with Angular 4. I need to control the access to a particular route based on user-rights. The simplified route configuration looks like this:

[
    { path: '', redirectTo: '/myApp/home(secondary:xyz)', pathMatch: 'full' },
    { path: 'myApp'
      children: [
        { path: '', redirectTo: 'home', pathMatch: 'full' },
        { path: 'home', ... },
        ...
        { path: 'product'
          children: [
            { path: '', redirectTo: 'categoryA', pathMatch: 'full' },
            { path: 'categoryA', component: CategoryAComponent, canActivate: [CategoryACanActivateGuard]},
            { path: 'categoryB', component: CategoryBComponent},
            ...
          ]
        },
        ...
      ]
    },
    ...
]

Now, I want to control the access to www.myWeb.com/myApp/product/categoryA. If the user doesn't have enough permission, he/she will be redirected to ... /product/CategoryB. I have written a CanActivate RouteGuard to do this, the guard class looks like this:

import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, ActivatedRoute } from '@angular/router';
import { MyService } from '... my-service.service';

@Injectable()
export class CategoryACanActivateGuard implements CanActivate {
    constructor(private myService: MyService, private router: Router) { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
        return this.myService.checkPermission()
            .then(result => {
                if (!result.hasAccess) {
                    //redirect here

                    this.router.navigate(['./myApp/product/categoryB']); 
                    //works, but I want to remove hardcoding 'myApp'

                    //this.router.navigate(['../../categoryB']);
                    //doesn't work, redirects to home page

                    //this.router.navigate(['./categoryB'], { relativeTo: this.route});
                    //do not have this.route object. Injecting Activated route in the constructor did not solve the problem

                }
                return result.hasAccess;
            });
    }
}

Everything works fine, but I want redirect relative to the target route like the following:

this.router.navigate(['/product/categoryB'], { relativeTo: <route-of-categoryA>});
// or 
this.router.navigate(['/categoryB'], { relativeTo: <route-of-categoryA>});

Unfortunately, relativeTo accepts only ActivatedRoute objects and all I have is ActivatedRouteSnapshot and RouterStateSnapshot. Is there a way to navigate relative to the target route (in this case categoryA)? Any help will be really appreciated.

Note:

  • I can not change the route configuration other than adding some route-guards.
  • I am not looking looking for this.router.navigateByUrl using state.url. I want to use router.navigate([...], { relativeTo: this-is-what-need}).
like image 913
Faruq Avatar asked Dec 13 '22 17:12

Faruq


2 Answers

It turns out, constructor injected ActivatedRoute works differently in RouteGuard compare to other places like a Component.

In a component, ActivatedRoute object points to the route that activated that component. For example, in the CategoryAComponent class, the following code will navigate to CategoryB:

this.router.navigate([ '../CategoryB' ], { relativeTo: this.route });

But, the same code above will not work in a RouteGuard class added to the CategoryA route configuration. In my test, I have found that the constructor injected ActivatedRoute object points to the root route. On the other hand, ActivatedRouteSnapshot object (passed in as a parameter in the canActivate function) points to the target route (in my case categoryA). However, we cannot pass this ActivatedRouteSnapshot object in this.router.navigate(...) function.

I couldn't find a better way to solve this problem, but the following code works for me:

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
    return this.myService.checkPermission()
        .then(result => {
            if (!result.hasAccess) {
                //redirect here

                let redirectTo = route.pathFromRoot
                    .filter(p => p !== route && p.url !== null && p.url.length > 0)
                    .reduce((arr, p) => arr.concat(p.url.map(u => u.path)), new Array<string>());

                this.router.navigate(redirectTo.concat('categoryB'), { relativeTo: this.route });
            }
            return result.hasAccess;
        });
}
like image 171
Faruq Avatar answered Dec 26 '22 19:12

Faruq


May I offer you a leaner solution:

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): true | UrlTree {
    const urlTree = createUrlTreeFromSnapshot(route, ['categoryB']);
    return result.hasAccess || urlTree;
  }

With this solution you have a possiblity of settings all other props you might need - named outlets navigation, query params, fragments very easily...

like image 21
Chladog Avatar answered Dec 26 '22 18:12

Chladog