Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at RIA Unleashed (Nov. 2009) with: Elad Elrom
Ben Nadel at RIA Unleashed (Nov. 2009) with: Elad Elrom@EladElrom )

Creating An Event-Driven Pre-Bootstrap Loading Screen In Angular 2.0.0

By Ben Nadel on

In an earlier post on creating a pre-bootstrap loading screen in Angular 2 RC 1, Moataz asked me about closing the loading screen based on an application event rather than the SystemJS-loaded event. And to be honest, I'm not really sure how to accomplish this in the prototypical "Angular way." Ultimately, we need the Angular app to communicate with the world outside the Angular app. Which means, in one or another, a bridge has to be created. For this demo, I'm using the underlying DOM (Document Object Model) tree as that bridge, emitting a DOM-event from within the application when the application is ready to receive user interaction.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

I am sure that there is some way for the parent page to reach into the Angular Injector and read some application state. But, this feels ultra-janky and would likely create an unnecessarily complex mental model for the developer. By using the DOM tree as the cross-boundary bridge, it uses a mental model and a communications-flow that developers are familiar with. And, hopefully we can still do it in a way that is cross-platform compatible.

First, let's look at our index page - where the root of our Angular 2 application is loaded. In the following code, you'll notice that the pre-bootstrap loading page is outside of the root component. We need to do this because Angular will immediately replace the root component's content with the root component's template when the app is initialized. And, sine we can't project static content into the root component, we can only maintain control by keeping the root component and our pre-bootstrap loading page as sibling elements:

In the following code, notice that the pre-bootstrap loading logic is binding to an "appready" DOM-event on the document node. It's using this as the initiator of the loading screen teardown process. This is the event that we will need to emit from within the Angular application.

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Creating An Event-Driven Pre-Bootstrap Loading Screen In Angular 2.0.0
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  •  
  • <!-- Load libraries (including polyfill(s) for older browsers). -->
  • <script type="text/javascript" src="../../vendor/angular2/2.0.0/node_modules/core-js/client/shim.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angular2/2.0.0/node_modules/zone.js/dist/zone.js"></script>
  • <script type="text/javascript" src="../../vendor/angular2/2.0.0/node_modules/reflect-metadata/Reflect.js"></script>
  • <script type="text/javascript" src="../../vendor/angular2/2.0.0/node_modules/systemjs/dist/system.src.js"></script>
  •  
  • <!-- Load the Web Animations API polyfill for most browsers (basically any browser other than Chrome and Firefox). -->
  • <!-- <script type="text/javascript" src="../../vendor/angular2/2.0.0/node_modules/web-animations-js/web-animations.min.js"></script> -->
  •  
  • <!-- Configure SystemJS loader. -->
  • <script type="text/javascript" src="./system.config.js"></script>
  • </head>
  • <body>
  •  
  • <h1>
  • Creating An Event-Driven Pre-Bootstrap Loading Screen In Angular 2.0.0
  • </h1>
  •  
  • <my-app></my-app>
  •  
  •  
  • <div id="pre-bootstrap-container">
  •  
  • <!--
  • In this approach, rather than putting the pre-bootstrap content inside
  • the <my-app> component content, we're leaving it external to the Angular 2
  • application entirely. This way, the content is not automatically removed when
  • the root component template is rendered. Instead, we'll leave this overlay in
  • place until the "appready" event bubbles up to the document, at which point,
  • we can gracefully fade it out of view.
  • -->
  •  
  • <script type="text/javascript">
  •  
  • // Listen for the "appready" event, which will be emitted by the application
  • // and bubble up (as far as we know) to the document root.
  • document.addEventListener( "appready", handleAppReady );
  •  
  •  
  • // I handle the "appready" event and teardown the loading screen.
  • function handleAppReady( event ) {
  •  
  • var preBootstrapContainer = document.getElementById( "pre-bootstrap-container" );
  • var preBootstrap = document.getElementById( "pre-bootstrap" );
  •  
  • // Add the class-name to initiate the transitions.
  • preBootstrap.className = "loaded";
  •  
  • // Remove the bootstrap container after the transition has
  • // completed (based on the known transition time).
  • setTimeout(
  • function removeLoadingScreen() {
  •  
  • preBootstrapContainer
  • .parentNode
  • .removeChild( preBootstrapContainer )
  • ;
  •  
  • },
  • 300
  • );
  •  
  • }
  •  
  • </script>
  •  
  • <style type="text/css">
  •  
  • #pre-bootstrap {
  • background-color: #262626 ;
  • bottom: 0px ;
  • left: 0px ;
  • opacity: 1.0 ;
  • position: fixed ;
  • right: 0px ;
  • top: 0px ;
  • transition: all linear 300ms ;
  • -webkit-transition: all linear 300ms ;
  • z-index: 999999 ;
  • }
  •  
  • #pre-bootstrap.loaded {
  • opacity: 0.0 ;
  • }
  •  
  • #pre-bootstrap div.messaging {
  • color: #FFFFFF ;
  • font-family: monospace ;
  • left: 0px ;
  • margin-top: -37px ;
  • position: absolute ;
  • right: 0px ;
  • text-align: center ;
  • top: 50% ;
  • }
  •  
  • #pre-bootstrap h1 {
  • font-size: 26px ;
  • line-height: 35px ;
  • margin: 0px 0px 20px 0px ;
  • }
  •  
  • #pre-bootstrap p {
  • font-size: 18px ;
  • line-height: 14px ;
  • margin: 0px 0px 0px 0px ;
  • }
  •  
  • </style>
  •  
  • <div id="pre-bootstrap">
  • <div class="messaging">
  •  
  • <h1>
  • App is Loading
  • </h1>
  •  
  • <p>
  • Please stand by for your ticket to awesome-town!
  • </p>
  •  
  • </div>
  • </div>
  •  
  • </div>
  •  
  • </body>
  • </html>

Now, imagine that in order to render the root of the component, we need to load some data over the network. For example, we might have to load the account data for the current user. And, in order to prevent the pre-bootstrap screen from revealing to an empty white page (while the Account request is running), we can keep the pre-bootstrap page in place until that first Account request comes back.

In the following root-component's constructor, you can see that we make a request to get the account data; then, only when that request comes back successfully, do we trigger that "appready" event for which the pre-bootstrap logic is listening:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // Import the application services.
  • import { AccountService } from "./account.service";
  • import { DOMEvents } from "./dom-events";
  • import { IAccount } from "./account.service";
  •  
  • @Component({
  • selector: "my-app",
  • template:
  • `
  • <template [ngIf]="account">
  •  
  • <h3>
  • Welcome {{ account.name }}.
  • </h3>
  •  
  • <p>
  • I hope youre having a beautiful day!
  • </p>
  •  
  • </template>
  • `
  • })
  • export class AppComponent {
  •  
  • public account: IAccount;
  •  
  •  
  • // I initialize the component.
  • constructor( accountService: AccountService, domEvents: DOMEvents ) {
  •  
  • this.account = null;
  •  
  • // At this point, the application has "loaded" in so much as the assets have
  • // loaded; but, the we're not going to consider the application "ready" until
  • // the core "data" has loaded. As such, we won't trigger the "appready" event
  • // until the account has been loaded.
  • accountService.getAccount().subscribe(
  • ( account ) => {
  •  
  • this.account = account;
  •  
  • // Now that the core data has loaded, let's trigger the event that the
  • // pre-bootstrap loading screen is listening for. This will initiate
  • // the teardown of the loading screen.
  • domEvents.triggerOnDocument( "appready" );
  •  
  • }
  • );
  •  
  • }
  •  
  • }

In Angular 2, I'm still mostly confused about the whole "platform agnostic" approach to application development. So, to mitigate some of this confusion, I'm trying to encapsulate the DOM-tree interaction inside its own service - DOMEvents. This way, if the application needs to run on a platform that doesn't have a DOM-tree, this service can be swapped-out in that platform's AppModule.

The DOMEvents service simply injects the DOCUMENT dependency-injection (DI) token and encapsulates the calls to .dispatchEvent() on the DOM:

  • // Import the core angular services.
  • import { DOCUMENT } from "@angular/platform-browser";
  • import { Inject } from "@angular/core";
  • import { Injectable } from "@angular/core";
  •  
  • @Injectable()
  • export class DOMEvents {
  •  
  • private doc: Document;
  •  
  •  
  • // I initialize the service.
  • // --
  • // NOTE: When I first tried to approach this problem, I was going to try and use the
  • // core Renderer service; however, it appears that the Renderer cannot be injected
  • // into a service object (throws error: No provider for Renderer!). As such, I am
  • // treating THIS class as the implementation of the DOM abstraction (so to speak),
  • // which can be overridden on a per-environment basis.
  • constructor( @Inject( DOCUMENT ) doc: any ) {
  •  
  • this.doc = doc;
  •  
  • }
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I trigger the given event on the document root.
  • public triggerOnDocument( eventType: string ) : Event {
  •  
  • return( this.triggerOnElement( this.doc, eventType ) );
  •  
  • }
  •  
  •  
  • // I trigger the given event configuration on the given element.
  • public triggerOnElement(
  • nativeElement: any,
  • eventType: string,
  • bubbles: boolean = true,
  • cancelable: boolean = false
  • ) : Event {
  •  
  • var customEvent = this.createEvent( eventType, bubbles, cancelable );
  •  
  • nativeElement.dispatchEvent( customEvent );
  •  
  • return( customEvent );
  •  
  • }
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I create and return a custom event with the given configuration.
  • private createEvent(
  • eventType: string,
  • bubbles: boolean,
  • cancelable: boolean
  • ) : Event {
  •  
  • // IE (shakes fist) uses some other kind of event initialization. As such,
  • // we'll default to trying the "normal" event generation and then fallback to
  • // using the IE version.
  • try {
  •  
  • var customEvent: any = new CustomEvent(
  • eventType,
  • {
  • bubbles: bubbles,
  • cancelable: cancelable
  • }
  • );
  •  
  • } catch ( error ) {
  •  
  • var customEvent: any = this.doc.createEvent( "CustomEvent" );
  •  
  • customEvent.initCustomEvent( eventType, bubbles, cancelable );
  •  
  • }
  •  
  • return( customEvent );
  •  
  • }
  •  
  • }

Now, when we load the Angular 2 application, the pre-bootstrap loading screen will remain open even after the application has been bootstrapped. It will remain open until the Angular 2 application emits the "appready" event on the DOM; which means, by the time the pre-bootstrap screen closes, the account will have been loaded and the application is ready for user interaction:


 
 
 

 
 Creating an event-driven pre-bootstrap loading screen in Angular 2. 
 
 
 

Again, I'm not sure if this is the "Angular way" to do this. But, I believe that the use of encapsulation around the triggering of the DOM-event means that it is still cross-platform compatible. That said, in hindsight (as I'm writing this), perhaps I should have called the event-service something like "AppReadyEvent"; and given it a single public method like ".trigger()". This would have further abstracted away the whole idea of the DOM (from with inside the application); and, would have allowed me to build in features like debouncing logic (to make sure it only ever gets triggered once).

Here's the AccountService class, in case you are curious about the simulated network latency:

  • // Import the core angular services.
  • import { Injectable } from "@angular/core";
  • import { Observable } from "rxjs/Observable";
  •  
  • // Import the rxJs modules for their side-effects.
  • import "rxjs/add/observable/of";
  • import "rxjs/add/operator/delay";
  • import "rxjs/add/operator/do";
  •  
  • export interface IAccount {
  • id: number;
  • name: string;
  • }
  •  
  • @Injectable()
  • export class AccountService {
  •  
  • // I initialize the service.
  • constructor() {
  •  
  • // ...
  •  
  • }
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I get the account of the current user. Returns a stream.
  • // --
  • // CAUTION: Implements a 2.5 second delay for demo.
  • public getAccount() : Observable<IAccount> {
  •  
  • var stream = Observable
  • .of({
  • id: 4,
  • name: "Kim"
  • })
  • .do(
  • function() {
  •  
  • console.group( "getAccount() - simulated network latency." );
  • console.log( "Initiating request." );
  • console.log( "Waiting 2,500ms ..." );
  •  
  • }
  • )
  • .delay( 2500 ) // To make the demo interesting.
  • .do(
  • function() {
  •  
  • console.log( "Wait over - delivering data." );
  • console.groupEnd();
  •  
  • }
  • )
  • ;
  •  
  • return( stream );
  •  
  • }
  •  
  • }

Because of the relative weight of a Single-Page Application (SPA), pre-bootstrap loading pages are an important part of the user experience (UX). But, the successful loading of the assets doesn't necessarily mean that the application is ready for user interaction. As such, it's nice to be able to keep the pre-bootstrap loading page open until the application announces itself as being ready to rawk.




Reader Comments

I am very grateful!! (and so shall be future readers :D ) . Now I know how to communicate with the "presenter" that hosts the Angular app. I am sure this can be further generalized to scenarios other than loading. One possible scenario is to display a crash error message when unhandled errors or unexpected states occur in the app.

Reply to this Comment

Seriously, this is one of my favorites from your archive. Thanks so much for taking the time to document and share it!

Reply to this Comment

@All,

Thanks fellas. This was interesting to think about. Plus, I'm still "iffy" on all the "browser platform" separation and encapsulation. It's especially hard to think about since I've never done any server-side rendering for JavaScript apps (well, not using "universal" type code).

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
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.