ng-Template Requests Are Affected By $http Interceptors In AngularJS
The other day, when I was exploring routing, nested views, and caching with ngRoute in AngularJS 1.x, I noticed something very interesting: the network latency, that I was simulating with an $http interceptor, was also delaying the loading and rendering of ng-template content. After a little digging into the AngularJS source code, I discovered that ng-template requests go through the $http service just like any other URL-based request; the only difference is that the requests are accompanied by the $templateCache.
Run this demo in my JavaScript Demos project on GitHub.
The beauty of the Script / ng-template directive is that content can be inlined and made available immediately, without having to go across the wire to your server. Beyond the philosophy of it, however, I never really thought much about how ng-template was implemented. But, once I saw that $http interceptors were affecting ng-template load times, I started to follow the request down the rabbit hole.
Staring with the ngInclude directive, ngInclude uses the $templateRequest() service to request the template. The $templatRequest() service then turns around and calls the $http() service; but, as part of the HTTP configuration object, it passes-in the $templateCache service. The $http() service then initiates the request, which is implemented as an asynchronous promise chain. The request workflow then looks in the provided cache, finds the cached template, and resolves the request before it has to go over the wire (ie, actually make an HTTP network call). The ngInclude directive then $compile()'s the resolved template and transcludes it into the DOM (Document Object Model).
It's just that simple (ha ha). Basically, it boils down to:
ngInclude -> $templateRequest -> $http.get( w/ cache:$templateCache ) -> $compile()
NOTE: The generic Directive "templateUrl" is also retrieved using the $templateRequest() and will, therefore, also be affected by $http interceptors.
To demonstrate this workflow, I've created a small app that has three ngInclude directives that leverage ng-template content. I also have an $http interceptor that incrementally slows down the request / response lifecycle. As such, we'll see the ngInclude directives slowly cascade onto the page:
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
ng-Template Requests Are Affected By $http Interceptors In AngularJS
</title>
</head>
<body>
<h1>
ng-Template Requests Are Affected By $http Interceptors In AngularJS
</h1>
<!--
Each of these ngInclude URLs can be pulled out of the Script / ng-template
directives below.
-->
<div ng-include=" 'one.htm' "></div>
<div ng-include=" 'two.htm' "></div>
<div ng-include=" 'three.htm' "></div>
<!--
These ng-include templates are here to provide a local implementation of the
"remote" URL content. This way, AngularJS doesn't have to make an HTTP request
over the wire (but, as you will see, it still goes through the $http() service).
-->
<script type="text/ng-template" id="one.htm">
<p>
I am the 1st ng-template include: <span bn-now></span>.
</p>
</script>
<script type="text/ng-template" id="two.htm">
<p>
I am the 2nd ng-template include: <span bn-now></span>.
</p>
</script>
<script type="text/ng-template" id="three.htm">
<p>
I am the 3rd ng-template include: <span bn-now></span>.
</p>
</script>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.13.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I add a delay to the HTTP request / response promise chain. Since all
// template requests go through the $http() service, these changes will also
// affect local requests for ng-template (which can be triggered by ngInclude
// and other directive template URLs).
app.config(
function simulateHttpLatency( $httpProvider ) {
$httpProvider.interceptors.push( slowDownRequest );
// I add a delay to post-HTTP portion of the promise-chain.
function slowDownRequest( $q, $timeout ) {
// The delay will be incremented with each request.
var delay = 0;
return({
response: function( response ) {
var latency = $q.defer();
$timeout(
function resolver() {
latency.resolve( response );
response = latency = null;
},
// Increment the delay with each HTTP request.
( delay += 1500 ),
// There's no need to trigger a $digest; the $q service
// will automatically do that when the request resolves.
false
);
return( latency.promise );
}
});
}
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I output the current time in the .text() of the parent element.
app.directive(
"bnNow",
function() {
// Return the directive configuration object.
return({
link: link
});
// I bind the JavaScript events to the local scope.
function link( scope, element, attributes ) {
element.text( ( new Date() ).toTimeString().split( " " ).shift() );
}
}
);
</script>
</body>
</html>
As you can see, each $http request is incrementally slowed down by 1,500 milliseconds. And, when we run the above code, we get the following page output:
As you can see, each of the ngInclude directives was rendered 1.5 seconds after the previous one. This is because each of them was delayed an additional 1,500 milliseconds in the $http service.
There's probably no practical take-away from this exploration. More than anything, it was just interesting to follow the workflow under the hood and see how everything ties together. It's quite fascinating to see how well all of the core AngularJS services work together in order to achieve awesomeness.
Want to use code from this post? Check out the license.
Reader Comments
Ben, as always, thanks for finding out these hidden angular tips and gotchas. Just wondering, is there a reason that you don't use build time tools like grunt-ngtemplate or gulp-ngtemplate to add the partial views $templateCache? We use this method to reduce the number of http request when loading / navigating through the app. What are your opinions on this?
I was surprised when I learned about this too! There are some interesting things you can do with it, though. For instance, check out this Gist:
https://gist.github.com/idosela/8421332
I thought that was a pretty neat example of how you might use $http interceptors for templates.
@Prabin,
Good question; I actually do use a "build step" of sorts. But, it's kind of janky. Right now, I actually use ColdFusion to include all of the templates into the main page request:
www.bennadel.com/blog/2430-inlining-angularjs-templates-using-coldfusion.htm
That said, I am starting to try to learn more about proper build tools like Gulp.js to handle more of this more me. I just haven't started a new project in a while that really necessitated a change in workflow.
@Sean,
Very interesting. I'm having a little trouble with the context, like where does the `activeProfile` come from. Is there a presentation that goes along with it? It looks like it was related to ng-conf.
Is there a way to overcome a directive's dependency on the http latency? Case in point: A have a directive for a Submit button that will disable the button whenever there is an outstanding http response. The directive uses a template to make the Submit button. When I simulate a delay, the Submit button does not show on the page until after the delay, during the first time the page loads. I would like for the Submit button to show immediately when the page loads, not after the http delay.
FYI: the Submit button initiates a request for more info which loads in a view, so the page is a SPA. Thus when the Submit button is clicked it auto-disables while it waits for the response, then re-enables once the response is received. It has an ng-click action that initiates the request, but the auto-disabling is hidden in the directive definition function that uses an http interceptor.
Thanks for your awesome info!!!
I just realized that the behavior I see in the Submit button directive has been there all along, even before I decided to put in the auto-disabling. So I think my issue is with the nature of the beast and cannot be overcome.