Revisited: Creating An Event-Driven Pre-Bootstrap Loading Screen In Angular 2.0.0
Earlier this week, I revisited the idea of creating a pre-bootstrap loading screen in Angular 2 (which was, itself, a revisiting of an earlier post). In that post, I used the Document Object Model (DOM) as a means to cross the application boundary, allowing the app to announce an "appready" event to the host page, which was managing the pre-bootstrap loading screen. While I like this idea, I didn't really like my particular implementation as it allowed the concept of the DOM to leak into the greater context. Today, I wanted to briefly revisit this idea (again), simply to refactor it into a set of boundaries that feel cleaner and more platform-agnostic.
Run this demo in my JavaScript Demos project on GitHub.
In my previous implementation, the carrier of the "appready" event was a service called DOMEvents. The root component then explicitly triggered the "appready" event on the DOM using using the following method:
domEvents.triggerOnDocument( "appready" );
The problem with this is that it clearly ties the entire mechanism to the DOM, which in turn, clearly ties it to the browser. This means that if we ever implemented this app on a different platform that didn't necessarily have a Document or a similar "DOM" concept, we'd end up with a service implementation whose name was terribly misleading.
To fix this, I refactored the service to be called "AppReadyEvent" which only exposes a single method (at this time): .trigger():
// Import the core angular services.
import { DOCUMENT } from "@angular/platform-browser";
import { Inject } from "@angular/core";
import { Injectable } from "@angular/core";
@Injectable()
export class AppReadyEvent {
private doc: Document;
private isAppReady: boolean;
// 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;
this.isAppReady = false;
}
// ---
// PUBLIC METHODS.
// ---
// I trigger the "appready" event.
// --
// NOTE: In this particular implementation of this service on this PLATFORM, this
// simply triggers the event on the DOM (Document Object Model); however, one could
// easily imagine this event being triggered on an Observable or some other type of
// message transport that makes more sense for a different platform. Nothing about
// the DOM-interaction leaks outside of this service.
public trigger() : void {
// If the app-ready event has already been triggered, just ignore any subsequent
// calls to trigger it again.
if ( this.isAppReady ) {
return;
}
var bubbles = true;
var cancelable = false;
this.doc.dispatchEvent( this.createEvent( "appready", bubbles, cancelable ) );
this.isAppReady = true;
}
// ---
// 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 );
}
}
As you can see, the implementation on the Browser platform is still tied to the Document Object Model. However, at this point, there's nothing in the naming of the service or its methods that indicate a platform relationship, creating a much cleaner abstraction.
And, with a cleaner abstraction at the event-service level, our root component also becomes much cleaner:
// Import the core angular services.
import { Component } from "@angular/core";
// Import the application services.
import { AccountService } from "./account.service";
import { AppReadyEvent } from "./app-ready-event";
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, appReadyEvent: AppReadyEvent ) {
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.
appReadyEvent.trigger();
}
);
}
}
Now, when the root component has its data loaded, it just triggers the AppReadyEvent. It doesn't know anything about the DOM - it doesn't know that this event is actually being triggered on the "document". It only knows that there's a service it should call.
I won't bother showing the rest of the code as it can all be found in the previous post. I just wanted to revisit this because my prior naming choices and lines of encapsulation where gnawing at my brain. As I struggle to wrap my head around platform-agnostic thinking, I'm sure I will continue to stumble a lot; but exercises like this are helping.
Want to use code from this post? Check out the license.
Reader Comments
Hey Ben. I needed to add a small polyfill to get this working in IE11:
http://stackoverflow.com/a/26596324/571237
I saw your comment about IE in the code, but without this IE would barf when creating the custom event.
Hi,
I love your posts.
I am trying to create a loading progress bar that progresses as elements are loaded and reaches 100% when everything is fetched from the server.
I am new to angular and I can't seem to understand how can I subscribe to loading events for all the resources or anythings similar.
Thanks for the great work you are doing :)
@Sam,
Oh snap! I had no idea that was needed. Which is kind of funny because I use MDN all the time when I have to look up custom Event creation and I don't remember seeing anything about IE, other than that it used a different method workflow (which I tried to use in my post). I didn't realize that this different workflow itself needed to by polyfilled. Thanks for the insight!
@Razvan,
That is a fantastic concept and is one that I have tried to think about a bit in the past. In my current demos, I have to load over 200+ files using System.js and it would be great to show some indication to the user that this is happening.
BUT, I temper that with the understanding that in a production system, I wouldn't be using System.js to load hundreds of files. Instead, I would load some "distribution" file that was pre-compiled down into one (or a few) massive JavaScript files. So, in that case, a progress bar makes less sense.
That said, I keep meaning to look at System.js to see if it emits any event as it loads individual files. Then, at least in my demos, I could tap into that event and put something on the screen.
So, to your question, I don't have a good answer - but, it is something I have been thinking about.
Hey Ben,
I like ur vids/posts about the pre-bootstrap loading screens! I'm searching for the same solution as Razvan (loading indicator which shows how far the app is loaded). The last post to that is some weeks ago, so maybe u have an approach right now to get the solution?
I've put your solution into a separate package, however I'm unable to make a simple project that uses it, build for production with AOT. The error is:
Can't resolve all parameters for AppReadyEvent in D:/DEVELOP/test_a6/node_modules/ws-core-lib/dist/src/common.services.d.ts: (?)
I've had a look through issues that look similar, e.g:
https://github.com/angular/angular/issues/22153
https://github.com/angular/angular/issues/24414
However none of the suggestions work
@Dave,
The only dependency for the
AppEvent
is theDOCUMENT
. And, looking at the Angular docs, I think perhaps the location of theDOCUMENT
switched to a different place. At the top of theplatform-browser
file, it says:Maybe try changing your
import
statement and see if that helps it?@Dev JB,
Sorry, no news on the progress indicator :(
@Ben,
Sadly, that did not help. This is an SO post that refers to the problem:
https://stackoverflow.com/questions/50920734/cant-resolve-all-parameters-for-when-consuming-injectable-from-a-package
I'm close to reporting it as a bug, since no-one seems to have a clue as to why it throws an error, and no-one has come up with something that will make it work
@Dave,
That's craziness :) Let me see if I can upgrade this demo to the latest version of Angular and see what shakes out. Now I'm curious :D
@All,
Ok, I've gone back and re-visited a pre-bootstrap loading screen one more time. Except, instead of using a custom DOM Event, I'm using the
window.postMessage()
method, which is intended to provide Cross-Origin communication:www.bennadel.com/blog/3482-creating-a-pre-bootstrap-loading-screen-with-window-postmessage-in-angular-6-1-2.htm
I think this is the best implementation I've done so far -- and, I think the
window.postMessage()
API feels more in alignment with the intent of the demo. Meaning, the demo is meant to communicate across the application boundary. Which, is not so different from Cross-Origin communication.