Advanced Routing

August 2018 State

Agenda

Introduction to the App

  • Repo: Here
  • what is the app about
  • Technical features (server, forms, guards)

Router Events

  • Routing Basics
  • Feature Target
  • Steps

Routing Basics

https://vsavkin.com/angular-2-router-d9e30599f9ea

Feature Target

Display a spinner in case the application is waiting for the data being loaded and seems not to be responding.

Step 1

To observe the unwanted behaviour lets add a delay to our Service.

              findOne(id: number): Observable<Book> {
                return this.http.get<Book>(BookService.BOOK_URI + '/' + id)
                  .pipe(delay(1000));
              }
            
How does the app behave now?

Step 2

To be sure that it is the resolve that is causing the issue lets trace it.

              RouterModule.forRoot(routes, { enableTracing: true }),
            
See the resolve now?

Step 3

Lets handle the ResolveStart - ResolveEnd case and show something special

              export class AppComponent {
                isLoadingData = false;
                
                constructor(private router: Router) {
                  router.events.subscribe((routerEvent: RouterEvent) => {
                    this.checkRouterEvent(routerEvent);
                  });
                }
            

Step 4


              checkRouterEvent(routerEvent: RouterEvent): void {
                if (routerEvent instanceof ResolveStart) {
                  this.isLoadingData = true;
                }
                if (routerEvent instanceof ResolveEnd ||
                    routerEvent instanceof NavigationCancel ||
                    routerEvent instanceof NavigationError) {
                    this.isLoadingData = false;
                  }
              }
            

Step 5

The template can now use the isLoadingData flag now.

                    

Final Step

Lets tweak it with some CSS

                .loader {
                  border: 16px solid #f3f3f3; /* Light grey */
                  border-top: 16px solid #3498db; /* Blue */
                  border-radius: 50%;
                  width: 120px;
                  height: 120px;
                  animation: spin 2s linear infinite;
                }
                @keyframes spin {
                  0% { transform: rotate(0deg); }
                  100% { transform: rotate(360deg); }
                }
            

Guards Details

Currently there are 4 Guards definded by Angular
  • CanActivate - Decides if a route can be activated
  • CanActivateChild - Decides if children routes of a route can be activated
  • CanDeactivate - Decides if a route can be deactivated
  • CanLoad - Decides if a module can be loaded lazily

CanActivate


interface CanActivate {
  canActivate(route: ActivatedRouteSnapshot, 
              state: RouterStateSnapshot): 
              Observable<boolean | UrlTree> | 
              Promise<boolean | UrlTree> | 
              boolean | 
              UrlTree
}
            
https://angular.io/api/router/CanActivate

CanActivateChild


interface CanActivateChild {
  canActivateChild(route: ActivatedRouteSnapshot, 
              state: RouterStateSnapshot): 
              Observable<boolean | UrlTree> | 
              Promise<boolean | UrlTree> | 
              boolean | 
              UrlTree
}
            
https://angular.io/api/router/CanActivateChild

CanDeactivate


interface CanDeactivate<T> {
  canDeactivate(component: T, 
                currentRoute: ActivatedRouteSnapshot, 
                currentState: RouterStateSnapshot, 
                nextState?: RouterStateSnapshot): 
                Observable<boolean | UrlTree> | 
                Promise<boolean | UrlTree> | 
                boolean | 
                UrlTree
}
            
https://angular.io/api/router/CanDeactivate

CanLoad


interface CanLoad {
  canLoad(route: Route, 
          segments: UrlSegment[]): 
          Observable<boolean> | 
          Promise<boolean> | 
          boolean
} 
            
https://angular.io/api/router/CanLoad

CanDeactivate

  • Feature Target
  • Steps

Feature Target

Prevent the user from leaving a dirty form unless he confirms his actions.

Step 1

Lets define an DirtyAware Interface at (rename the folder):

                  src/app/shared/routing/dirty-aware.ts
              
And inside:

                  export interface DirtyAware {
                    isDirty(): boolean;
                  }
              

Step 2

Now we can create an abstract guard service implementing a CanDeactivate interface

                      src/app/shared/routing/can-deactivate-guard.service.ts
                  
And inside:

                      @Injectable()
                      export class CanDeactivateGuard implements CanDeactivate<DirtyAware> {
                        
                        canDeactivate(): Observable<boolean> | boolean {
                          return false;
                        }

                      }
                  

Step 3

Now, having a dummy guard, lets connect it to our existing Routing at routes.ts

                ...
                  component: BookDetailsComponent,
                  resolve: {
                    book: BookDetailsResolver
                  },
                  canDeactivate: [CanDeactivateGuard]
                },
                {
                  path: 'book',
                  component: BookDetailsComponent,
                  canDeactivate: [CanDeactivateGuard]
                },
                ...
              
Try it now.

Step 4

Naturally, it is not present in the app.module.ts - providers part, adapt it.

                  @NgModule({
                    ...,
                    imports: [
                      ...
                    ],
                    providers: [CanDeactivateGuard],
                    bootstrap: [AppComponent]
                  })
              

Step 5

Now it is time to make use of the DirtyAware interface.

                export class BookDetailsComponent implements OnInit, DirtyAware {
            
and the implmementation:

                  isDirty(): boolean {
                    return this.bookForm.dirty && !this.submitted;
                  }
            

Step 6

The Guard itself also needs to use it

                canDeactivate(
                  component: DirtyAware
                ): Observable<boolean> | boolean {

                  if (!component.isDirty()) {
                    return true;
                  }

                  return false;
                }
            

Step 7

Improve the UX - ask the User now

                canDeactivate(
                  component: DirtyAware
                  ): Observable<boolean> | boolean {
                    
                  if (!component.isDirty()) {
                    return true;
                  }
              
                  if (confirm('Are you sure to cancel?')) {
                    return true;
                  }
              
                  return false;
                }
            

Step 8*

Rework the modal dialog to use the Material Design. Generate a new Component

              ng g c can-deactivate-dialog
          

Step 9*

Invoke it when the can-deactivate-guard is triggered

              constructor(public dialog: MatDialog) {}

              ...
              const dialogRef = this.dialog.open(CanDeactivateDialogComponent);
              return dialogRef.afterClosed().pipe(
                map(result => {
                  if (result) {
                    return true;
                  }
                    return false;
                })
              );
          

Step 10*

This does not work now because of a very good reason.

                BookModule,
                MatDialogModule
              ],
              entryComponents: [CanDeactivateDialogComponent],
              providers: [CanDeactivateGuard],
          

Step 11*

Now, lets tweak the dialog so it displays something meaningfull.

                

Discard changes

Are you sure?

Step 12*

Repair the declarations of the dialog component, so it works.

                @NgModule({
                  declarations: [AppComponent,
                    CanDeactivateDialogComponent],
                  imports: [
          

CanActivate

  • Feature Target
  • Steps

Feature Target

Allow adding new Books only for those Users having a concrete permission.

Step 1

Create a Roles Object

                  export enum Role {
                    ANONYMOUS, ADMIN
                  }
            

Step 2

Extend the Routes so they are able to hold the Roles that will guard them.

                  export interface AuthorizedRoute extends Route {
                    authorizedRoles?: Role[];
                    permitAll?: boolean;
                    children?: AuthorizedRoutes;
                  }

                  export declare type AuthorizedRoutes = AuthorizedRoute[];
            

Step 3

Overwrite the Routing configuration and provide some Role definitions

                ...
                permitAll: true
                ...

                path: 'book',
                component: BookDetailsComponent,
                canDeactivate: [CanDeactivateGuard],
                authorizedRoles: [Role.ADMIN]
              },
            

Step 4

Trigger reset config so the router can actually use the guards.

                  export class AppModule {
                    constructor (private router: Router) {
                          // add security checkers
                          router.resetConfig(addAuthorizationGuards(routes));
                    }
            

Step 5

Provide a bit of magic so the Routing is actually using the Roles. (File: add-authorization-guards.ts)

                  export function addAuthorizationGuards(routes: Routes): Routes {
                    addAuthorizationGuardsToRoutesHavingPathsAndComponents(routes);
                    return routes;

                    function addAuthorizationGuardsToRoutesHavingPathsAndComponents(routesToGuard: Routes) {
                      for (const route of routesToGuard) {
                        if (route.path && route.component) {
                          route.canActivate = route.canActivate || [];
                          route.canActivate.push(ActivateIfUserAuthorized);
                        }
                        if (route.children) {
                          addAuthorizationGuardsToRoutesHavingPathsAndComponents(route.children);
                        }
                      }
                    }
                  }
            

Step 6_1

Now implement the overwritten guard.

@Injectable()
export class ActivateIfUserAuthorized implements CanActivate {
  constructor(private security: SecurityService, private router: Router) {}
   
  canActivate(route: ActivatedRouteSnapshot): Observable<boolean> | boolean {
    if (route && route.routeConfig && route.routeConfig['permitAll']) {
      return true;
    }
            

Step 6_2


  if (route && route.routeConfig && route.routeConfig['authorizedRoles']) {
    const routeAuthorizedRoles: Role[] = route.routeConfig['authorizedRoles'];
    return this.security.getCurrentUserRoles().pipe(
      map((roles: Role[]) =>
        roles.reduce(
          (prev, role) => routeAuthorizedRoles.includes(role) || prev,
          false
        )
      ),
      catchError(() => of(false))
    );
  }
  return false;
  }
}
            

Step 7

Create a Security Service that returns the current User-Roles.

@Injectable()
export class SecurityService {

  currentRoles: Role[] = [Role.ADMIN, Role.ANONYMOUS];

  getCurrentUserRoles(): Observable<Role[]> {
    return of(this.currentRoles);
  }
}
            

Step 8

Now provide the Service and Guard. Then see if everything is working okay. Play with it changing the Roles that the Service returns.

                  providers: [BookService,
                  ActivateIfUserAuthorized,
                  SecurityService]          
            

Step 9

Provide a bar with a slider so one can switch the roles on and off. (File: app.component.html)

                  Anonymous
                    Admin
                  
            
and style it

                .admin-menu {
                  background-color: #3498db;
                }
          

Step 10

Import it in the app.module so it runs smoothly.

                  BookModule,
                  MatDialogModule,
                  MatSlideToggleModule
                ],
                entryComponents: [CanDeactivateDialogComponent],
            

Step 11

Make use of the slider!

                  Anonymous
                    Admin
                  
            

Step 12

Implement event handling

  slideUpdate(e: Event) {
    console.log('Changing App Roles');
    this.securityService.changeRoleTo(e['checked']);
  }
            

Step 13

Provide an implememntation to the Security Service.

        changeRoleTo(isAdmin: boolean) {
          if (isAdmin) {
            this.currentRoles = [Role.ADMIN];
          } else {
            this.currentRoles = [Role.ANONYMOUS];
          }
        }