Creating A Pre-Bootstrap Loading Screen In AngularJS
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.
Want to use code from this post? Check out the license.
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:
(I hope this comment works markup-wise ;) )
@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!
@Vincent,
Ugg, now I'm itching to go research one-time bindings... why is the "work day" so darn long :D
@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`.
@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.
@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.
@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.
@All,
If anyone is interested, I dug into the one-time data bindings. Now, it makes a lot more sense in my head:
www.bennadel.com/blog/2759-exploring-one-time-bindings-in-angularjs-1-3.htm
One-time data bindings aren't "one thing"; they are the interplay between the $parse() service and the $watch() bindings. This is why I was so confused by the placement of the "::" token in the expressions.
Cool. Thanks!
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?
@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
@Ben,
how about the old fashion way
added a pre-loader that gets removed by window.onload - that seems to work as well http://corkywine.com/ang_seed/ui/
@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
@Meetbryce,
Yup that works - sorry changed the url to http://satronapps.com/ and the loader is running there
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?
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).
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.
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!
Great! Very useful, thank you.
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 ?
@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).
Nice example, thanks!
It doesn't seem to work with angular and animate version 1.4.8
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?
Any chance of doing a demo for Angular2?
Thanks
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
@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);
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.
@Tim,
It's not working for me. I'm @ angular 1.5.6. Does some one have a suggestion?
@Anna,
There are many ways of implementing this. Could you post some code or a longer description?
@Anna,
Did you include the ngAnimate module in you code?
@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.
@Yaseen,
Most excellent suggestion! Now that I'm actually digging into Angular 2, the time is now!! :D
@Yaseen,
I took a look at the way I might approach this in an Angular 2 application:
www.bennadel.com/blog/3105-creating-a-pre-bootstrap-loading-screen-in-angular-2-rc-1.htm
The approach is fundamentally the same. But, since NG1 and NG2 apps are bootstrapped differently (the entire HTML document vs. a root component), we have to add a little more logic to get the nice user experience.
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?
Hi,
How would you elongate the time that the loading page is visible?
On my Angular app, routes are managed by StateProvider, and this fix works only for a split second and you can still see some content loading on the screen. I think it'd be perfect if it stayed for 1s or 2s longer.
From what I understood, you're relying on 'link' to indicate when the content has been loaded.I wonder if this is irrelevant when using StateProvider.
@Nicolai,
I am relying on the link() function to animate the element out of the page. But, I am also calling $animate immediately. I think if you wanted the loading page to be visible for a bit longer, you could just use a setTimeout() before you make the call the $animate. That way, there would be a delay between the link() invocation and the actual remove of the DOM node, if that makes sense.
@Nithin,
Sorry, I'm not too familiar with mobile app development.
It seems that since angular 1.4 and the introduction of $animateCss this code is not working properly anymore. The fading is not triggered.
Anyone has any suggestions?
I found out I have to enable animation during angular bootstrap (disabled by default). It was the only thing missing.
Here is the related doc: https://docs.angularjs.org/guide/animations#animations-on-app-bootstrap-page-load.
Hoping this can help someone else!