Select Page

The Goal

This is the sequel to my AngularJS Dynamic Templates – Yes We Can! post which, to my surprise, has received a fair amount of attention. The number one question I get regarding that post is “This is great! But! How can I do this with remote templates?” and so I wanted to finally address that question with a way to compile dynamic templates using remote resources. I also updated the repository to AngularJS 1.3 as it was originally built on AngularJS 1.0.1! Yikes! Welcome to the future!

Before we get started, I want to make it clear that there are a few other ways to do this such as using ng-switch, ng-if, etc. I think that is fine if you have a few templates but if would turn into a DOM nightmare if you had to dynamically render like 40 templates. Also, using $compile to cook your AngularJS templates is pretty low level and so it gives you the ability to dynamically compose some pretty interesting layouts. For instance, you could dynamically pull a header template, body template and footer template based on some business logic and then dynamically compile them into a single template at runtime. As with all my posts, my goal is to show one possible way of doing something so that you have one more tool in your toolbox to build awesome things.

With that said, grab the code, check out the demo, and let’s roll!

Demo Code

The Template Service

The first thing we need to do is to set up a service to retrieve our remote templates. We will create a service called TemplateService with a single method called getTemplates.

app.constant('URL', 'data/');

app.factory('TemplateService', function ($http, URL) {
    var getTemplates = function () {
        return $http.get(URL + 'templates.json');
    };

<pre><code>return {
    getTemplates: getTemplates
};
</code></pre>
});

And in the getTemplates method, we are going to return the results of $http.get, which will be the contents of templates.json, which we can see in an abbreviated form below:

{
  "imageTemplate": "

<

div class='entry-photo'>...",
  "videoTemplate": "

<

div class='entry-video'>...",
  "noteTemplate": "

<

div class='entry-note'>..."
}

Content Item Directive

Now that we have created a way to pull in the remote templates, we need to consume them in the contentItem directive. We will start out by injecting the TemplateService into the directive and then calling TemplateService.getTemplates in the linker function.

Upon completion of the call, we are assigning response.data to our templates variable to be passed into the getTemplate method. We are also passing the content type of the current content item as a second parameter to getTemplate via scope.content.content_type.

app.directive('contentItem', function ($compile, TemplateService) {
    //...

<pre><code>var linker = function (scope, element, attrs) {
    scope.rootDirectory = 'images/';

    TemplateService.getTemplates().then(function (response) {
        var templates = response.data;

        element.html(getTemplate(templates, scope.content.content_type));

        $compile(element.contents())(scope);
    });
};

//...
</code></pre>
});

We are then taking the result of getTemplate (which is essentially a string template) and inserting it into the element that our contentItem directive was declared on. We have inserted the template into the element, but up until this point it is hasn’t been compiled into a ‘living, breathing’ AngularJS template. This is where the $compile service comes in as we can take HTML and a scope object and ‘zip’ them up to breathe life into our template. The vital line of code to make this happen is $compile(element.contents())(scope).

This technique also translates really well to testing when you want to spin up directives and templates and bind them to $scope.

element = angular.element('

<div my-directive></div>
');
$compile(element)($rootScope)
expect(element.scope().cube(3)).toBe(27);

And just to lock this down, we will take a quick look at the getTemplate method. This is a pretty straightforward switch statement that returns the appropriate template, although this could be condensed quite a bit if someone were so inclined.

app.directive('contentItem', function ($compile, TemplateService) {
    var getTemplate = function (templates, contentType) {
        var template = '';

<pre><code>    switch (contentType) {
        case 'image':
            template = templates.imageTemplate;
            break;
        case 'video':
            template = templates.videoTemplate;
            break;
        case 'notes':
            template = templates.noteTemplate;
            break;
    }

    return template;
};
//...
</code></pre>
});

Strict Contextual Escaping

By upgrading to AngularJS 1.3, we need to turn one more dial to get this working. As of AngularJS 1.2, strict contextual escaping was turned on by default meaning that certain bindings had to be explicitly marked as safe by the developer. If you are dealing with a potentially sensitive situation then it is important to take this seriously but in our case we are just going to sidestep it by whitelisting all domains as safe.

In our config block, we accomplish this by adding in this line $sceDelegateProvider.resourceUrlWhitelist([‘self’, ‘**’]);.

app.config(function ($sceDelegateProvider) {
    $sceDelegateProvider.resourceUrlWhitelist(['self', '**']);
});

And that is it! We are now loading our templates via a remote JSON file and compiling them into our application at runtime.

Bonus: Remote HTML Templates

Since we have come this far, let us go for a bonus round by showing how to do this using HTML templates. If you hop on to the remote-files branch of the repository, you will find the code below.

We are going to modify the TemplateService to return the actual HTML template itself. We have an HTML template in the templates directory and we are using the content parameter to generate the endpoint URL for the template.

app.factory('TemplateService', function ($http) {
    var getTemplate = function (content) {
        return $http.get('templates/' + content + '.html');
    };

<pre><code>return {
    getTemplate: getTemplate
};
</code></pre>
});

From here, we are calling element.html and passing in response.data which is the markup from our TemplateService call.

app.directive('contentItem', function ($compile, TemplateService) {
    var linker = function (scope, element, attrs) {
        scope.rootDirectory = 'images/';

<pre><code>    TemplateService.getTemplate(scope.content.content_type).then(function (response) {
        element.html(response.data);
        $compile(element.contents())(scope);
    });
};

//...
</code></pre>
});

And then $compile! #missionAccomplished.

Review

In a nutshell, we are doing three things to get remote templates to work.

  1. Retrieve the remote templates via the TemplateService.
  2. Inserting the correct template into the directive’s element by calling element.html.
  3. Call $compile to ‘zip’ up the new HTML with the directive’s scope object.

You will also need to dial in strict context escaping depending on your needs.

Let me know what you think in the comments below and if someone is up for the challenge, I would love to see a totally dynamic version where you would not have to update the directive every time you added a new content type.

Resources

$compile Documentation
https://docs.angularjs.org/api/ng/service/$compile

$sce Documentation
https://docs.angularjs.org/api/ng/service/$sce

Demo Code