Angular Guards in Disneyland Park

In an Angular application you might need to run some checks before allowing the user to navigate to or away from a page. That’s where Angular Guards come into play. 

The picture below displays how guards work. A user is trying to navigate in the application, but before allowing navigation there is a guard that is running some checks and then the guard can decide for the next steps: allow the user or not to navigate, display a confirmation/information dialog or even redirect the user somewhere else.

Guards

For this guide we will take a trip into Disneyland, Walt Disney and Disney Park. As we pass guards we discuss what they are checking and how they are implemented by giving a full picture of Guards. 

  • CanActivate

We just arrived at the main entrance of the Park and we see a guard waiting to check our ticket. If we have a ticket and it’s valid, the Guard will allow us to enter the park. 

Now let’s see how we can implement it. We have a component called Disneyland and we want the guests to access this component only when they are authenticated, have a valid token in a real case scenario or a valid ticket in our case.  We want to prevent the users from accessing this component either by a navigation link or through URL access. When the user opens the app, a form to enter the ticket is displayed as below: 

Disneyland Entrance

According to Angular Documentation CanActivate is an interface that a class can implement to be a guard deciding if a route can be activated. 

With this in mind we implement our TicketGuard as below: 

@Injectable({
    providedIn: 'root',
  })
export class TicketGuard implements CanActivate {
 
    constructor(
        private disneyService: DisneyService,
        private snackBar: MatSnackBar
        ) {}
 
    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const ticket = this.disneyService.getTicket();       
        const isValid = ticket && ticket.startsWith('D');
        
        if (!isValid) {
             this.snackBar.open('Your ticket is not valid!', 'OK');
        }
 
        return isValid;
    }
}

We are checking that the user has a ticket and this ticket should start with D to be valid. Now it’s time to add this guard in our route as below: 

const routes: Routes = [
    {path: 'home', component: GuardsHomeComponent},
   {
       path: 'disneyland',
       component: DisneyComponent,
       canActivate: [TicketGuard],
    },
    {path: '', redirectTo: 'home', pathMatch: 'full'},
];

We have configured here that the path disneyland which loads DisneylandComponent is protected by TicketGuard.

  • CanActivateChild

Let’s suppose that once you enter the main entrance there are some shops and cafes  and two other entrances, Walt Park and Disney Park. Each of these park should be available once you enter at the main entrance. 

For this case we need to handle protecting nested routes, both park-walt-component and park-disney-component are nested inside disneyland-component. We could use CanActivate for each of these components, but we follow DRY

According to Angular Documentation CanActivateChild is an interface that a class can implement to be a guard deciding if a child route can be activated. 

We will be using the same guard and will update the above TicketGuard to implement the CanActivateChild Interface  and the route protection is implemented as below: 

const routes: Routes = [
   {path: 'home', component: GuardsHomeComponent},
   
   {
     path: 'disneyland',
     component: DisneyComponent,
     canActivate: [TicketGuard],
     canActivateChild: [TicketGuard],
     children: [
       {
         path: 'park', component: ParkDisneyComponent,
       },
       {
         path: 'walt', component: WaltDisneyComponent,
       }
     ]
   },
 
   {path: '', redirectTo: 'home', pathMatch: 'full'},
];
  • Resolve

Time after time some rides are under maintenance. In case all the rides of the park are under maintenance we do not want to allow the user to enter there, but instead redirect to the nice shops and cafes. We also want to show the user a list of all the available rides once he enters inside one of the parks. 

According to Angular Documentation, Resolve is an interface that classes can implement to be a data provider, which can be used with router to resolve data during navigation. 

By implementing Resolve guard we will fetch the rides for the selected park and if all rides are under maintenance we will redirect the user to the park, if not we will redirect him to the Disneyland.

@Injectable({
  providedIn: 'root',
})
export class RidesResolverService implements Resolve<Ride[]> {
  constructor(
        private disneyService: DisneyService, 
        private router: Router,
        private snackBar: MatSnackBar
        ) {}
 
  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):    Observable<Ride[]> | Observable<never> {
    const url = state.url;
 
    const park = url.indexOf('park') > -1 ? DISNEY.PARK : DISNEY.WALT;
 
    return this.disneyService.getRides(park).pipe(
      take(1),
      mergeMap(rides => {
        if (rides?.length > 0) {
          return of(rides);
        } else {
          this.snackBar.open('Currently all the rides are in maintenance, enjoy the DISNEY shops!', 'OK');
          this.router.navigate(['guards', 'disneyland']);
          return EMPTY;
        }
      })
    );
  }
}

And inside any of the parks we will get the list of rides by routing as below: 

 this.route.data
    .subscribe((data: { rides: Ride[] }) => {
      this.rides = data.rides;
    });

Next we need to configure resolve in both park routes as below: 

 children: [
       {
         path: 'park', component: ParkDisneyComponent,
         resolve: {
          rides: RidesResolverService
        }
       },
       {
         path: 'walt', component: WaltDisneyComponent,
         resolve: {
          rides: RidesResolverService
        }
       }
  • CanDeactivate 

After a guest enters one of the parks, before leaving we might want to make sure that he experienced all the magic: visited a castle or took some rides. In case he did not do any we want to ask him if he is sure that he wants to leave and once we get the confirmation allow him to leave the park. 

Brought in a real life scenario, after the user has filled a large and complex form with data, before allowing him to navigate away in case the form was not saved, we might want to make sure if the user is purposely navigating away or it is by mistake. 

According to Angular Documentation, CanDeactivate is an interface that a class can implement to be a guard deciding if a route can be deactivated. 

Our guard will look like this:    

@Injectable({
     providedIn: 'root',
   })
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
    
     canDeactivate(component: CanComponentDeactivate) {
       const canDeactivate = component.canDeactivate ? component.canDeactivate() : true;
 
       return canDeactivate;
     }
   }

canDeactivate function implemented in each of the parks looks like this:  

canDeactivate(): Observable<boolean> | boolean {
 
    if (this.experiencedDisney) {
      	return true;
    }
 
    const confirmation = window.confirm('You sure you want to leave Walt Disney without experiencing it?');
    return of(confirmation);
  }

Next step is to configure the CanDeactivate Guard inside each of the paths: 

{
         path: 'park', component: ParkDisneyComponent,
         canDeactivate: [CanDeactivateGuard],
         resolve: {
          rides: RidesResolverService
        }
       },
  • CanLoad

Last guard we will have a look is CanLoad, which prevents the loading of a LazyLoadedModule. While CanActivate is protecting the route, through CanLoad Guard we can protect the performance, by not loading modules in the browser that can not be activated. The implementation and configuration of CanLoad Guard follows the same structure as CanActivateGuard, but it’s applied on the lazy loaded modules instead of path. 

This was a short guide in Angular Guards, their functionalities, implementation and configuration. If you have any question or any idea on how to improve it, please do not hesitate to reach out. 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s