Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Andy Weber and Gunnar Lieb and Thilo Hermann
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Andy Weber , Gunnar Lieb@akitogo ) , and Thilo Hermann ( @thfusion_de )

Showing A Loading Indicator For Lazy-Loaded Route Modules In Angular 6.1.7

By Ben Nadel on

Now that I've been able to get the lazy loading of routes to work in Angular 6.1.7 with Ahead of Time (AoT) compiling and Webpack 4, I wanted to think about how to steward the user through a lazy-loaded application. Especially when that user is on a slower network connection and the lazy-loading of routes actually creates a noticeable delay in the application's responsiveness. In order to create a more pleasing user experience (UX), I'd like to be able to show a "loading indicator" while the routes are being loaded asynchronously; but, only show the loading indicator in cases where the navigation actually takes several seconds to load the target module.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

When the user navigates around an Angular application, the Router emits a series of events as the target route is loaded, validated, and resolved. With relation to the lazy loading of routes, the Router emits two events that are perfect for our "loading indicator" use case:

  • RouteConfigLoadStart
  • RouteConfigLoadEnd

The RouteConfigLoadStart event is emitted when a lazy-loaded route is first encountered and Angular makes an asynchronous request for the module code. The RouteConfigLoadEnd event is emitted when the lazy-loaded module code has been retrieved and the new configuration has been merged into the application's active route configuration.

If we simply increment and decrement a counter when the RouteConfigLoadStart and RouteConfigLoadEnd events are emitted, respectively, we can easily determine if the application is currently making a request for a lazy-loaded module. To see this in action, I've taken my previous lazy-loading demo, injected the Router into the App component, and then subscribed to the two config-related router events:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  • import { Event as RouterEvent } from "@angular/router";
  • import { Router } from "@angular/router";
  • import { RouteConfigLoadEnd } from "@angular/router";
  • import { RouteConfigLoadStart } from "@angular/router";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app-view.component.less" ],
  • templateUrl: "./app-view.component.htm"
  • })
  • export class AppViewComponent {
  •  
  • public isShowingRouteLoadIndicator: boolean;
  •  
  • // I initialize the app view component.
  • constructor( router: Router ) {
  •  
  • this.isShowingRouteLoadIndicator = false;
  •  
  • // As the router loads modules asynchronously (via loadChildren), we're going to
  • // keep track of how many asynchronous requests are currently active. If there is
  • // at least one pending load request, we'll show the indicator.
  • var asyncLoadCount = 0;
  •  
  • // The Router emits special events for "loadChildren" configuration loading. We
  • // just need to listen for the Start and End events in order to determine if we
  • // have any pending configuration requests.
  • router.events.subscribe(
  • ( event: RouterEvent ) : void => {
  •  
  • if ( event instanceof RouteConfigLoadStart ) {
  •  
  • asyncLoadCount++;
  •  
  • } else if ( event instanceof RouteConfigLoadEnd ) {
  •  
  • asyncLoadCount--;
  •  
  • }
  •  
  • // If there is at least one pending asynchronous config load request,
  • // then let's show the loading indicator.
  • // --
  • // CAUTION: I'm using CSS to include a small delay such that this loading
  • // indicator won't be seen by people with sufficiently fast connections.
  • this.isShowingRouteLoadIndicator = !! asyncLoadCount;
  •  
  • }
  • );
  •  
  • }
  •  
  • }

As you can see, every time the RouteConfigLoadStart event is emitted, we increment a counter. And, every time the RouteConfigLoadEnd event is emitted, we decrement the same counter. Then, by using the count of pending module request, we can define a public property that determines whether or not to show the lazy-loading indicator:

  • App View
  •  
  • <p>
  • <a routerLink="/app/">Home</a> &mdash;
  • <a routerLink="/app/feature-a">Feature A</a> &mdash;
  • <a routerLink="/app/feature-b">Feature B</a> &mdash;
  • <a routerLink="/app/feature-c">Feature C</a> &mdash;
  • <a [routerLink]="[ '/app', { outlets: { aside: 'aside' } } ]">Aside</a>
  • </p>
  •  
  • <router-outlet></router-outlet>
  •  
  • <router-outlet name="aside"></router-outlet>
  •  
  • <!-- I indicate that a router module is being loaded asynchronously. -->
  • <div
  • *ngIf="isShowingRouteLoadIndicator"
  • class="router-load-indicator">
  • Loading Module
  • </div>

So far, so good; but, on a fast network connection where it takes maybe 35ms to load the async route, I don't want the loading indicator to flash on the screen. Such an interaction could be quite distracting to the user. As such, I only want to show the loading indicator if the lazy-loaded module takes longer than a given threshold to load.

I could have managed this delay with a Timer. And, I'm sure some RxJS aficionado could figure out how to do it with a .delay() stream. But, for the simplicity of the demo, I decided to put the delay logic in the CSS. By using a CSS animation, I can fade the loading indicator into view; but, only after a delay that will be longer than the latency of most fast network connections.

NOTE: To be clear I am not dismissing Timers or RxJS as a valid solution. I am just trying to keep the demo as simple as possible.

  • :host {
  • display: block ;
  • }
  •  
  • a {
  • color: red ;
  • cursor: pointer ;
  • text-decoration: underline ;
  • }
  •  
  • // On a fast network connection, the lazy-loaded modules will load almost instantly. In
  • // such a case, we don't want to bother showing the asynchronous loading indicator as it
  • // will simply flash the UI and create a distracting user experience. As such, we want to
  • // put a small delay on the "observability" of the loading indicator. This way, on a fast
  • // connection, it will be removed before the delay is consumed; and, on a slower network
  • // connection, it will be shown to the user soon after the asynchronous load starts.
  • // --
  • // CAUTION: keyframes are not "protected" by Angular's simulated encapsulation. As such,
  • // this animation name needs to be universally unique to the application.
  • @keyframes router-load-indicator-animation {
  • from {
  • opacity: 0.0 ;
  • }
  •  
  • to {
  • opacity: 1.0 ;
  • }
  • }
  •  
  • .router-load-indicator {
  • // Delay the animation for a fast network connection (so users don't see the loader).
  • animation-delay: 100ms ;
  • animation-duration: 200ms ;
  • animation-fill-mode: both ;
  • animation-name: router-load-indicator-animation ;
  • // --
  • background-color: #ffdc73 ;
  • border-radius: 5px 5px 5px 5px ;
  • box-shadow: 0px 2px 2px fade( #000000, 20% ) ;
  • color: #000000 ;
  • font-family: monospace ;
  • font-size: 16px ;
  • left: 50% ;
  • padding: 7px 15px 7px 15px ;
  • position: fixed ;
  • text-transform: lowercase ;
  • top: 10px ;
  • transform: translateX( -50% ) ;
  • z-index: 2 ;
  • }

As you can see, the loading indicator for the lazy-loading routes uses a CSS animation. The animation will fade the indicator into view using Opacity. But, will wait 100ms before moving past the first keyframe. This will keep the loading indicator hidden for the first 100ms of its existence, giving fast network connections the chance to load the remote code before the indicator is presented. But, will likely show the indicator for slower network connections.

Ideally, we might want to use Angular's Animation module to handle this since the Animations module can animate elements both into and out of existence, where as a CSS animation can only handle elements that exist. But, like I said earlier, I'm trying to keep this demo as simple as possible.

That said, if we load the Angular app and switch to a slow network connection using Chrome's Network tools, we can see that the loading indicator shows up as we navigate to a lazy-loaded route:


 
 
 

 
 Showing a loading indicator for lazy-loaded routes for users on slow network connections. 
 
 
 

As you can see, the simulated slow network connection gave the CSS animation enough to time to run, bringing the loading indicator into view.

Now, to be clear, this loading indicator only shows the first time that the user navigates to the route. Once a lazy-loaded route configuration is merged into the active application, the RouteConfigLoadStart and RouteConfigLoadEnd events stop firing (since the code doesn't need to be loaded a second time).

When it comes to the mechanics of the lazy-loaded routes, Angular 6.1.7 and Webpack 4 are a blackbox to me. I don't understand how Angular performs the HTTP requests; or how it knows where to event locate the code. But, that doesn't mean that I can't create a better user experience (UX) around the lazy-loaded routes. In this case, the Router's events make it fairly easy to know when a remote route configuration is being loaded. And, we can use those events to let the user know remote code is being loaded while they wait for the application to respond.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.