Skip to main content

Angular - Routing and Navigation Part 7

Milestone 6: Asynchronous routing

As you've worked through the milestones, the application has naturally gotten larger. As you continue to build out feature areas, the overall application size will continue to grow. At some point you'll reach a tipping point where the application takes long time to load.
How do you combat this problem? With asynchronous routing, which loads feature modules lazily, on request. Lazy loading has multiple benefits.
  • You can load feature areas only when requested by the user.
  • You can speed up load time for users that only visit certain areas of the application.
  • You can continue expanding lazy loaded feature areas without increasing the size of the initial load bundle.
You're already made part way there. By organizing the application into modules—AppModuleHeroesModuleAdminModule and CrisisCenterModule—you have natural candidates for lazy loading.
Some modules, like AppModule, must be loaded from the start. But others can and should be lazy loaded. The AdminModule, for example, is needed by a few authorized users, so you should only load it when requested by the right people.

Lazy Loading route configuration

Change the admin path in the admin-routing.module.ts from 'admin' to an empty string, '', the empty path.
The Router supports empty path routes; use them to group routes together without adding any additional path segments to the URL. Users will still visit /admin and the AdminComponent still serves as the Routing Component containing child routes.
Open the AppRoutingModule and add a new admin route to its appRoutes array.
Give it a loadChildren property (not a children property!), set to the address of the AdminModule. The address is the AdminModule file location (relative to the app root), followed by a # separator, followed by the name of the exported module class, AdminModule.
app-routing.module.ts (load children)
{
  path: 'admin',
  loadChildren: 'app/admin/admin.module#AdminModule',
},

When the router navigates to this route, it uses the loadChildren string to dynamically load the AdminModule. Then it adds the AdminModule routes to its current route configuration. Finally, it loads the requested route to the destination admin component.
The lazy loading and re-configuration happen just once, when the route is first requested; the module and routes are available immediately for subsequent requests.
Angular provides a built-in module loader that supports SystemJS to load modules asynchronously. If you were using another bundling tool, such as Webpack, you would use the Webpack mechanism for asynchronously loading modules.
Take the final step and detach the admin feature set from the main application. The root AppModule must neither load nor reference the AdminModule or its files.
In app.module.ts, remove the AdminModule import statement from the top of the file and remove the AdminModule from the NgModule's imports array.

CanLoad Guard: guarding unauthorized loading of feature modules

You're already protecting the AdminModule with a CanActivate guard that prevents unauthorized users from accessing the admin feature area. It redirects to the login page if the user is not authorized.
But the router is still loading the AdminModule even if the user can't visit any of its components. Ideally, you'd only load the AdminModule if the user is logged in.
Add a CanLoad guard that only loads the AdminModule once the user is logged in and attempts to access the admin feature area.
The existing AuthGuard already has the essential logic in its checkLogin() method to support the CanLoadguard.
Open auth-guard.service.ts. Import the CanLoad interface from @angular/router. Add it to the AuthGuardclass's implements list. Then implement canLoad() as follows:
src/app/auth-guard.service.ts (CanLoad guard)
canLoad(route: Route): boolean {
  let url = `/${route.path}`;

  return this.checkLogin(url);
}

The router sets the canLoad() method's route parameter to the intended destination URL. The checkLogin()method redirects to that URL once the user has logged in.
Now import the AuthGuard into the AppRoutingModule and add the AuthGuard to the canLoad array property for the admin route. The completed admin route looks like this:
app-routing.module.ts (lazy admin route)
{
  path: 'admin',
  loadChildren: 'app/admin/admin.module#AdminModule',
  canLoad: [AuthGuard]
},

Preloading: background loading of feature areas

You've learned how to load modules on-demand. You can also load modules asynchronously with preloading.
This may seem like what the app has been doing all along. Not quite. The AppModule is loaded when the application starts; that's eager loading. Now the AdminModule loads only when the user clicks on a link; that's lazy loading.
Preloading is something in between. Consider the Crisis Center. It isn't the first view that a user sees. By default, the Heroes are the first view. For the smallest initial payload and fastest launch time, you should eagerly load the AppModule and the HeroesModule.
You could lazy load the Crisis Center. But you're almost certain that the user will visit the Crisis Centerwithin minutes of launching the app. Ideally, the app would launch with just the AppModule and the HeroesModule loaded and then, almost immediately, load the CrisisCenterModule in the background. By the time the user navigates to the Crisis Center, its module will have been loaded and ready to go.
That's preloading.

How preloading works

After each successful navigation, the router looks in its configuration for an unloaded module that it can preload. Whether it preloads a module, and which modules it preloads, depends upon the preload strategy.
The Router offers two preloading strategies out of the box:
  • No preloading at all which is the default. Lazy loaded feature areas are still loaded on demand.
  • Preloading of all lazy loaded feature areas.
Out of the box, the router either never preloads, or preloads every lazy load module. The Router also supports custom preloading strategies for fine control over which modules to preload and when.
In this next section, you'll update the CrisisCenterModule to load lazily by default and use the PreloadAllModules strategy to load it (and all other lazy loaded modules) as soon as possible.

Lazy load the crisis center

Update the route configuration to lazy load the CrisisCenterModule. Take the same steps you used to configure AdminModule for lazy load.
  1. Change the crisis-center path in the CrisisCenterRoutingModule to an empty string.
  2. Add a crisis-center route to the AppRoutingModule.
  3. Set the loadChildren string to load the CrisisCenterModule.
  4. Remove all mention of the CrisisCenterModule from app.module.ts.
Here are the updated modules before enabling preload:

  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. }
You could try this now and confirm that the CrisisCenterModule loads after you click the "Crisis Center" button.
To enable preloading of all lazy loaded modules, import the PreloadAllModules token from the Angular router package.
The second argument in the RouterModule.forRoot method takes an object for additional configuration options. The preloadingStrategy is one of those options. Add the PreloadAllModules token to the forRootcall:
src/app/app-routing.module.ts (preload all)
RouterModule.forRoot(
  appRoutes,
  {
    enableTracing: true, // <-- debugging="" only="" purposes="" span="">
    preloadingStrategy: PreloadAllModules
  }
)

This tells the Router preloader to immediately load all lazy loaded routes (routes with a loadChildrenproperty).
When you visit http://localhost:3000, the /heroes route loads immediately upon launch and the router starts loading the CrisisCenterModule right after the HeroesModule loads.
Surprisingly, the AdminModule does not preload. Something is blocking it.

CanLoad blocks preload

The PreloadAllModules strategy does not load feature areas protected by a CanLoad guard. This is by design.
You added a CanLoad guard to the route in the AdminModule a few steps back to block loading of that module until the user is authorized. That CanLoad guard takes precedence over the preload strategy.
If you want to preload a module and guard against unauthorized access, drop the canLoad() guard method and rely on the canActivate() guard alone.

Custom Preloading Strategy

Preloading every lazy loaded modules works well in many situations, but it isn't always the right choice, especially on mobile devices and over low bandwidth connections. You may choose to preload only certain feature modules, based on user metrics and other business and technical factors.
You can control what and how the router preloads with a custom preloading strategy.
In this section, you'll add a custom strategy that only preloads routes whose data.preload flag is set to true. Recall that you can add anything to the data property of a route.
Set the data.preload flag in the crisis-center route in the AppRoutingModule.
src/app/app-routing.module.ts (route data preload)
{
  path: 'crisis-center',
  loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule',
  data: { preload: true }
},

Add a new file to the project called selective-preloading-strategy.ts and define a SelectivePreloadingStrategy service class as follows:
src/app/selective-preloading-strategy.ts (excerpt)
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable()
export class SelectivePreloadingStrategy implements PreloadingStrategy {
  preloadedModules: string[] = [];

  preload(route: Route, load: () => Observable): Observable {
    if (route.data && route.data['preload']) {
      // add the route path to the preloaded module array
      this.preloadedModules.push(route.path);

      // log the route path to the console
      console.log('Preloaded: ' + route.path);

      return load();
    } else {
      return of(null);
    }
  }
}

SelectivePreloadingStrategy implements the PreloadingStrategy, which has one method, preload.
The router calls the preload method with two arguments:
  1. The route to consider.
  2. A loader function that can load the routed module asynchronously.
An implementation of preload must return an Observable. If the route should preload, it returns the observable returned by calling the loader function. If the route should not preload, it returns an Observableof null.
In this sample, the preload method loads the route if the route's data.preload flag is truthy.
It also has a side-effect. SelectivePreloadingStrategy logs the path of a selected route in its public preloadedModules array.
Shortly, you'll extend the AdminDashboardComponent to inject this service and display its preloadedModulesarray.
But first, make a few changes to the AppRoutingModule.
  1. Import SelectivePreloadingStrategy into AppRoutingModule.
  2. Replace the PreloadAllModules strategy in the call to forRoot with this SelectivePreloadingStrategy.
  3. Add the SelectivePreloadingStrategy strategy to the AppRoutingModule providers array so it can be injected elsewhere in the app.
Now edit the AdminDashboardComponent to display the log of preloaded routes.
  1. Import the SelectivePreloadingStrategy (it's a service).
  2. Inject it into the dashboard's constructor.
  3. Update the template to display the strategy service's preloadedModules array.
When you're done it looks like this.
src/app/admin/admin-dashboard.component.ts (preloaded modules)
import { Component, OnInit }    from '@angular/core';
import { ActivatedRoute }       from '@angular/router';
import { Observable }           from 'rxjs';
import { map }                  from 'rxjs/operators';

import { SelectivePreloadingStrategy } from '../selective-preloading-strategy';


@Component({
  template:  `
    Dashboard
Session ID: {{ sessionId | async }}
<
a id="anchor"></a> Token: {{ token | async }}
Preloaded Modules
  • ngFor="let module of modules">{{ module }}
`
}) export class AdminDashboardComponent implements OnInit { sessionId: Observable; token: Observable; modules: string[]; constructor( private route: ActivatedRoute, private preloadStrategy: SelectivePreloadingStrategy ) { this.modules = preloadStrategy.preloadedModules; } 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')); } }

Once the application loads the initial route, the CrisisCenterModule is preloaded. Verify this by logging in to the Admin feature area and noting that the crisis-center is listed in the Preloaded Modules. It's also logged to the browser's console.

Migrating URLs with Redirects

You've setup the routes for navigating around your application. You've used navigation imperatively and declaratively to many different routes. But like any application, requirements change over time. You've setup links and navigation to /heroes and /hero/:id from the HeroListComponent and HeroDetailComponentcomponents. If there was a requirement that links to heroes become superheroes, you still want the previous URLs to navigate correctly. You also don't want to go and update every link in your application, so redirects makes refactoring routes trivial.

Changing /heroes to /superheroes

Let's take the Hero routes and migrate them to new URLs. The Router checks for redirects in your configuration before navigating, so each redirect is triggered when needed. To support this change, you'll add redirects from the old routes to the new routes in the heroes-routing.module.
src/app/heroes/heroes-routing.module.ts (heroes redirects)
import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

const heroesRoutes: Routes = [
  { path: 'heroes', redirectTo: '/superheroes' },
  { path: 'hero/:id', redirectTo: '/superhero/:id' },
  { path: 'superheroes',  component: HeroListComponent },
  { path: 'superhero/:id', component: HeroDetailComponent }
];

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

You'll notice two different types of redirects. The first change is from /heroes to /superheroes without any parameters. This is a straightforward redirect, unlike the change from /hero/:id to /superhero/:id, which includes the :id route parameter. Router redirects also use powerful pattern matching, so the Routerinspects the URL and replaces route parameters in the path with their appropriate destination. Previously, you navigated to a URL such as /hero/15 with a route parameter id of 15.
The Router also supports query parameters and the fragment when using redirects.
  • When using absolute redirects, the Router will use the query parameters and the fragment from the redirectTo in the route config.
  • When using relative redirects, the Router use the query params and the fragment from the source URL.
Before updating the app-routing.module.ts, you'll need to consider an important rule. Currently, our empty path route redirects to /heroes, which redirects to /superheroes. This won't work and is by design as the Router handles redirects once at each level of routing configuration. This prevents chaining of redirects, which can lead to endless redirect loops.
So instead, you'll update the empty path route in app-routing.module.ts to redirect to /superheroes.
src/app/app-routing.module.ts (superheroes redirect)
import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { ComposeMessageComponent }  from './compose-message.component';
import { PageNotFoundComponent }    from './not-found.component';

import { CanDeactivateGuard }       from './can-deactivate-guard.service';
import { AuthGuard }                from './auth-guard.service';
import { SelectivePreloadingStrategy } from './selective-preloading-strategy';

const appRoutes: Routes = [
  {
    path: 'compose',
    component: ComposeMessageComponent,
    outlet: 'popup'
  },
  {
    path: 'admin',
    loadChildren: 'app/admin/admin.module#AdminModule',
    canLoad: [AuthGuard]
  },
  {
    path: 'crisis-center',
    loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule',
    data: { preload: true }
  },
  { path: '',   redirectTo: '/superheroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      {
        enableTracing: true, // <-- debugging="" only="" purposes="" span="">
        preloadingStrategy: SelectivePreloadingStrategy,

      }
    )
  ],
  exports: [
    RouterModule
  ],
  providers: [
    CanDeactivateGuard,
    SelectivePreloadingStrategy
  ]
})
export class AppRoutingModule { }

Since RouterLinks aren't tied to route configuration, you'll need to update the associated router links so they remain active when the new route is active. You'll update the app.component.ts template for the /heroes routerLink.
src/app/app.component.ts (superheroes active routerLink)
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    

Angular

Router
a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/superheroes" routerLinkActive="active">Heroes</a> <a routerLink="/admin" routerLinkActive="active">Admin</a> <a routerLink="/login" routerLinkActive="active">Login</a> <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a> <router-outlet></router-outlet> <router-outlet name="popup"></router-outlet> ` }) export class AppComponent { }
With the redirects setup, all previous routes now point to their new destinations and both URLs still function as intended.

Inspect the router's configuration

You put a lot of effort into configuring the router in several routing module files and were careful to list them in the proper order. Are routes actually evaluated as you planned? How is the router really configured?
You can inspect the router's current configuration any time by injecting it and examining its config property. For example, update the AppModule as follows and look in the browser console window to see the finished route configuration.
src/app/app.module.ts (inspect the router config)
import { Router } from '@angular/router';

export class AppModule {
  // Diagnostic only: inspect router configuration
  constructor(router: Router) {
    console.log('Routes: ', JSON.stringify(router.config, undefined, 2));
  }
}

Wrap up and final app

You've covered a lot of ground in this guide and the application is too big to reprint here. Please visit the Router Sample in Stackblitz / download example where you can download the final source code.

Appendices

The balance of this guide is a set of appendices that elaborate some of the points you covered quickly above.
The appendix material isn't essential. Continued reading is for the curious.
A link parameters array holds the following ingredients for router navigation:
  • The path of the route to the destination component.
  • Required and optional route parameters that go into the route URL.
You can bind the RouterLink directive to such an array like this:
src/app/app.component.ts (h-anchor)
<a [routerLink]="['/heroes']">Heroes</a>

You've written a two element array when specifying a route parameter like this:
src/app/heroes/hero-list.component.ts (nav-to-detail)
<a [routerLink]="['/hero', hero.id]">
   class="badge">{{ hero.id }}{{ hero.name }}
</a>

You can provide optional route parameters in an object like this:
src/app/app.component.ts (cc-query-params)
<a [routerLink]="['/crisis-center', { foo: 'foo' }]">Crisis Center</a>

These three examples cover the need for an app with one level routing. The moment you add a child router, such as the crisis center, you create new link array possibilities.
Recall that you specified a default child route for the crisis center so this simple RouterLink is fine.
src/app/app.component.ts (cc-anchor-w-default)
<a [routerLink]="['/crisis-center']">Crisis Center</a>

Parse it out.
  • The first item in the array identifies the parent route (/crisis-center).
  • There are no parameters for this parent route so you're done with it.
  • There is no default for the child route so you need to pick one.
  • You're navigating to the CrisisListComponent, whose route path is /, but you don't need to explicitly add the slash.
  • Voilà! ['/crisis-center'].
Take it a step further. Consider the following router link that navigates from the root of the application down to the Dragon Crisis:
src/app/app.component.ts (Dragon-anchor)
<a [routerLink]="['/crisis-center', 1]">Dragon Crisis</a>

  • The first item in the array identifies the parent route (/crisis-center).
  • There are no parameters for this parent route so you're done with it.
  • The second item identifies the child route details about a particular crisis (/:id).
  • The details child route requires an id route parameter.
  • You added the id of the Dragon Crisis as the second item in the array (1).
  • The resulting path is /crisis-center/1.
If you wanted to, you could redefine the AppComponent template with Crisis Center routes exclusively:
src/app/app.component.ts (template)
template: `
  

Angular

Router
a [routerLink]="['/crisis-center']">Crisis Center</a> <a [routerLink]="['/crisis-center/1', { foo: 'foo' }]">Dragon Crisis</a> <a [routerLink]="['/crisis-center/2']">Shark Crisis</a> <router-outlet></router-outlet> `
In sum, you can write applications with one, two or more levels of routing. The link parameters array affords the flexibility to represent any routing depth and any legal sequence of route paths, (required) router parameters, and (optional) route parameter objects.

Appendix: LocationStrategy and browser URL styles

When the router navigates to a new component view, it updates the browser's location and history with a URL for that view. This is a strictly local URL. The browser shouldn't send this URL to the server and should not reload the page.
Modern HTML5 browsers support history.pushState, a technique that changes a browser's location and history without triggering a server page request. The router can compose a "natural" URL that is indistinguishable from one that would otherwise require a page load.
Here's the Crisis Center URL in this "HTML5 pushState" style:
localhost:3002/crisis-center/

Older browsers send page requests to the server when the location URL changes unless the change occurs after a "#" (called the "hash"). Routers can take advantage of this exception by composing in-application route URLs with hashes. Here's a "hash URL" that routes to the Crisis Center.
localhost:3002/src/#/crisis-center/

The router supports both styles with two LocationStrategy providers:
  1. PathLocationStrategy—the default "HTML5 pushState" style.
  2. HashLocationStrategy—the "hash URL" style.
The RouterModule.forRoot function sets the LocationStrategy to the PathLocationStrategy, making it the default strategy. You can switch to the HashLocationStrategy with an override during the bootstrapping process if you prefer it.
Learn about providers and the bootstrap process in the Dependency Injection guide.

Which strategy is best?

You must choose a strategy and you need to make the right call early in the project. It won't be easy to change later once the application is in production and there are lots of application URL references in the wild.
Almost all Angular projects should use the default HTML5 style. It produces URLs that are easier for users to understand. And it preserves the option to do server-side rendering later.
Rendering critical pages on the server is a technique that can greatly improve perceived responsiveness when the app first loads. An app that would otherwise take ten or more seconds to start could be rendered on the server and delivered to the user's device in less than a second.
This option is only available if application URLs look like normal web URLs without hashes (#) in the middle.
Stick with the default unless you have a compelling reason to resort to hash routes.

HTML5 URLs and the 

While the router uses the HTML5 pushState style by default, you must configure that strategy with a base href.
The preferred way to configure the strategy is to add a  element tag in the  of the index.html.
src/index.html (base-href)
 href="/">

Without that tag, the browser may not be able to load resources (images, CSS, scripts) when "deep linking" into the app. Bad things could happen when someone pastes an application link into the browser's address bar or clicks such a link in an email.
Some developers may not be able to add the  element, perhaps because they don't have access to  or the index.html.
Those developers may still use HTML5 URLs by taking two remedial steps:
  1. Provide the router with an appropriate APP_BASE_HREF value.
  2. Use root URLs for all web resources: CSS, images, scripts, and template HTML files.

HashLocationStrategy

You can go old-school with the HashLocationStrategy by providing the useHash: true in an object as the second argument of the RouterModule.forRoot in the AppModule.
src/app/app.module.ts (hash URL strategy)

import { NgModule }             from '@angular/core';
import { BrowserModule }        from '@angular/platform-browser';
import { FormsModule }          from '@angular/forms';
import { Routes, RouterModule } from '@angular/router';

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

const routes: Routes = [

];

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    RouterModule.forRoot(routes, { useHash: true })  // .../#/crisis-center/
  ],
  declarations: [
    AppComponent,
    PageNotFoundComponent
  ],
  providers: [

  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Comments