5

I'm working in an Angular 9 app and I'm trying to load (or not) an specific module only when my app state (ngrx) have a property != null

First, I have an AuthGuard in my routes but with canActivate. So I want the 'dashboard' module only loads when mt AppState have a token

Here is my route file

const routes: Routes = [
{
  path: '',
  component: AppLayoutComponent,
  canActivate: [ AuthGuard ],
  children: [
    { path: '',  loadChildren: () => import('./pages/modules/dashboard/dashboard.module').then(m => m.DashboardModule) }
  ]
},
{
  path: '',
  component: AuthLayoutComponent,
  children: [
    { path: 'session',  loadChildren: () => import('./pages/modules/session/session.module').then(m => m.SessionModule) }
  ]
},
{
  path: '**',
  redirectTo: 'session/not-found'
}];

And this is my AuthGuard. It localestorage doesn't have a session, then redirects to the login page.

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private router: Router, public authService: AuthService) {}


  public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    if (localStorage.getItem('session')) {
        // logged in so return true
        return true;
    }

    // not logged in so redirect to login page with the return url
    this.router.navigate(['session/signin']);
    return false;
  }
}

this is kind of what I want to do whit the canLoad in AuthModuleGuard, but this doesn't work

  public canLoad(): Observable<boolean> {
    return this.store.select('session').pipe(
      take(1),
      map((authstate) => {
          console.log('Token status', authstate.token);
          if (authstate.token !== null) {
              return true;
          } else {
              this.router.navigate(['session/signin']);
              return false;
          }
        })
      );
  }

if I do this... the application gives me errors and still loads both files

{
  path: '',
  component: AppLayoutComponent,
  canLoad: [ AuthModuleGuard ],
  children: [ ... ]
}

enter image description here

and If I do this ... the app never finish loading

{ path: '', canLoad: [ AuthModuleGuard ], loadChildren: () => import('./pages/modules/dashboard/dashboard.module').then(m => m.DashboardModule) },

HERE IS A STACKBLITZ EXAMPLE (including my folder structure)---> https://stackblitz.com/edit/angular-ivy-nasx7r

I need a way to load the dashboard module (and other modules) only if the token in my store are seted, and If is not, redirect to the Login. Please help

Sergio Mendez
  • 1,311
  • 8
  • 33
  • 56

1 Answers1

4

After spending some time on this, I've learnt some very interesting things:

if (route.children) {
  // The children belong to the same module
  return of(new LoadedRouterConfig(route.children, ngModule));
}

  if (route.loadChildren) { /* ... */ }

This also denotes that canLoad is redundant in this case:

{
  path: '',
  component: AppLayoutComponent,
  canLoad: [ AuthModuleGuard ],
  children: [ ... ]
}

as this route guard has effect when used together with loadChildren.

  • you should be mindful of when to redirect from your guard

    With a configuration like this:

{
  path: '',
  component: AppLayoutComponent,
  children: [
    { 
      path: '', 
      loadChildren: () => import('./pages/modules/dashboard/dashboard.module').then(m => m.DashboardModule),
      canLoad: [AuthModuleGuard]
    }
  ]
},

and a canLoad guard like this:

canLoad(route: Route, segments: UrlSegment[]): Observable<boolean> {
  return this.store.select('session').pipe(
      take(1),
      map((authstate) => {
          console.log('Token status', authstate.token);
          if (authstate.token !== null) {
              return true;
          } else {
              this.router.navigate(['session/signin']);
              return false;
          }
      })
  );
}

you'll get into an infinite loop. When the app first loads, it will go through each configuration in a depth-first manner and will compare the path with the current segments(initially, segments = []).

But remember that if a route has a children property, it will go through each of them and see if the segments match the route. Since the child route has path: '', it will match any segments and because it has loadChildren, it will invoke the canLoad guards.

Eventually, this lines will be reached:

this.router.navigate(['session/signin']);
return false;

this.router.navigate(['session/signin']); indicates a redirect, which means it will repeat the steps delineated above.


The solution I came up with is to add a pathMatch: 'full' to your child route:

{
  path: '',
  component: AppLayoutComponent,
  children: [
    { 
      path: '', 
      pathMatch: 'full',
      loadChildren: () => import('./pages/modules/dashboard/dashboard.module').then(m => m.DashboardModule),
      canLoad: [AuthModuleGuard]
    }
  ]
},

When the app loads, the segments will be an empty array and because path: '' matches any group of segments and that group of segments is [] initially, there will be a match:

if (route.path === '') {
  if ((route.pathMatch === 'full') && (segmentGroup.hasChildren() || segments.length > 0)) {
    return {matched: false, consumedSegments: [], lastChild: 0, positionalParamSegments: {}};
  }

  return {matched: true, consumedSegments: [], lastChild: 0, positionalParamSegments: {}};
}

This means that the guard will be invoked and the if's alternative block will be reached and this.router.navigate(['session/signin']) will be invoked.

Next time the comparison is made, the segments will be (roughly) ['session', 'signin'] and there will be no match, since this is returned:

{matched: false, consumedSegments: [], lastChild: 0, positionalParamSegments: {}}

If no match occurs, it will keep on searching until something is found, but the guard will not be invoked again.

StackBlitz

Andrei Gătej
  • 11,116
  • 1
  • 14
  • 31