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.
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;
}
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;
}
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',
},
],
},
];