Using Angular as a Template expander

Now we all know AngularJS is an awesome framework to build large Single Page Applications. At the core of angular is a little thing called the HTML Compiler. It takes text as input (most likely from your templates) and transforms it into DOM elements. In the process it also does a bunch of things like:

  • Directive detection
  • Expression interpolation
  • Template expansion
  • Setting up data-bindings, etc.

Let me drive your attention to one area: Template Expansion. I was curious to know if AngularJS could be used purely as a template expander. Something akin to Handlebars or Underscore templates. Is it possible? Can we take that aspect of Angular and use it independently?

Well, it turns out, it’s not that straightforward. But hey, this is an experiment and we are on a path to discover something!

But Why?

A valid question. YMMV: Your Motivations May Vary. For me it was about:

  • Using it as an Isomorphic library on both client and server
  • Use it in a custom control (in non-Angular projects) where you need to expand a template
  • Just for fun
  • Learning something new!

Disclaimer Now, if you were to ask me point-blank: “Should I be doing this in my project?”, I will say “NO”, without blinking an eyelid.

Elements of Angular

Templates are the fundamental building blocks of modern-day web apps. It enables better structuring by keeping your view separate from the domain logic. It also gives you composability where you can compose the UI by combining a set of templates. This is of course better than one giant, monolithic HTML file. So there are real, practical benefits to using templates.

Angular definitely gives you this capability but does so with a few key abstractions:

Let’s see them in turn.

  • $templateRequest: Does the $http request to fetch the template from a URL. Before it does that, it checks the $templateCache if it’s already available. If not, it goes out to get it.
  • $templateCache: This is the cache of all your templates. Why make a second request when you can cache it, right?
  • $compile: Does the hard part of converting text to a function. Calling the function with a scope will generate the jqLite element (DOM)
  • $digest(): The scope from the previous step must have $digest invoked to do the real “template expansion”. The $digest is the necessary syncing mechanism between the scope and the DOM.

Extracting Text

At the end of step 5, we should have a jqLite element bound to the right data from the scope. Now, the reason why we are doing this is to generate a template-expanded text. So we really need the text part.

This can be done by reading the element’s outerHTML property, like so: element[0].outerHTML. Finally, we have what we started out to get. It was a little round-about but we used a template and expanded it to real text by supplying a scope (the context for the template). All with AngularJS.

Few gotchas

I must admit, the above is not the complete picture to generate template-expanded text. In order to use the services such as $templateRequest, $templateCache and $compile, you have to rely on Angular’s $injector. Additonally the scope has to be an instance of the angular Scope. It can’t be a plain JavaScript object! To create a scope you have to rely on $rootScope, which you also get from $injector.

[Aside]: If you try using a simple object, you will see exceptions being thrown. Also we need to call $digest() to bind the data. As you guessed, we can’t do that on a simple js-object.

The code below is a working example of using Angular to expand a template. You can copy and paste it as a snippet in Chrome DevTools, and execute to see the results. Make sure you run on a web-page that uses the AngularJS library. AngularJS.org is a decent bet.

 1 // The injector knows about all the angular services
 2 var injector = angular.element(document).injector();
 3 
 4 // This template could have been fetched with a $templateRequest
 5 // We are inlining it here for this snippet
 6 var template = '<div>{{ ::firstName }} -- {{ ::lastName }}</div>';
 7 
 8 // $compile creates a template-function that can be invoked with the scope
 9 // to expand the template
10 var templateFunction = injector.get('$compile')(template);
11 
12 // Create the scope. 
13 // Note: this has to be a real angular Scope and not a plain js-object
14 var scope = injector.get('$rootScope').$new();
15 
16 // Set some properties
17 scope.firstName = 'Pavan';
18 scope.lastName = 'Podila';
19 
20 var element = templateFunction(scope);
21 scope.$digest();
22 
23 // Remove the noise around the generated element. This can be disabled
24 // by configuring the $compileProvider.debugInfoEnabled()
25 // Here we take the easy way out
26 element.removeClass('ng-scope ng-binding');
27 
28 // -------- GRAND FINALE ---------
29 // The expanded template as TEXT
30 // -------------------------------
31 var expandedText = element[0].outerHTML; 
32 
33 // Output: 
34 // <div class="">Pavan -- Podila</div>

Grey Matter

As you can see, it is quite round about. Definitely not suggested for a real project. If you need such capability, you are better off with Handlebars or Underscore templates. But if you have read this far, hopefully you have put an additional fold in your Angular grey matter!