Route guards

Route guards

The next explanations are based on this project. To get the most of this article it is recommended to not only read it, but to practice creating a similar project.

This is a demo of the application functionality:

On chrome devtools Console tab, we can see when the guards are being called thanks to the logs.

Guards allow or disallow routing. All of them are services that implement an interface as we will see. There are some available route guards in angular:

  • CanActivate: Allows or disallows reaching a component.
  • CanActivateChild: Allows or disallows reaching a child component.
  • CanDeactivate: Allows or disallows leaving a component.
  • CanLoad: Allows or disallows fetching a module.
  • Resolve: Ensures that some data is gathered before route activation. If used with guards, guards will be executed first. If resolves an error we can navigate to an error page.

CanActivate:

We create a guard with the CanActivate interface using the command:

ng generate guard profile/can-activate

or

ng g g profile/can-activate

We add a console.log to be aware of when we are using the guard:

// can-activate.guard.ts
import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanActivate,
  RouterStateSnapshot,
  UrlTree,
} from '@angular/router';
import { Observable } from 'rxjs';

@Injectable()
export class CanActivateGuard implements CanActivate {
  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    console.log('CanActivate');
    return true;
  }
}

As we told earlier the class implements the CanActivate interface which has the canActivate method. We can return a boolean, a Promise, an Observable or a UrlTree. If the boolean is true we allow the navigation to the requested route, if it is false we disallow it. If we return UrlTree we will be redirected to the specified URL. We can also define the constructor and inject a service.

We then provide (as a service) the guard in the module that imports the RoutingModule that uses the guard, in this case, ProfileModule:

// profile.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProfileComponent } from './profile.component';
import { ProfileRoutingModule } from './profile-routing.module';
import { CanActivateGuard } from './can-activate.guard';
import { CanActivateChildGuard } from './can-activate-child.guard';
import { PhotoModule } from './photo/photo.module';

@NgModule({
  declarations: [ProfileComponent],
  imports: [CommonModule, PhotoModule, ProfileRoutingModule],
  providers: [CanActivateGuard, CanActivateChildGuard],
})
export class ProfileModule {}

And then we use it in the RoutingModule:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CanActivateChildGuard } from './can-activate-child.guard';
import { CanActivateGuard } from './can-activate.guard';
import { PhotoComponent } from './photo/photo.component';
import { ProfileComponent } from './profile.component';

const routes: Routes = [
  {
    path: '',
    component: ProfileComponent,
    canActivate: [CanActivateGuard],
    canActivateChild: [CanActivateChildGuard],
    children: [{ path: 'photo', component: PhotoComponent }],
  },
  {
    path: '**',
    redirectTo: '',
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class ProfileRoutingModule {}

Here we are using also CanActivateChild guard but we will discuss it later.

The result is the first shown in the demo video. If we change the return boolean of the guard to false the result is this:

As we see on the log each time we click on Profile menu option we are using the CanActivate guard but the view is not rendered. This is the result we expected.

A possible real use case for this guard is disallowing the access to a part of the navigation based on a role (e.g. if the user is not an admin is not allowed to navigate to the "files" URL. In this practical example we simply set a true allowing always the navigation but you can clone the code and make changes to see how it works.

CanActivateChild:

We create a guard with the CanActivateChild interface using the command:

ng generate guard profile/can-activate-child

or

ng g g profile/can-activate-child

We add a console.log to be aware of when we are using the guard:

// can-activate-child.guard.ts
import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanActivate,
  CanActivateChild,
  RouterStateSnapshot,
  UrlTree,
} from '@angular/router';
import { Observable } from 'rxjs';

@Injectable()
export class CanActivateChildGuard implements CanActivateChild {
  canActivateChild(
    childRoute: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | boolean
    | UrlTree
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree> {
    console.log('CanActivateChild');
    return true;
  }
}

As in the previous guard, the class implements an interface, in this case the CanActivateChild interface which has the canActivateChild method. We can return a boolean, a Promise, an Observable or a UrlTree. Again if the boolean is true we allow the navigation to the requested route, if it is false we disallow it. If we return UrlTree we will be redirected to the specified URL. We can also define the constructor and inject a service.

We then provide (as a service) the guard in the module that imports the RoutingModule that uses the guard, in this case, ProfileModule:

// profile.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProfileComponent } from './profile.component';
import { ProfileRoutingModule } from './profile-routing.module';
import { CanActivateGuard } from './can-activate.guard';
import { CanActivateChildGuard } from './can-activate-child.guard';
import { PhotoModule } from './photo/photo.module';

@NgModule({
  declarations: [ProfileComponent],
  imports: [CommonModule, PhotoModule, ProfileRoutingModule],
  providers: [CanActivateGuard, CanActivateChildGuard],
})
export class ProfileModule {}

And then we use it in the RoutingModule:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CanActivateChildGuard } from './can-activate-child.guard';
import { CanActivateGuard } from './can-activate.guard';
import { PhotoComponent } from './photo/photo.component';
import { ProfileComponent } from './profile.component';

const routes: Routes = [
  {
    path: '',
    component: ProfileComponent,
    canActivate: [CanActivateGuard],
    canActivateChild: [CanActivateChildGuard],
    children: [{ path: 'photo', component: PhotoComponent }],
  },
  {
    path: '**',
    redirectTo: '',
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class ProfileRoutingModule {}

The result again is the first shown in the demo video. If we change the return boolean of the guard to false the result is this:

As we see on the log each time we click on Show photo button we are using the CanActivateChild guard but the view is not rendered. This is the result we expected.

Again, a possible real use case for this guard is disallowing access to a part of the navigation based on a role (e.g. if the user is not an admin is not allowed to navigate to the users creation URL. In this practical example, we simply set a true allowing always the navigation but you can clone the code and make changes to see how it works.

CanDeactivate:

We create a guard with the CanDeactivate interface using the command:

ng generate guard faq/can-deactivate

or

ng g g faq/can-deactivate

We add a console.log to be aware of when we are using the guard:

// can-deactivate.guard.ts
import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanDeactivate,
  RouterStateSnapshot,
  UrlTree,
} from '@angular/router';
import { Observable } from 'rxjs';

@Injectable()
export class CanDeactivateGuard implements CanDeactivate<any> {
  canDeactivate(
    component: any,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState: RouterStateSnapshot
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    console.log('CanDeactivate');
    return true;
  }
}

As in the previous guard, the class implements an interface, in this case the CanDeactivate interface which has the canDeactivate method. We can return a boolean, a Promise, an Observable or a UrlTree. Again if the boolean is true we allow the navigation to the requested route, if it is false we disallow it. If we return UrlTree we will be redirected to the specified URL. We can also define the constructor and inject a service.

We then provide (as a service) the guard in the module that imports the RoutingModule that uses the guard, in this case, FaqModule:

// faq.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FaqComponent } from './faq.component';
import { FaqRoutingModule } from './faq-routing.module';
import { CanDeactivateGuard } from './can-deactivate.guard';



@NgModule({
  declarations: [FaqComponent],
  imports: [
    CommonModule,
    FaqRoutingModule
  ],
  providers: [CanDeactivateGuard]
})
export class FaqModule { }

And then we use it in the RoutingModule:

// faq-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CanDeactivateGuard } from './can-deactivate.guard';
import { FaqComponent } from './faq.component';

const routes: Routes = [
  {
    path: '',
    component: FaqComponent,
    canDeactivate: [CanDeactivateGuard],
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class FaqRoutingModule {}

The result again is the first shown in the demo video. If we change the return boolean of the guard to false the result is this:

As we see once we visit the faq view, we cannot visit any other view and each time we click on any other menu option we are using the CanDeactivate guard as we see on the logs. This is the result we expected.

Again, a possible real use case for this guard is making a question to an user about if is secure of navigating to other page because there is some data in the current view that has not been persisted. If the users clicks yes we return true and the navigation happens and if clicks no the navigation will not happen. In this practical example, we simply set a true allowing always the navigation but you can clone the code and make changes to see how it works.

CanLoad:

We create a guard with the CanLoad interface using the command:

ng generate guard catalog/can-load

or

ng g g catalog/can-load

We add a console.log to be aware of when we are using the guard:

// can-load.guard.ts
import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanActivate,
  CanLoad,
  Route,
  RouterStateSnapshot,
  UrlSegment,
  UrlTree,
} from '@angular/router';
import { Observable } from 'rxjs';

@Injectable()
export class CanLoadGuard implements CanLoad {
  canLoad(
    route: Route,
    segments: UrlSegment[]
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    console.log('CanLoad');
    return true;
  }
}

As in the previous guard, the class implements an interface, in this case the CanLoad interface which has the canLoad method. We can return a boolean, a Promise, an Observable or a UrlTree. In this case if the boolean is true we allow requesting the module of the requested route, if it is false we disallow it. If we return UrlTree we will be redirected to the specified URL. We can also define the constructor and inject a service.

We then provide (as a service) the guard in the module that imports the RoutingModule that uses the guard, in this case, CatalogModule:

// catalog.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CatalogComponent } from './catalog.component';
import { CatalogRoutingModule } from './catalog-routing.module';
import { CanLoadGuard } from './can-load.guard';

@NgModule({
  declarations: [CatalogComponent],
  imports: [CommonModule, CatalogRoutingModule],
  providers: [CanLoadGuard],
})
export class CatalogModule {}

And then we use it in the RoutingModule:

// catalog-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CanLoadGuard } from './can-load.guard';
import { CatalogComponent } from './catalog.component';

const routes: Routes = [
  {
    path: '',
    component: CatalogComponent,
  },
  {
    path: 'product',
    loadChildren: () =>
      import('./product/product.module').then(
        (m) => m.ProductModule
      ),
    canLoad: [CanLoadGuard],
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class CatalogRoutingModule {}

The result again is the first shown in the demo video. We will see two different videos, in one of them we will see how the module is requested and arrives and in the second one how this will not happen. To make the example more visible on chrome devtools I will select the network tab and in the NO throttling select I will select Slow 3G (You can see how I do this on the video):

Allowing the request and getting the response:

You can see as when I click the button info to navigate to the catalog/product URL the module is requested, the spinner starts showing and when the module arrives, the spinner hides and the view is rendered.

If we don't allow the request this is the result:

No request is being done because we are returning false on the CanLoad guard.

Again, a possible real use case for this guard is disallowing modules requests depending on the user role, this way the bundle size is minimal and the application will start sooner. In this practical example, we simply set a true allowing always the request but you can clone the code and make changes to see how it works.

Resolve:

We create a resolver with the Resolve interface using the command:

ng generate resolver catalog/product

or

ng g r catalog/product

We add a console.log to be aware of when we are using the resolver. We also create a dummy method that returns a product depending on the id we pass in the URL and some dummy data (in a real case this could be a request to an backend API to get a product based on the id):

// product.resolver.ts
import { Injectable } from '@angular/core';
import {
  Resolve,
  RouterStateSnapshot,
  ActivatedRouteSnapshot,
} from '@angular/router';
import { Observable, of } from 'rxjs';
import { Product } from './product.model';

@Injectable()
export class ProductResolver implements Resolve<Product> {
  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<Product> {
    console.log('Resolver');
    return this.createProduct(route.paramMap.get('id'));
  }

  createProduct(id: any) {
    const description =
      'Lorem Ipsum is simply dummy text of the printing and typesetting industry.';
    const product = new Product(id, description, 'assets/images/cloud.png');
    return of(product);
  }
}

As in the previous guards, the class implements an interface, in this case the Resolve interface which has the resolve method. We can return an Observable (Instead of product we can return the model we choose). We can also define the constructor and inject a service.

We then provide (as a service) the resolver in the module that imports the RoutingModule that uses the resolver, in this case, ProductModule:

// product.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductRoutingModule } from './product-routing.module';
import { ProductComponent } from './product.component';
import { ProductResolver } from './product.resolver';

@NgModule({
  declarations: [ProductComponent],
  imports: [CommonModule, ProductRoutingModule],
  providers: [ProductResolver],
})
export class ProductModule {}

And then we use it in the RoutingModule:

// product-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProductComponent } from './product.component';
import { ProductResolver } from './product.resolver';

const routes: Routes = [
  {
    path: ':id',
    component: ProductComponent,
    resolve: { product: ProductResolver },
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class ProductRoutingModule {}

To get the returning data from the resolver on the component we are navigating to we do it instantiating the ActivatedRoute class in the constructor of the component:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { Product } from './product.model';

@Component({
  selector: 'app-product',
  templateUrl: './product.component.html',
  styleUrls: ['./product.component.scss'],
})
export class ProductComponent implements OnInit {
  actualProduct: Product;
  constructor(private activatedRoute: ActivatedRoute) {
    this.actualProduct = new Product('', '', '');
  }

  ngOnInit(): void {
    this.activatedRoute.data.subscribe((data) => {
      this.actualProduct = data["product"];
    });
  }
}

If we get an error for example if we are requesting an endpoint that is not available to get some data, we can handle the error and return EMPTY observable and we can navigate to an error page for example using the router:

this.router.navigate(["/error"]);
return EMPTY;

Again, a possible real use case for the resolver is when we want to guarantee to access a view with displaying data previously preloaded.

Bibliography: