Angular CanActivate guard - createUrlTree relative navigation
Asked Answered
C

6

11

I currently have a route guard such as

export class EntityGuard implements CanActivate {
  constructor(private readonly router: Router, ...) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean | UrlTree> {

  ...

This guard is activated for the URL

basePath/select/10/123

which means

basePath/select/:type/:id

When a certain condition is met, I need to force navigation back to

basePath/select/:type

How can I do that using createUrlTree? E.g.

if (...) {
   return of(this.router.createUrlTree([../', type]));
}

I'd need to set the

{ relativeTo: parent }

option. However I can't seem to find a way to do it.


My current solution is

private navigateBack(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
  const end = state.url.indexOf(route.url[route.url.length - 1].path);
  const parentUrl = state.url.slice(0, end);
  return of(this.router.createUrlTree([parentUrl]));
}

Which I really don't like. String manipulation is big no for me.

Cofsky answered 29/4, 2019 at 13:49 Comment(1)
Note that createUrlTree([parentUrl]) won't work if the parentUrl has matrix parameters. Return parseUrl(parentUrl) instead.Spiritualism
R
9

I'm felt at the same situation, and got similar solution. The main issue is if we inject ActivateRoute using the constructor, we get the previous route. They are planing to pass ActivateRoute at the canActivate method instead of the ActivatedRouteSnapshot.

This is the ticket: https://github.com/angular/angular/issues/22763

you can keep your solution until the angular team create better solution.

Regards.

Ricketts answered 29/4, 2019 at 14:8 Comment(1)
Thanks! On Angular 7.2.14, constructor-injecting ActivatedRoute produces null. So I think it is really unusable. I was hoping for Angular 8, but it seems it won't be doneCofsky
B
2

I have a workaround to get the URL Segment for current route:

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot,
  ): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
    const currentUrlFragments = route.pathFromRoot.flatMap((s) => s?.url ?? []).map((s) => s.path);
    return this.router.createUrlTree([...currentUrlFragments, '../']);
  }
}
Barbitone answered 21/9, 2021 at 13:45 Comment(0)
D
2

There is one more workaround getting the url from Router like this this.router.getCurrentNavigation()?.extractedUrl.toString()

Full example below:


@Injectable({
  providedIn: 'root',
})
export class MyGuard implements CanActivate, CanLoad {
  constructor(private router: Router) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    return this.redirect();
  }

  canLoad(
    route: Route,
    segments: UrlSegment[]
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    return this.redirect();
  }

  private redirect() {
    let url = this.router.getCurrentNavigation()?.extractedUrl.toString() || '/';
    return this.router.createUrlTree([url, 'other', 'relative','path']);
  }
}

Guarding a path like hello/:name with the above guard would result navigating to hello/:name/other/relative/path, e.g. hello/world/other/relative/path.

Durarte answered 18/2, 2022 at 11:17 Comment(0)
F
2

Since the 14.1, angular team has added an helper to create relative UrlTree from a ActivatedRouteSnapshot, called createUrlTreeFromSnapshot:

canActivate(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
    ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
  // Will redirect relatively to the commands
  return of(createUrlTreeFromSnapshot(route, ['../relativeFragment']));
}
Filipe answered 10/8, 2022 at 13:22 Comment(0)
H
1

Since it still doesn't work in angular 12, I wrote myself an util function to never come back here googling ;) It behaves like I would expect router to behave, which means it takes path like './../../relative' and goes relatively deep as needed to create UrlTree.

export const createRelativeUrlTree = (
  relativePath: string, // pass your path like this: './../foronelevel' './../../fortwolevels'
  currentUrl: string, // e.g. from RouterStateSnapshot .url
  router: Router,
): UrlTree => {
  if (!relativePath.startsWith('./../')) { throw new Error('createRelativeUrlTree: Path needs to start with \'./../\'!') }

  let levels = 1;
  let path = relativePath.substring(5); // removes obligatory './../' for first level of relativeness

  while (path.startsWith('../')) {
    ++levels;
    path = path.substring(3);
  }
  
  let reversedUrl = currentUrl.split('').reverse();
  let wantedLevelSlashIdx = 0;

  for (let i = levels; i > 0; i--) {
    wantedLevelSlashIdx = reversedUrl.findIndex(sign => sign === '/');
    reversedUrl = reversedUrl.slice(wantedLevelSlashIdx + 1);
  }

  const baseUrl = reversedUrl.reverse().join('');
  const compactUrlParts = [baseUrl, path].filter(part => !!part); // removes empty url parts
  return router.createUrlTree(compactUrlParts);
};
Hollar answered 28/7, 2022 at 11:45 Comment(1)
I'm back to Angular 17 (from 8). I suspect it still doesn't have this built-in functionality, right?Cofsky
E
1

The issue pointed out by the accepted answer has been closed on May 24, 2022 and released with version 14.1.0 (check out the Angular changelog, commit 53ca936366). It's now possibile to create a UrlTree starting from an ActivatedRouteSnapshot as outlined here https://angular.dev/api/router/createUrlTreeFromSnapshot#.

Here is an example of how to do that in the context of a canActivate guard:

const canActivateTeam: CanActivateFn = (
   route: ActivatedRouteSnapshot,
   state: RouterStateSnapshot,
) => {
  return createUrlTreeFromSnapshot(route, ['team', 666]);
};
Enriqueenriqueta answered 23/4 at 11:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.