Skip to main content

Angular - Routing and Navigation Part 6



Milestone 5: Route guards

At the moment, any user can navigate anywhere in the application anytime. That's not always the right thing to do.
  • Perhaps the user is not authorized to navigate to the target component.
  • Maybe the user must login (authenticate) first.
  • Maybe you should fetch some data before you display the target component.
  • You might want to save pending changes before leaving a component.
  • You might ask the user if it's OK to discard pending changes rather than save them.
You can add guards to the route configuration to handle these scenarios.
A guard's return value controls the router's behavior:
  • If it returns true, the navigation process continues.
  • If it returns false, the navigation process stops and the user stays put.


The guard can also tell the router to navigate elsewhere, effectively canceling the current navigation.
The guard might return its boolean answer synchronously. But in many cases, the guard can't produce an answer synchronously. The guard could ask the user a question, save changes to the server, or fetch fresh data. These are all asynchronous operations.
Accordingly, a routing guard can return an Observable or a Promise and the router will wait for the observable to resolve to true or false.
The router supports multiple guard interfaces:
  • CanActivate to mediate navigation to a route.
  • CanActivateChild to mediate navigation to a child route.
  • CanDeactivate to mediate navigation away from the current route.
  • Resolve to perform route data retrieval before route activation.
  • CanLoad to mediate navigation to a feature module loaded asynchronously.
You can have multiple guards at every level of a routing hierarchy. The router checks the CanDeactivateand CanActivateChild guards first, from the deepest child route to the top. Then it checks the CanActivateguards from the top down to the deepest child route. If the feature module is loaded asynchronously, the CanLoad guard is checked before the module is loaded. If any guard returns false, pending guards that have not completed will be canceled, and the entire navigation is canceled.
There are several examples over the next few sections.

CanActivate: requiring authentication

Applications often restrict access to a feature area based on who the user is. You could permit access only to authenticated users or to users with a specific role. You might block or limit access until the user's account is activated.
The CanActivate guard is the tool to manage these navigation business rules.

Add an admin feature module

In this next section, you'll extend the crisis center with some new administrative features. Those features aren't defined yet. But you can start by adding a new feature module named AdminModule.
Create an admin folder with a feature module file, a routing configuration file, and supporting components.
The admin feature file structure looks like this:
src/app/admin
admin-dashboard.component.ts
admin.component.ts
admin.module.ts
admin-routing.module.ts
manage-crises.component.ts
manage-heroes.component.ts
The admin feature module contains the AdminComponent used for routing within the feature module, a dashboard route and two unfinished components to manage crises and heroes.



import { Component } from '@angular/core';

@Component({
  template:  `
    Dashboard


  `
})
export class AdminDashboardComponent { }

Since the admin dashboard RouterLink is an empty path route in the AdminComponent, it is considered a match to any route within the admin feature area. You only want the Dashboard link to be active when the user visits that route. Adding an additional binding to the Dashboard routerLink,[routerLinkActiveOptions]="{ exact: true }", marks the ./ link as active when the user navigates to the /admin URL and not when navigating to any of the child routes.
The initial admin routing configuration:

src/app/admin/admin-routing.module.ts (admin routing)
const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    children: [
      {
        path: '',
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ]
      }
    ]
  }
];

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

Component-less route: grouping routes without a component

Looking at the child route under the AdminComponent, there is a path and a children property but it's not using a component. You haven't made a mistake in the configuration. You've defined a component-lessroute.

The goal is to group the Crisis Center management routes under the admin path. You don't need a component to do it. A component-less route makes it easier to guard child routes.
Next, import the AdminModule into app.module.ts and add it to the imports array to register the admin routes.

src/app/app.module.ts (admin module)
import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { AppComponent }            from './app.component';
import { PageNotFoundComponent }   from './not-found.component';

import { AppRoutingModule }        from './app-routing.module';
import { HeroesModule }            from './heroes/heroes.module';
import { CrisisCenterModule }      from './crisis-center/crisis-center.module';
import { AdminModule }             from './admin/admin.module';

import { DialogService }           from './dialog.service';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HeroesModule,
    CrisisCenterModule,
    AdminModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    PageNotFoundComponent
  ],
  providers: [
    DialogService
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Add an "Admin" link to the AppComponent shell so that users can get to this feature.

src/app/app.component.ts (template)
template: `
  

Angular

Router

a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> <a routerLink="/admin" routerLinkActive="active">Admin</a> <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a> <router-outlet></router-outlet> <router-outlet name="popup"></router-outlet> `

Guard the admin feature

Currently every route within the Crisis Center is open to everyone. The new admin feature should be accessible only to authenticated users.
You could hide the link until the user logs in. But that's tricky and difficult to maintain.
Instead you'll write a canActivate() guard method to redirect anonymous users to the login page when they try to enter the admin area.
This is a general purpose guard—you can imagine other features that require authenticated users—so you create an auth-guard.service.ts in the application root folder.
At the moment you're interested in seeing how guards work so the first version does nothing useful. It simply logs to console and returns true immediately, allowing navigation to proceed:

src/app/auth-guard.service.ts (excerpt)
import { Injectable }     from '@angular/core';
import { CanActivate }    from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate() {
    console.log('AuthGuard#canActivate called');
    return true;
  }
}

Next, open admin-routing.module.ts, import the AuthGuard class, and update the admin route with a canActivate guard property that references it:

src/app/admin/admin-routing.module.ts (guarded admin route)
import { AuthGuard }                from '../auth-guard.service';

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ],
      }
    ]
  }
];

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

The admin feature is now protected by the guard, albeit protected poorly.

Teach AuthGuard to authenticate

Make the AuthGuard at least pretend to authenticate.
The AuthGuard should call an application service that can login a user and retain information about the current user. Here's a demo AuthService:

src/app/auth.service.ts (excerpt)
import { Injectable } from '@angular/core';

import { Observable, of } from 'rxjs';
import { tap, delay } from 'rxjs/operators';

@Injectable()
export class AuthService {
  isLoggedIn = false;

  // store the URL so we can redirect after logging in
  redirectUrl: string;

  login(): Observable {
    return of(true).pipe(
      delay(1000),
      tap(val => this.isLoggedIn = true)
    );
  }

  logout(): void {
    this.isLoggedIn = false;
  }
}

Although it doesn't actually log in, it has what you need for this discussion. It has an isLoggedIn flag to tell you whether the user is authenticated. Its login method simulates an API call to an external service by returning an observable that resolves successfully after a short pause. The redirectUrl property will store the attempted URL so you can navigate to it after authenticating.
Revise the AuthGuard to call it.

src/app/auth-guard.service.ts (v2)
import { Injectable }       from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot
}                           from '@angular/router';
import { AuthService }      from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    let url: string = state.url;

    return this.checkLogin(url);
  }

  checkLogin(url: string): boolean {
    if (this.authService.isLoggedIn) { return true; }

    // Store the attempted URL for redirecting
    this.authService.redirectUrl = url;

    // Navigate to the login page with extras
    this.router.navigate(['/login']);
    return false;
  }
}

Notice that you inject the AuthService and the Router in the constructor. You haven't provided the AuthService yet but it's good to know that you can inject helpful services into routing guards.
This guard returns a synchronous boolean result. If the user is logged in, it returns true and the navigation continues.
The ActivatedRouteSnapshot contains the future route that will be activated and the RouterStateSnapshotcontains the future RouterState of the application, should you pass through the guard check.
If the user is not logged in, you store the attempted URL the user came from using the RouterStateSnapshot.url and tell the router to navigate to a login page—a page you haven't created yet. This secondary navigation automatically cancels the current navigation; checkLogin() returns false just to be clear about that.

Add the LoginComponent

You need a LoginComponent for the user to log in to the app. After logging in, you'll redirect to the stored URL if available, or use the default URL. There is nothing new about this component or the way you wire it into the router configuration.
Register a /login route in the login-routing.module.ts and add the necessary providers to the providersarray. In app.module.ts, import the LoginComponent and add it to the AppModule declarations. Import and add the LoginRoutingModule to the AppModule imports as well.



  1. import { NgModule } from '@angular/core';
  2. import { BrowserModule } from '@angular/platform-browser';
  3. import { FormsModule } from '@angular/forms';
  4. import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
  5.  
  6. import { Router } from '@angular/router';
  7.  
  8. import { AppComponent } from './app.component';
  9. import { AppRoutingModule } from './app-routing.module';
  10.  
  11. import { HeroesModule } from './heroes/heroes.module';
  12. import { ComposeMessageComponent } from './compose-message.component';
  13. import { LoginRoutingModule } from './login-routing.module';
  14. import { LoginComponent } from './login.component';
  15. import { PageNotFoundComponent } from './not-found.component';
  16.  
  17. import { DialogService } from './dialog.service';
  18.  
  19. @NgModule({
  20. imports: [
  21. BrowserModule,
  22. FormsModule,
  23. HeroesModule,
  24. LoginRoutingModule,
  25. AppRoutingModule,
  26. BrowserAnimationsModule
  27. ],
  28. declarations: [
  29. AppComponent,
  30. ComposeMessageComponent,
  31. LoginComponent,
  32. PageNotFoundComponent
  33. ],
  34. providers: [
  35. DialogService
  36. ],
  37. bootstrap: [ AppComponent ]
  38. })
  39. export class AppModule {
  40. // Diagnostic only: inspect router configuration
  41. constructor(router: Router) {
  42. console.log('Routes: ', JSON.stringify(router.config, undefined, 2));
  43. }
  44. }

Guards and the service providers they require must be provided at the module-level. This allows the Router access to retrieve these services from the Injector during the navigation process. The same rule applies for feature modules loaded asynchronously.

CanActivateChild: guarding child routes

You can also protect child routes with the CanActivateChild guard. The CanActivateChild guard is similar to the CanActivate guard. The key difference is that it runs before any child route is activated.


You protected the admin feature module from unauthorized access. You should also protect child routes within the feature module.
Extend the AuthGuard to protect when navigating between the admin routes. Open auth-guard.service.tsand add the CanActivateChild interface to the imported tokens from the router package.
Next, implement the canActivateChild() method which takes the same arguments as the canActivate()method: an ActivatedRouteSnapshot and RouterStateSnapshot. The canActivateChild() method can return an Observable or Promise for async checks and a boolean for sync checks. This one returns a boolean:

src/app/auth-guard.service.ts (excerpt)
import { Injectable }       from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  CanActivateChild
}                           from '@angular/router';
import { AuthService }      from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    let url: string = state.url;

    return this.checkLogin(url);
  }

  canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.canActivate(route, state);
  }

/* . . . */
}

Add the same AuthGuard to the component-less admin route to protect all other child routes at one time instead of adding the AuthGuard to each route individually.

src/app/admin/admin-routing.module.ts (excerpt)
const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard],
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ]
      }
    ]
  }
];

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

CanDeactivate: handling unsaved changes

Back in the "Heroes" workflow, the app accepts every change to a hero immediately without hesitation or validation.
In the real world, you might have to accumulate the users changes. You might have to validate across fields. You might have to validate on the server. You might have to hold changes in a pending state until the user confirms them as a group or cancels and reverts all changes.
What do you do about unapproved, unsaved changes when the user navigates away? You can't just leave and risk losing the user's changes; that would be a terrible experience.
It's better to pause and let the user decide what to do. If the user cancels, you'll stay put and allow more changes. If the user approves, the app can save.
You still might delay navigation until the save succeeds. If you let the user move to the next screen immediately and the save were to fail (perhaps the data are ruled invalid), you would lose the context of the error.
You can't block while waiting for the server—that's not possible in a browser. You need to stop the navigation while you wait, asynchronously, for the server to return with its answer.
You need the CanDeactivate guard.

Cancel and save

The sample application doesn't talk to a server. Fortunately, you have another way to demonstrate an asynchronous router hook.
Users update crisis information in the CrisisDetailComponent. Unlike the HeroDetailComponent, the user changes do not update the crisis entity immediately. Instead, the app updates the entity when the user presses the Save button and discards the changes when the user presses the Cancel button.
Both buttons navigate back to the crisis list after save or cancel.

src/app/crisis-center/crisis-detail.component.ts (cancel and save methods)
cancel() {
  this.gotoCrises();
}

save() {
  this.crisis.name = this.editName;
  this.gotoCrises();
}

What if the user tries to navigate away without saving or canceling? The user could push the browser back button or click the heroes link. Both actions trigger a navigation. Should the app save or cancel automatically?
This demo does neither. Instead, it asks the user to make that choice explicitly in a confirmation dialog box that waits asynchronously for the user's answer.
You could wait for the user's answer with synchronous, blocking code. The app will be more responsive—and can do other work—by waiting for the user's answer asynchronously. Waiting for the user asynchronously is like waiting for the server asynchronously.
The DialogService, provided in the AppModule for app-wide use, does the asking.
It returns an Observable that resolves when the user eventually decides what to do: either to discard changes and navigate away (true) or to preserve the pending changes and stay in the crisis editor (false).

Create a guard that checks for the presence of a canDeactivate() method in a component—any component. The CrisisDetailComponent will have this method. But the guard doesn't have to know that. The guard shouldn't know the details of any component's deactivation method. It need only detect that the component has a canDeactivate() method and call it. This approach makes the guard reusable.

src/app/can-deactivate-guard.service.ts
  1. import { Injectable } from '@angular/core';
  2. import { CanDeactivate } from '@angular/router';
  3. import { Observable } from 'rxjs';
  4.  
  5. export interface CanComponentDeactivate {
  6. canDeactivate: () => Observable | Promise | boolean;
  7. }
  8.  
  9. @Injectable()
  10. export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  11. canDeactivate(component: CanComponentDeactivate) {
  12. return component.canDeactivate ? component.canDeactivate() : true;
  13. }
  14. }

Alternatively, you could make a component-specific CanDeactivate guard for the CrisisDetailComponent. The canDeactivate() method provides you with the current instance of the component, the current ActivatedRoute, and RouterStateSnapshot in case you needed to access some external information. This would be useful if you only wanted to use this guard for this component and needed to get the component's properties or confirm whether the router should allow navigation away from it.

src/app/can-deactivate-guard.service.ts (component-specific)
import { Injectable }           from '@angular/core';
import { Observable }           from 'rxjs';
import { CanDeactivate,
         ActivatedRouteSnapshot,
         RouterStateSnapshot }  from '@angular/router';

import { CrisisDetailComponent } from './crisis-center/crisis-detail.component';

@Injectable()
export class CanDeactivateGuard implements CanDeactivate<CrisisDetailComponent> {

  canDeactivate(
    component: CrisisDetailComponent,
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable | boolean {
    // Get the Crisis Center ID
    console.log(route.paramMap.get('id'));

    // Get the current URL
    console.log(state.url);

    // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
    if (!component.crisis || component.crisis.name === component.editName) {
      return true;
    }
    // Otherwise ask the user with the dialog service and return its
    // observable which resolves to true or false when the user decides
    return component.dialogService.confirm('Discard changes?');
  }
}

Looking back at the CrisisDetailComponent, it implements the confirmation workflow for unsaved changes.

src/app/crisis-center/crisis-detail.component.ts (excerpt)
canDeactivate(): Observable | boolean {
  // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
  if (!this.crisis || this.crisis.name === this.editName) {
    return true;
  }
  // Otherwise ask the user with the dialog service and return its
  // observable which resolves to true or false when the user decides
  return this.dialogService.confirm('Discard changes?');
}

Notice that the canDeactivate() method can return synchronously; it returns true immediately if there is no crisis or there are no pending changes. But it can also return a Promise or an Observable and the router will wait for that to resolve to truthy (navigate) or falsy (stay put).

Add the Guard to the crisis detail route in crisis-center-routing.module.ts using the canDeactivate array property.

src/app/crisis-center/crisis-center-routing.module.ts (can deactivate guard)
import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { CrisisCenterHomeComponent } from './crisis-center-home.component';
import { CrisisListComponent }       from './crisis-list.component';
import { CrisisCenterComponent }     from './crisis-center.component';
import { CrisisDetailComponent }     from './crisis-detail.component';

import { CanDeactivateGuard }    from '../can-deactivate-guard.service';

const crisisCenterRoutes: Routes = [
  {
    path: '',
    redirectTo: '/crisis-center',
    pathMatch: 'full'
  },
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent,
            canDeactivate: [CanDeactivateGuard]
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

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

Add the Guard to the main AppRoutingModule providers array so the Router can inject it during the navigation process.

  1. import { NgModule } from '@angular/core';
  2. import { RouterModule, Routes } from '@angular/router';
  3.  
  4. import { ComposeMessageComponent } from './compose-message.component';
  5. import { CanDeactivateGuard } from './can-deactivate-guard.service';
  6. import { PageNotFoundComponent } from './not-found.component';
  7.  
  8. const appRoutes: Routes = [
  9. {
  10. path: 'compose',
  11. component: ComposeMessageComponent,
  12. outlet: 'popup'
  13. },
  14. { path: '', redirectTo: '/heroes', pathMatch: 'full' },
  15. { path: '**', component: PageNotFoundComponent }
  16. ];
  17.  
  18. @NgModule({
  19. imports: [
  20. RouterModule.forRoot(
  21. appRoutes,
  22. { enableTracing: true } // <-- debugging="" only="" purposes="" span="">
  23. )
  24. ],
  25. exports: [
  26. RouterModule
  27. ],
  28. providers: [
  29. CanDeactivateGuard
  30. ]
  31. })
  32. export class AppRoutingModule {}

Now you have given the user a safeguard against unsaved changes.

Resolve: pre-fetching component data

In the Hero Detail and Crisis Detail, the app waited until the route was activated to fetch the respective hero or crisis.
This worked well, but there's a better way. If you were using a real world API, there might be some delay before the data to display is returned from the server. You don't want to display a blank component while waiting for the data.
It's preferable to pre-fetch data from the server so it's ready the moment the route is activated. This also allows you to handle errors before routing to the component. There's no point in navigating to a crisis detail for an id that doesn't have a record. It'd be better to send the user back to the Crisis List that shows only valid crisis centers.
In summary, you want to delay rendering the routed component until all necessary data have been fetched.
You need a resolver.

Fetch data before navigating

At the moment, the CrisisDetailComponent retrieves the selected crisis. If the crisis is not found, it navigates back to the crisis list view.
The experience might be better if all of this were handled first, before the route is activated. A CrisisDetailResolver service could retrieve a Crisis or navigate away if the Crisis does not exist beforeactivating the route and creating the CrisisDetailComponent.
Create the crisis-detail-resolver.service.ts file within the Crisis Center feature area.

src/app/crisis-center/crisis-detail-resolver.service.ts
  1. import { Injectable } from '@angular/core';
  2. import { Router, Resolve, RouterStateSnapshot,
  3. ActivatedRouteSnapshot } from '@angular/router';
  4. import { Observable } from 'rxjs';
  5. import { map, take } from 'rxjs/operators';
  6.  
  7. import { Crisis, CrisisService } from './crisis.service';
  8.  
  9. @Injectable()
  10. export class CrisisDetailResolver implements Resolve<Crisis> {
  11. constructor(private cs: CrisisService, private router: Router) {}
  12.  
  13. resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Crisis> {
  14. let id = route.paramMap.get('id');
  15.  
  16. return this.cs.getCrisis(id).pipe(
  17. take(1),
  18. map(crisis => {
  19. if (crisis) {
  20. return crisis;
  21. } else { // id not found
  22. this.router.navigate(['/crisis-center']);
  23. return null;
  24. }
  25. })
  26. );
  27. }
  28. }

Take the relevant parts of the crisis retrieval logic in CrisisDetailComponent.ngOnInit and move them into the CrisisDetailResolver. Import the Crisis model, CrisisService, and the Router so you can navigate elsewhere if you can't fetch the crisis.
Be explicit. Implement the Resolve interface with a type of Crisis.


Inject the CrisisService and Router and implement the resolve() method. That method could return a Promise, an Observable, or a synchronous return value.
The CrisisService.getCrisis method returns an observable, in order to prevent the route from loading until the data is fetched. The Router guards require an observable to complete, meaning it has emitted all of its values. You use the take operator with an argument of 1 to ensure that the observable completes after retrieving the first value from the observable returned by the getCrisis method. If it doesn't return a valid Crisis, navigate the user back to the CrisisListComponent, canceling the previous in-flight navigation to the CrisisDetailComponent.
Import this resolver in the crisis-center-routing.module.ts and add a resolve object to the CrisisDetailComponent route configuration.
Remember to add the CrisisDetailResolver service to the CrisisCenterRoutingModule's providers array.

src/app/crisis-center/crisis-center-routing.module.ts (resolver)
import { CrisisDetailResolver }   from './crisis-detail-resolver.service';

@NgModule({
  imports: [
    RouterModule.forChild(crisisCenterRoutes)
  ],
  exports: [
    RouterModule
  ],
  providers: [
    CrisisDetailResolver
  ]
})
export class CrisisCenterRoutingModule { }

The CrisisDetailComponent should no longer fetch the crisis. Update the CrisisDetailComponent to get the crisis from the ActivatedRoute.data.crisis property instead; that's where you said it should be when you re-configured the route. It will be there when the CrisisDetailComponent ask for it.

src/app/crisis-center/crisis-detail.component.ts (ngOnInit v2)
ngOnInit() {
  this.route.data
    .subscribe((data: { crisis: Crisis }) => {
      this.editName = data.crisis.name;
      this.crisis = data.crisis;
    });
}

Three critical points
  1. The router's Resolve interface is optional. The CrisisDetailResolver doesn't inherit from a base class. The router looks for that method and calls it if found.
  2. Rely on the router to call the resolver. Don't worry about all the ways that the user could navigate away. That's the router's job. Write this class and let the router take it from there.
  3. The observable provided to the Router must complete. If the observable does not complete, the navigation will not continue.

Query parameters and fragments

In the route parameters example, you only dealt with parameters specific to the route, but what if you wanted optional parameters available to all routes? This is where query parameters come into play.
Fragments refer to certain elements on the page identified with an id attribute.
Update the AuthGuard to provide a session_id query that will remain after navigating to another route.
Add an anchor element so you can jump to a certain point on the page.
Add the NavigationExtras object to the router.navigate method that navigates you to the /login route.

src/app/auth-guard.service.ts (v3)
import { Injectable }       from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  CanActivateChild,
  NavigationExtras
}                           from '@angular/router';
import { AuthService }      from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    let url: string = state.url;

    return this.checkLogin(url);
  }

  canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.canActivate(route, state);
  }

  checkLogin(url: string): boolean {
    if (this.authService.isLoggedIn) { return true; }

    // Store the attempted URL for redirecting
    this.authService.redirectUrl = url;

    // Create a dummy session id
    let sessionId = 123456789;

    // Set our navigation extras object
    // that contains our global query params and fragment
    let navigationExtras: NavigationExtras = {
      queryParams: { 'session_id': sessionId },
      fragment: 'anchor'
    };

    // Navigate to the login page with extras
    this.router.navigate(['/login'], navigationExtras);
    return false;
  }
}

You can also preserve query parameters and fragments across navigations without having to provide them again when navigating. In the LoginComponent, you'll add an object as the second argument in the router.navigate function and provide the queryParamsHandling and preserveFragment to pass along the current query parameters and fragment to the next route.

src/app/login.component.ts (preserve)
// Set our navigation extras object
// that passes on our global query params and fragment
let navigationExtras: NavigationExtras = {
  queryParamsHandling: 'preserve',
  preserveFragment: true
};

// Redirect the user
this.router.navigate([redirect], navigationExtras);

The queryParamsHandling feature also provides a merge option, which will preserve and combine the current query parameters with any provided query parameters when navigating.
Since you'll be navigating to the Admin Dashboard route after logging in, you'll update it to handle the query parameters and fragment.

src/app/admin/admin-dashboard.component.ts (v2)
import { Component, OnInit }  from '@angular/core';
import { ActivatedRoute }     from '@angular/router';
import { Observable }         from 'rxjs';
import { map }                from 'rxjs/operators';

@Component({
  template:  `
    Dashboard



    Session ID: {{ sessionId | async }}


    <a id="anchor"></a>
    Token: {{ token | async }}


  `
})
export class AdminDashboardComponent implements OnInit {
  sessionId: Observable;
  token: Observable;

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    // Capture the session ID if available
    this.sessionId = this.route
      .queryParamMap
      .pipe(map(params => params.get('session_id') || 'None'));

    // Capture the fragment if available
    this.token = this.route
      .fragment
      .pipe(map(fragment => fragment || 'None'));
  }
}

Query parameters and fragments are also available through the ActivatedRoute service. Just like route parameters, the query parameters and fragments are provided as an Observable. The updated Crisis Admincomponent feeds the Observable directly into the template using the AsyncPipe.
Now, you can click on the Admin button, which takes you to the Login page with the provided queryParamMapand fragment. After you click the login button, notice that you have been redirected to the Admin Dashboardpage with the query parameters and fragment still intact in the address bar.
You can use these persistent bits of information for things that need to be provided across pages like authentication tokens or session ids.
The query params and fragment can also be preserved using a RouterLink with the queryParamsHandling and preserveFragment bindings respectively.



Comments