angularangular2-routingangular-router-guardsangular-auxiliary-routes

How to have a route guard that routes to an auxiliary route after passing?


I have a section of my Angular app that is only accessible through a generated link sent to the user's email (a Docusign type thing). It is sectioned off from the rest of the app via a feature module, with it's own routes, and is lazily loaded in the main app.routes.ts.

The user will get a link like {domain}.com/sign-doc/document/:id, where :id will be a unique string of characters.

I wanted to set up the feature module routing structure like this

export const routes: Routes = [
  {
    path: 'document/:id',
    component: SignDocumentComponent,
    canActivate: [isValidDocumentIdGuard],
    children: [
      {
        path: 'metadata',
        component: DocumentMetadataComponent,
        outlet: 'sign'
      },
      {
        path: 'confirm-identity',
        component: ConfirmIdentityComponent,
        outlet: 'sign'
      },
      {
        path: 'view-document',
        canActivateChild: [ identityConfirmedGuard ],
        children: [
          {
            path: 'signed',
            component: ViewAndSignComponent,
            outlet: 'sign'
          },
          {
            path: 'unsigned',
            component: ViewAndSignComponent,
            outlet: 'sign'
          },
        ],
      },
    ],
  },
  {
    path: 'contact-admin',
    component: ContactAdminComponent,
  },
];

The SignDocumentComponent template looks like this:

<router-outlet name="sign" />
<app-session-extender />

and the app.routes.ts looks something like this:

export const routes: Routes = [
   /** other routes */,
   {
     path: 'sign-doc',
     loadChildren: () => import('./sign-doc/sign.module').then(x => x.SignDocumentModule),
   }

];

I have a route guard, isValidDocumentIdGuard, which will asynchronously check if the provided id is one we have a document for, and if it is valid should route the auxiliary outlet to metadata, otherwise it should redirect the primary outlet to the contact-admin path. The way I did this was to have the guard return createUrlTreeFromSnapshot(routeSnapshot, [{ outlets: { sign: ['metadata'] } }]) if successful.

The problem is that I seemingly get caught in an endless loop of entering sign-doc/document/:id/(sign:metadata), presumably because it routes itself back through the guard.

I have re-created a minimal example on StackBlitz.

Any help would be greatly appreciated - either to fix this problem or tell me where I am going wrong so I can fix it myself.


Solution

  • I tried to make it work from the guard but it kept on leading to an infinite loop.

    I have tried an alternative approach using redirectFunction of angular (Pretty new), this will construct the full URL and trigger the navigation, here it redirecting to the auxillary route without any loops.

    export const redirectToFn: RedirectFunction = (redirectData: any) => {
      const router = inject(Router);
      return router.createUrlTree([
        `/xyz/${redirectData.params.id}`,
        { outlets: { child: ['def'] } },
      ]);
    };
    

    The code is pretty straightforward, we use router.createUrlTree to navigate to the full route + auxillary route. We use inject to access the router.

    We can specify this function on the redirectTo property to trigger for wildcard routes.

    export const routes: Routes = [
      {
        path: ':id',
        component: XyzComponent,
        canActivate: [validIdGuard],
        children: [
          {
            path: 'def',
            outlet: 'child',
            component: DefComponent,
          },
          {
            path: '',
            redirectTo: redirectToFn,
            pathMatch: 'full',
          },
        ],
      },
    ];
    

    Finally, we make the guard return a boolean instead of navigation actions.

    export const validIdGuard: CanActivateFn = async (route, state) => {
      const id = route.params['id'] as string;
    
      const valid = await validId(id);
    
      if (valid) {
        console.log('asdf', route, state);
        return true;
      }
    

    Full Code:

    Guard:

    import { CanActivateFn } from '@angular/router';
    export const validIdGuard: CanActivateFn = async (route, state) => {
      const id = route.params['id'] as string;
    
      const valid = await validId(id);
    
      if (valid) {
        console.log('asdf', route, state);
        return true;
      }
    
      return false; // redirect to error page in real deal
    };
    
    async function validId(id: string): Promise<boolean> {
      return true;
    }
    

    Routing:

    import {
      RedirectFunction,
      Routes,
      Router,
      ActivatedRoute,
    } from '@angular/router';
    import { XyzComponent } from './xyz.component';
    import { DefComponent } from './def/def.component';
    import { validIdGuard } from './guards/redirect-guard';
    import { inject } from '@angular/core';
    
    export const redirectToFn: RedirectFunction = (redirectData: any) => {
      const route = inject(ActivatedRoute);
      const router = inject(Router);
      return router.createUrlTree([
        `/xyz/${redirectData.params.id}`,
        { outlets: { child: ['def'] } },
      ]);
    };
    export const routes: Routes = [
      {
        path: ':id',
        component: XyzComponent,
        canActivate: [validIdGuard],
        children: [
          {
            path: 'def',
            outlet: 'child',
            component: DefComponent,
          },
          {
            path: '',
            redirectTo: redirectToFn,
            pathMatch: 'full',
          },
        ],
      },
    ];
    

    Stackblitz Demo