Skip to main content
Ben Nadel at Scotch On The Rock (SOTR) 2010 (Brussels) with: Ray Camden
Ben Nadel at Scotch On The Rock (SOTR) 2010 (Brussels) with: Ray Camden ( @cfjedimaster )

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

By on

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

1 Comments

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 :)

15,674 Comments

@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!

15,674 Comments

@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.

1 Comments

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?

2 Comments

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

15,674 Comments

@Dave,

The only dependency for the AppEvent is the DOCUMENT. And, looking at the Angular docs, I think perhaps the location of the DOCUMENT switched to a different place. At the top of the platform-browser file, it says:

import from @angular/common instead.

Maybe try changing your import statement and see if that helps it?

15,674 Comments

@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

15,674 Comments

@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.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel