Showing A Loading Indicator For Lazy-Loaded Route Modules In Angular 6.1.7
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> —
<a routerLink="/app/feature-a">Feature A</a> —
<a routerLink="/app/feature-b">Feature B</a> —
<a routerLink="/app/feature-c">Feature C</a> —
<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:
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.
Want to use code from this post? Check out the license.
Reader Comments
@All,
I did a follow-up to this lazy-loading approach to now include preloading of lazy-loaded modules:
www.bennadel.com/blog/3506-preloading-lazy-loaded-feature-modules-in-angular-6-1-9.htm
With the Angular Router, it is actually super simple! And, we can keep the "loading indicator" when it is meaningful for user feedback. Playa!!
Ben, thank you so much for this, it's super helpful and practical.
@Alex,
Awesome, I'm glad you found this helpful :D
Just a question. Whats the purpose of !! ?
@Ben Nadel
Once again, thanks a bunch for awesome content. Whenever I need a solution to a complex angular challenge, I know that you're the guy whose blog will have the answer!
@Tom,
That's awesome :D I am thrilled to know your experience here has been quality.
@Snso,
The
!!
, sometimes referred to as the "double-bang" operator, is really just two "not operators" (!
) next to each other. The double-bang operator is used to convert a Truthy / Falsy value to an actualBoolean
value. Think about it like this:... is the same as:
So, if
value
is a Truthy value, like3
, this becomes:... which, becomes partially evaluated as:
... which then becomes:
So, we converted
3
- a Truthy value - totrue
- aBoolean
value.Now, going back to the demo, my use of
!!
:... is to convert the
asyncLoadCount
- a Truthy value - to a full-onBoolean
value.I hope that makes more sense.
@Ben, thanks for this awesome article. Just one question. Is there a way where I can put a label on a module so that instead of displaying a generic "loading module", I could instead display a message based on that label. For example, if the administrator module is being loaded, the message will show "Loading the Administrator module..."? Thanks!
@Wendell,
Oh, very interesting question! It looks like the
RouteConfigLoadStart
event type includes aroute
property:https://angular.io/api/router/RouteConfigLoadStart
This
route
property includes all of the data that was provided at configuration time for the route. So, it seems like you could definitely add something likedata.lazyLoadName
, which you could then bind to a public property in the View. I've not tried this myself; but, the idea seems sound.