Functional CanDeactivate Guards in Angular

tldr;

CanDeactivate guards in Angular can be very helpful in your app. Preventing users from leaving a route can keep the user from losing their progress on a form or something similar. Angular makes it easy with the CanDeactivate guard, and they’re even more straight-forward to write with the (not-so-new anymore) format of functional guards.

Creating the Guard

First, create the guard either manually or with the Angular or Nx CLI. If you use the CLI, make sure to select that you want to implement the CanDeactivate interface. Once the guard is corrected, you can start working on the guard. There are a few pieces you’ll need to add for the guard, so let’s walk through each piece.

The first is not actually necessary, but it is helpful, and that is to create a type that we can use to refer to the return type of the guard. All this will do is make it easier to refer to the return type in our code.

export type CanDeactivateType = Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;

The types listed here are all options for the CanDeactivate guard to return. We’ll use this in a couple spots, but again it’s unnecessary.

The next step is to create an interface that we’ll use for the generic argument of the guard. Essentially, this is going to say that the component that the guard is applied to could have a canDeactivate method that the guard will call to see if the component can be deactivated.

export interface CanComponentDeactivate {
canDeactivate: () => CanDeactivateType;
}Calling the method canDeactivate is just an example. You could call it whatever you want, but canDeactivate makes it very easy for everyone to know what is going on.

Finally, we actually have the guard to write. The function will return a CanDeactivateFn, with the generic type of CanComponentDeactivate.

export const canDeactivateGuard: CanDeactivateFn<CanComponentDeactivate> = (component: CanComponentDeactivate) => {
return component.canDeactivate ? component.canDeactivate() : true;
};

The guard function can accept other parameters as well. Those parameters are currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, and nextState: RouterStateSnapshot. In many simple cases, you’ll just need to call the canDeactivate method on your component (if it exists) as shown above.

At this point, the guard is ready to implement.

Implementing the Guard

After creating the guard we need to actually implement it. The first step is to add the guard to the route definition. All that’s needed is to add a canDeactivate array to the route definition with the guard added to the array.

{ path: ‘my-route’, component: MyRouteComponent, canDeactivate: [canDeactivateGuard] },

In the component’s Typescript file, we implement the canDeactivate method. This method will return one of the types on our CanDeactivateType. If the result is true, the user will be able to leave the page. If the result is false, the navigation is cancelled and the user stays on the page. This is slightly tricky if you want to get the user’s feedback before staying or routing away from the page. In this situation, just create a Subject and return it. Then show the user a prompt, and call the next method on the subject with the result of the user’s selection.

canDeactivate(): CanDeactivateType {
if (this.promptUser) {
const deactivateSubject = new Subject<boolean>();
this._confirmation.confirm({
message: ‘Are you sure you want to leave? Some data may be lost.’,
icon: ”,
accept: () => {
deactivateSubject.next(true);
},
reject: () => {
deactivateSubject.next(false);
},
});
return deactivateSubject;
} else {
return true;
}
}The _confirmation.confirm here comes from PrimeNg. However, you can use whatever process you want for your situation.

Now when you’re on this route, and try to route away, the canDeactivate method will be called. If it returns true the user will be able to change routes. Otherwise they will stay on the page.

Testing the Guard

The last piece of implementing the guard is testing it. This step is a lot easier now with the RouterTestingHarness and HttpClientTestingModule. You can read more about functional route guards and how to test them in this previous article of mine. In this article I’ll simply show you how to test this type of guard.

For the tests, we’ll create a few test components which we’ll configure as routes that we will “route” to and from in the tests. The components should represent different options that your application might have. For example, one component can return false for the canDeactivate method, one returns true, and one that doesn’t implement the method. This way in the tests you can start on a row, trigger a navigation event, and then check to see where the application ends up. Below is an example of the tests.

@Component({ standalone: true, template: ” })
class HasCanDeactivateTrueComponent {
canDeactivate() {
return true;
}
}
@Component({ standalone: true, template: ” })
class HasCanDeactivateFalseComponent {
canDeactivate() {
return false;
}
}
@Component({ standalone: true, template: ” })
class HasNoCanDeactivateComponent {}
@Component({ standalone: true, template: ” })
class DashboardComponent {}

describe(‘canDeactivateGuard’, () => {
let routes: Route[] = [];
beforeEach(() => {
routes = [
{ path: ‘can-deactivate’, canDeactivate: [canDeactivateGuard], component: HasCanDeactivateTrueComponent },
{ path: ‘cant-deactivate’, canDeactivate: [canDeactivateGuard], component: HasCanDeactivateFalseComponent },
{ path: ‘no-guard’, canDeactivate: [canDeactivateGuard], component: HasNoCanDeactivateComponent },
{ path: ‘dashboard’, component: DashboardComponent },
];
});
it(‘should allow the component to deactivate’, async () => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [provideRouter(routes)],
});
const testRouter = TestBed.inject(Router);
await RouterTestingHarness.create(‘/can-deactivate’);
await testRouter.navigateByUrl(‘dashboard’);
expect(testRouter.url).toEqual(‘/dashboard’);
});
it(‘should allow the component to deactivate’, async () => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [provideRouter(routes)],
});
const testRouter = TestBed.inject(Router);
await RouterTestingHarness.create(‘/no-guard’);
await testRouter.navigateByUrl(‘dashboard’);
expect(testRouter.url).toEqual(‘/dashboard’);
});
it(‘should prevent the component from deactivating’, async () => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [provideRouter(routes)],
});
const testRouter = TestBed.inject(Router);
await RouterTestingHarness.create(‘/cant-deactivate’);
await testRouter.navigateByUrl(‘dashboard’);
expect(testRouter.url).toEqual(‘/cant-deactivate’);
});
});

In these few tests, we can check to make sure that our guard is working properly.

Conclusion

CanDeactivate guards have their place, although you may want to be careful about using them too frequently; users generally know when they want to change routes and when they don’t. Angular makes it really easy to implement and test this functionality.

Functional CanDeactivate Guards in Angular was originally published in ngconf on Medium, where people are continuing the conversation by highlighting and responding to this story.

Leave a Comment

Your email address will not be published. Required fields are marked *