Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at TechCrunch Disrupt (New York, NY) with: Seth Godin
Ben Nadel at TechCrunch Disrupt (New York, NY) with: Seth Godin@ThisIsSethsBlog )

ng-Template Requests Are Affected By $http Interceptors In AngularJS

By Ben Nadel on

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:


 
 
 

 
 ng-template (and directive templateUrl) requests are affected by $http interceptors. 
 
 
 

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.




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?

Reply to this Comment

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

http://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.

Reply to this Comment

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

Reply to this Comment

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

Reply to this Comment

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.

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.