Creating An Event-Driven Pre-Bootstrap Loading Screen In Angular 2.0.0
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:
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.
Want to use code from this post? Check out the license.
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.
Seriously, this is one of my favorites from your archive. Thanks so much for taking the time to document and share it!
@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).
@All,
Ok, the service naming I was using was bothering me. I feel like it was too closely tied to the Browser platform and the concept of the DOM; so, I refactored it slightly to have a cleaner abstraction:
www.bennadel.com/blog/3151-revisited-creating-an-event-driven-pre-bootstrap-loading-screen-in-angular-2-0-0.htm
The concept is exactly the same; the lines of separation are just cleaner.
Hi,
I've gone for a CSS approach for my Angular2 pre loading page.
Regarding your loading approach does that work on mobile?
I just display none to my pre loading div when the app is not empty using css.
But my approach doesnt seem to show on mobile and only on desktop.
On mobile I get a white screen and then it shows the app. This is chrome and safari on an iphone 6 plus.
I look forward to hearing your comments.
Kind regards,
Mark
When running your demo on Internet Explorer, I am getting following error:
TypeError: Argument not optional
Any thoughts why it can be better than a simple approach described here -http://stackoverflow.com/a/35244932/968003 ?
@AK,
Great question - someone actually just pointing out that solution in another post of mine. Using the CSS3 selectors for :empty and + sibling are very clever. And, in fact that solution can even leverage Transition timing to get the loader to fade out nicely.
I think that is a good solution. But, in the context of this particular blog post - waiting until application data is loaded before closing the loading screen - I don't see that as being possible with CSS alone. After all, there is a big difference between ":empty" pseudo-selector and having data loaded. When your application loads, there will still be, at the very least, an application shell that loads into the root component. This means that ":empty" will no longer select after the app is boot-strapped, regardless of whether or not the data is loaded.
Plus, a minor note is that the DOM elements are always there, which just makes the DOM tree a bit larger, for the lifetime of the app.
So, it really depends on your needs. If you just want your loading screen to disappear the moment your app is bootstrapped, using the CSS approach is totally legit, and very clever. But, if you want a bit more control over the timing and the interplay with the application itself, I don't think you can accomplish that with CSS alone.
@Mark,
If you're using CSS, I am not sure why things wouldn't work on mobile. Perhaps you are loading too many Scripts *above* the DOM so that it blocks too long on your mobile processor. I am not too familiar with mobile development, sorry.
@Ashfaq,
very late to the game, but just add a fourth parameter 'null'
"customEvent.initCustomEvent(eventType, bubbles, cancelable, null);"
CustomEvent is defined as following
"interface CustomEvent extends Event {
readonly detail: any;
initCustomEvent(typeArg: string, canBubbleArg: boolean, cancelableArg: boolean, detailArg: any): void;
}"
Somehow IE (tested with 11) totally needs all parameters.