Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Katie Maher
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Katie Maher

Creating A Pre-Bootstrap Loading Screen In AngularJS

By Ben Nadel on

As your AngularJS applications get bigger, you may start to notice that the apps don't bootstrap immediately - it takes time to load all the scripts over the network. Out of the box, AngularJS deals with this by providing an ngCloak directive which will hide pre-compiled HTML. But that's kind of a weak solution. Instead, it would be nicer to present the user with a meaningful "loading" screen that gets removed when the scripts have loaded and the AngularJS application has been fully bootstrapped.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

Until your AngularJS application has been bootstrapped, all the HTML in your browser is just normal "static" HTML. This isn't a weakness, though, it's a strength. As web developers, static HTML is our bread-and-butter. Of all the things that we do on a daily basis, static HTML is the easiest to deal with and the easiest to reason about.

Getting a loading page to show is simple - we just need to slap some HTML on the page. Getting the loading page to disappear once the AngularJS application is bootstrapped, that's the point of complexity.

The easiest way to do this is to simply wrap some static HTML in an ngIf directive that is set to false:

  • <!-- BEGIN: Pre-Bootstrap Loading Screen. -->
  • <div ng-if="false">
  •  
  • <p>
  • This will show until the application is bootstrapped.
  • Then, the ngIf directive will immediately rip it out of
  • the page.
  • </p>
  •  
  • </div>
  • <!-- END: Pre-Bootstrap Loading Screen. -->

This will show the HTML, by default (that's what browsers do); then, once AngularJS compiles and links the HTML, the ngIf directive will remove the given DOM element.

While this approach is very simple, it does have few drawbacks. For one, the ngIf directive binds a $watch() handler which will live for the duration of the application. Granted, it doesn't do any computation, so it has no practical cost; but, it just feels less than clean. And, another drawback is that it's hard to get animations to work during the bootstrapping phase of the application.

To get around these two drawbacks, I'm going to create a custom directive that bypasses any $watch() bindings and elegantly animates the pre-loading screen out of view using the ngAnimate module (and $animate service).

This it the first time that I've ever used $animate and I, of course, immediately ran into an issue. When the AngularJS application is bootstrapping, all animations are disabled. They remain disabled until all routing and templating information is loaded and at least two digests have passed. This is intended to prevent a flurry of animation when the application loads.

But, I want the animation to run immediately, regardless of the state of the application. Luckily, you can override this by using the ngAnimateChildren directive. This directive is intended to allow you to animate a child element even while the parent containers are animating. But, an ?? undocumented ?? side-effect of this is that it will also allow you to animate children while animations are disabled at the root (such as they are during bootstrapping).

Bringing this all together, my directive will execute the .leave() animation on its child container; then, once the animation is complete, the directive will completely remove all elements from the page. And, since it doesn't create a new scope or define any new $watch() bindings, it leaves the main AngularJS application quite clean.

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Creating A Pre-Bootstrap Loading Screen In AngularJS
  • </title>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <!--
  • BEGIN: App-Loading Screen.
  • --
  • Until the AngularJS application code is loaded and bootstrapped, this is just
  • "static HTML." Meaning, the [class-based] directive, "mAppLoading", won't
  • actually do anything until the application is initialized. As such, we'll give
  • it just enough CSS to "fail open"; then, when the AngularJS app loads, the
  • directive will run and we'll remove this loading screen.
  •  
  • NOTES ON ANIMATION:
  •  
  • When the AngularJS application is loaded and starts bootstrapping, all
  • animations are disabled until all the routing information and templating
  • information is loaded AND at least two digests have run (in order to prevent
  • a flurry of animation activity). As such, we can't animate the root of the
  • directive. Instead, we have to add "ngAnimateChildren" to the root element
  • and then animate the inner container. The "ngAnimateChildren" directive allows
  • us to override the animation-blocking within the bounds of our directive, which
  • is fine since it only runs once.
  • -->
  • <div class="m-app-loading" ng-animate-children>
  •  
  • <!--
  • HACKY CODE WARNING: I'm putting Style block inside directive so that it
  • will be removed from the DOM when we remove the directive container.
  • -->
  • <style type="text/css">
  •  
  • div.m-app-loading {
  • position: fixed ;
  • }
  •  
  • div.m-app-loading div.animated-container {
  • background-color: #333333 ;
  • bottom: 0px ;
  • left: 0px ;
  • opacity: 1.0 ;
  • position: fixed ;
  • right: 0px ;
  • top: 0px ;
  • z-index: 999999 ;
  • }
  •  
  • /* Used to initialize the ng-leave animation state. */
  • div.m-app-loading div.animated-container.ng-leave {
  • opacity: 1.0 ;
  • transition: all linear 200ms ;
  • -webkit-transition: all linear 200ms ;
  • }
  •  
  • /* Used to set the end properties of the ng-leave animation state. */
  • div.m-app-loading div.animated-container.ng-leave-active {
  • opacity: 0 ;
  • }
  •  
  • div.m-app-loading div.messaging {
  • color: #FFFFFF ;
  • font-family: monospace ;
  • left: 0px ;
  • margin-top: -37px ;
  • position: absolute ;
  • right: 0px ;
  • text-align: center ;
  • top: 50% ;
  • }
  •  
  • div.m-app-loading h1 {
  • font-size: 26px ;
  • line-height: 35px ;
  • margin: 0px 0px 20px 0px ;
  • }
  •  
  • div.m-app-loading p {
  • font-size: 18px ;
  • line-height: 14px ;
  • margin: 0px 0px 0px 0px ;
  • }
  •  
  • </style>
  •  
  •  
  • <!-- BEGIN: Actual animated container. -->
  • <div class="animated-container">
  •  
  • <div class="messaging">
  •  
  • <h1>
  • App is Loading
  • </h1>
  •  
  • <p>
  • Please stand by for your ticket to awesome-town!
  • </p>
  •  
  • </div>
  •  
  • </div>
  • <!-- END: Actual animated container. -->
  •  
  • </div>
  • <!-- END: App-Loading Screen. -->
  •  
  •  
  •  
  • <h1>
  • Creating A Pre-Bootstrap Loading Screen In AngularJS
  • </h1>
  •  
  • <p>
  • You have {{ friends.length }} friends:
  • </p>
  •  
  • <ul>
  • <li ng-repeat="friend in friends">
  •  
  • {{ friend }}
  •  
  • </li>
  • </ul>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.3.8.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-animate-1.3.8.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [ "ngAnimate" ] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // SIMULATING NETWORK LATENCY AND LOAD TIME. We haven't included the ngApp
  • // directive since we're going to manually bootstrap the application. This is to
  • // give the page a delay, which it wouldn't normally have with such a small app.
  • setTimeout(
  • function asyncBootstrap() {
  •  
  • angular.bootstrap( document, [ "Demo" ] );
  •  
  • },
  • ( 2 * 1000 )
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • app.controller(
  • "AppController",
  • function( $scope ) {
  •  
  • console.log( "App Loaded!", $scope );
  •  
  • $scope.friends = [ "Kim", "Sarah", "Tricia" ];
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // This CSS class-based directive controls the pre-bootstrap loading screen. By
  • // default, it is visible on the screen; but, once the application loads, we'll
  • // fade it out and remove it from the DOM.
  • // --
  • // NOTE: Normally, I would probably just jQuery to fade-out the container; but,
  • // I thought this would be a nice moment to learn a bit more about AngularJS
  • // animation. As such, I'm using the ng-leave workflow to learn more about the
  • // ngAnimate module.
  • app.directive(
  • "mAppLoading",
  • function( $animate ) {
  •  
  • // Return the directive configuration.
  • return({
  • link: link,
  • restrict: "C"
  • });
  •  
  •  
  • // I bind the JavaScript events to the scope.
  • function link( scope, element, attributes ) {
  •  
  • // Due to the way AngularJS prevents animation during the bootstrap
  • // of the application, we can't animate the top-level container; but,
  • // since we added "ngAnimateChildren", we can animated the inner
  • // container during this phase.
  • // --
  • // NOTE: Am using .eq(1) so that we don't animate the Style block.
  • $animate.leave( element.children().eq( 1 ) ).then(
  • function cleanupAfterAnimation() {
  •  
  • // Remove the root directive element.
  • element.remove();
  •  
  • // Clear the closed-over variable references.
  • scope = element = attributes = null;
  •  
  • }
  • );
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As a sort of hack, I'm putting the minimum viable Style block inside the loading screen directive container. This way, when I strip out the container, the Style block will be stripped out as well.

This is the first time that I've used the $animate service; it seems pretty cool! Normally, I would have just used jQuery to .fadeOut() the container; but, I'm trying to be more adventurous in what responsibilities I give to AngularJS. That said, regardless of the animation mechanism, this is a pretty easy way to create a pre-bootstrap loading screen in your AngularJS application.




Reader Comments

You don't have the benefits of using ngAnimate, but: wouldn't it be far easier to use the new (Angular 1.3) one-time binding feature? In other words:

  • <div ng-if="::false">
  • <!-- ... -->
  • </div>

(I hope this comment works markup-wise ;) )

Reply to this Comment

@Vincent,

When I was trying to put this together, I did *try* to play around with the one-time binding stuff. But, it was the first time I've ever tried it (it's on my list of things to research). I keep getting "parsing" errors with the expression. I think I wasn't sure where to put the "::" in the expression.

That said, if your code works, I think it would definitely be the easiest solution - and would get rid of that watcher. But, I am just not personally sure how the one-time binding works yet.

Good feedback!

Reply to this Comment

@Vincent,

Ugg, now I'm itching to go research one-time bindings... why is the "work day" so darn long :D

Reply to this Comment

@Ben,

I fell you -- I'm now wondering whether Angular might even detect that there are no variables in an expression and only parse it once when it just says `false`.

Reply to this Comment

@Vincent. It works. It evaluates any valid angular expression once. Eg: 1+2, a+b, user.name, items[index] or function calls update(). Angular Expressions (http://goo.gl/L36C6T)

@Ben. I can't believe you don't know about it =) You can try one-time binding and play with all new features in 1.3 doing this hacking session http://goo.gl/WckmAL including full code (jsbin) that I did for my Angular meetup group.

Reply to this Comment

@Vincent,

I just looked at the AngularJS code and it turns out that "false" only gets evaluated once, regardless of the one-time binding. At least in 1.3, it determines that the expression is a "constant" and therefore unbinds the watch handler during the first invocation of the callback.

It looks like this behavior might have existed since 1.2 - I am not seeing any special treatment of "constants" in 1.0.8.

So that said, it seems the *only* downside to using `ng-if="false"` is that you can't get the animation to work.

Reply to this Comment

@Gerard,

Thanks for the link - I'll try to take a look. Currently exploring the $parse() function trying to see how this all comes together.

Reply to this Comment

If the point is to entertain the user and make them feel like something is happening, couldn't you use css to create a simple animation for this?

Reply to this Comment

@Kirk,

I think so. That would actually be fun to have something a little interactive while the page loads. Of course, hopefully, the page loads as fast as possible, so we *hopefully* don't have time to do much :D

Reply to this Comment

@Satron,

your link seems to be broken... here's a Codepen I created off the back of your suggestion.

http://codepen.io/meetbryce/pen/PqNOXe

Reply to this Comment

Works great ! But, when I open up the console, I get an error : "Error: $animate.leave(...) is undefined". Could you explain, why I'm getting this?

Reply to this Comment

Hi Ben,

I'm having issues with getting the default route to resolve after the manual bootstrap.

Our app has grown and started as an automatically bootstrapped app, but we now need to get some configuration data from a service before we continue with initialising the app. So following your article above, everything seems to have loaded, I have a navigation pane which contains all the routes, If I click on them it redirects as it should, however the default view just doesn't seem to want to load.

Not sure what the problem might be. All the examples I have found online are very simple examples where most of the code is contained in a single page with script tags etc.

I'm now trying to do a redirection after the "angular.bootstrap" call to see if I can force the view/controller to be loaded.

BTW: I'm using the ng-view directive to load the views (if that helps/makes a difference).

Reply to this Comment

OK, my bad. I had mistakenly removed the app.run() code block away, thinking that would not be needed with manual bootstrapping. But it does.

Reply to this Comment

This saved me a lot of time and effort, thank you!

However, there was some issues with overlap of the underlying elements, so I changed the position of this element to relative positioning.
```
div.m-app-loading {
position: relative ;
}
```

I also wanted to hide some translation work and other initial messyness so I wrapped the $animate.leave call in a $timeout to extend the "loading time", and I think it worked out great!

Reply to this Comment

Thanks for the code sample. As most of your articles, this was useful.
I have a slightly more elaborated requirement. I want the splash screen to stay on screen at least x seconds. If the app takes longer to load, then the splash screen should disappear once the bootstrapping is complete. However, if the apps takes less than x seconds, the screen should stay on. Not been a conclusive effort to date. Any suggestion ?

Reply to this Comment

@Cedric,

I'd use the same functionality but you'd need to combine it with a 2 variables. One that gets set to true after the and has elapsed (use $timeout) and another after the page has loaded.

Then have the code that removes the loader wrapped in a while loop where (!time || !loaded).

Reply to this Comment

I want to put pleasewait.js into the animation,

and I am unable to create the splashscreen itself.

I am unable to understand the filestructure.

This seems to be a standalone HTML page. Does this code has to be in my index page to work?

Reply to this Comment

Thanks for this interesting article! I tried to apply this method but it somehow fails to work... The spinner only is displayed after AngularJS is done bootstrapping.

Any idea why this might happen?

I put more details about my issue in this stackoverflow question, in case anyone is interested in looking further... Thanks!
http://stackoverflow.com/questions/35361208/why-are-the-dom-plain-html-elements-rendered-only-after-my-angular-app-is-done-l

Reply to this Comment

@Filip,

Actually it does. Though you need to force animations to be active.

I added the following line just above the line: $animate.leave

$animate.enabled(true);

Reply to this Comment

Hi,
I'm using a simple:

<div class="bootstrap-loader" ng-class="{loaded: true}">

And i just make it disappear by css when the "loaded" class is added by angular. Is it so bad? Any performance issue or something that i didn't think about that make your code preferable?

Thank you.

Reply to this Comment

@Tim,

It's not working for me. I'm @ angular 1.5.6. Does some one have a suggestion?

Reply to this Comment

@Marco,

For all intents and purposes, there's no difference between your code and my code. The only real difference is that your DIV will remain in the DOM for the duration of the application life-cycle since you're only "hiding it" with ngClass, not destroying it.

The ngIf directive, on the other hand, will actually rip the element out of the DOM. So, there may be a tiny tiny tiny difference in memory usage (since there are less Nodes in the DOM tree). But, the difference is likely to be negligible in the context of the larger application.

My personal preference would be to use ngIf; but, both approaches are totally valid.

Reply to this Comment

@Yaseen,

Most excellent suggestion! Now that I'm actually digging into Angular 2, the time is now!! :D

Reply to this Comment

Hello Ben,

I've been following you for quite some time now.

This stuff works well for websites, but for cordova apps, the splash screen can't be GIF animated that's what we found in a small Research. Is there anyway we can either use GIF images or some html to hold on till that point?

Any ideas?

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.