Jerry's Blog

Recording what I learned everyday

View on GitHub


26 June 2019

Angular (13) -- Routing

by Jerry Zhang

Day 21: Angular Routing

Route Guards

Create a new service

auth-guard.service.ts

This class should implement CanActivate interface in @angular/router. It enforces us to write a canActivate() method. This method will receive two arguments, ActivatedRouteSnapshot and RouterStateSnapshot.

Where do we get these two parameters from? We will define that Angular should execute this code before a route is loaded.

canActivate() method also return something. We put the return type after the parameters. It can return an Observable with boolean, a Promise with boolean, or just a boolean.

The Observable and Promise are asynchronous, while returning a boolean is synchronous. When is it synchronous? Some guards execute some code which runs completely on the client, thus it is synchronous, or maybe some code takes a couple of seconds to finish because of a timeout or reach out to a server, so it runs asynchronously.

Mock a fake auth service

To test our auth-guard service, we create a fake service.

export class AuthService {
  loggedIn = false;

  isAuthenticated() {
    const promise = new Promise(
      (resolve, reject) => {
        setTimeout(() => {
          resolve(this.loggedIn);
        }, 800);
      }
    );
    return promise;
  }

  login() {
    this.loggedIn = true;
  }

  logout() {
    this.loggedIn = false;
  }
}

Use the fake service in the auth-guard

canActivate(route: ActivatedRouteSnapshot,
              state: RouterStateSnapshot):
    Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return this.authService.isAuthenticated()
      .then(
        (authenticated: boolean) => {
          if (authenticated) {
            return true;
          } else {
            this.router.navigate(['/']);
            return false;
          }
        }
      );
}

If this user is authenticated, return true; otherwise navigate this user to the root and return false.

Define which routes should be protected by this guard.

In the app-routing.module file, add canActivate property to the routes that we want to guard. It automatically get applied to all the child routes.

{ path: 'servers', canActivate: [AuthGuardService], component: ServersComponent, children: [
      { path: ':id', component: ServerComponent },
      { path: ':id/edit', component: EditServerComponent }
    ] },

This will make sure that servers routes as well as all its child routes, are now only accessible if the AuthGuard, canActivate method returns true in the end.

Tips: Remember to add the two new services as providers in our app.module.ts

Now, we can only click other tabs. If we click the service tab, it will lead us to the home page after 800 milliseconds.

Protecting Child Routes with canActivateChild

We could move the canActivate attribute to any child path, but if we have many child-paths, then we have to add it to all of them. This is not a convenient way.

There is another guard that we can use, CanActivateChild. We can implement this interface and overwrite canActivateChild method.

Because the logic is all the same, we can reuse our canActivate method.

canActivateChild(childRoute: ActivatedRouteSnapshot,
                   state: RouterStateSnapshot):
      Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return this.canActivate(childRoute, state);
  }

Then we can use canActivateChild in the app-routing.module

We can choose whether we want to protect the whole route or its child routes.

{ path: 'servers',
    // canActivate: [AuthGuardService],
    canActivateChild: [AuthGuardService],
    component: ServersComponent,
    children: [
      { path: ':id', component: ServerComponent },
      { path: ':id/edit', component: EditServerComponent }
    ] },

Add login and logout buttons

In the home component, we add two buttons.

<button class="btn btn-light" (click)="onLogin()">Login</button>
<button class="btn btn-light" (click)="onLogout()">Logout</button>

Then in these two methods, we simply call the authService which needs to be injected to the home component.

export class HomeComponent implements OnInit {

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

  ngOnInit() {
  }

  onLoadServer(id: number) {
    this.router.navigate(['/servers', id, 'edit'], {queryParams: {allowEdit: '1'}, fragment: 'loading'});
  }

  onLogin() {
    this.authService.login();
  }

  onLogout() {
    this.authService.logout();
  }
}

Now this is working.

Controlling Navigation with canDeactivate

canActivate is used for controlling access into a route, now we will discuss whether a user is allowed to leave a route or not.

This is very useful because sometimes we want to ask a user if he/she really want to leave this page, for example, without saving, the user click the go back by accident.

this.router.navigate(['../'], {relativeTo: this.route});

Add a guard service

A guard always needs to be a service.

In this service, we firstly export an interface.

import {Observable} from 'rxjs';
import {ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot, UrlTree} from '@angular/router';

export interface CanComponentDeactivate {
  canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(component: CanComponentDeactivate,
                currentRoute: ActivatedRouteSnapshot,
                currentState: RouterStateSnapshot,
                nextState?: RouterStateSnapshot):
      Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return component.canDeactivate();
  }
}

Config app-routing.module file

In the app-routing.module, we can add a canDeactivate property, which is passed in an array. We point to our CanDeactivateGuard.

Now, Angular will run this guard whenever we try to leave this path.

Implement CanComponentDeactive interface in the component

In the component, we need to the logic to check whether this user can leave or not.

canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
    if (!this.allowEdit) {
      return true;
    }
    if ((this.serverName !== this.server.name || this.serverStatus !== this.server.status) &&
        !this.changesSaved) {
        return confirm('Do you want to discard the changes?');
    } else {
      return true;
    }
  }
tags: Angular