Variable templates for an AngularJS directive
Sometimes web services and APIs return different data sets for the same type of object. For example, the tumblr API will return a list of posts, but each post type has different data associated with it: text posts have a title and body, while photo posts have captions and images.
Since having different components and directives for each post type doesn't make much sense (especially when they're all displayed in one stream, as on tumblr), it's beneficial to be able to conditionally load a template based on the post type of each individual post.
This code is based on these two articles/posts:
https://github.com/angular/angular.js/issues/1039#issuecomment-10673465
http://onehungrymind.com/angularjs-dynamic-templates/
and here is the tumblr API for reference: http://www.tumblr.com/docs/en/api/v2
components.js
angular.module('components', []).
directive('tumblrPost', ['$compile', '$http', '$templateCache', function($compile, $http, $templateCache) {
var getTemplate = function(contentType) {
var templateLoader,
baseUrl = '/templates/components/tumblr/',
templateMap = {
text: 'text.html',
photo: 'photo.html',
video: 'video.html',
quote: 'quote.html',
link: 'link.html',
chat: 'chat.html',
audio: 'audio.html',
answer: 'answer.html'
};
var templateUrl = baseUrl + templateMap[contentType];
templateLoader = $http.get(templateUrl, {cache: $templateCache});
return templateLoader;
}
var linker = function(scope, element, attrs) {
var loader = getTemplate(scope.post.type);
var promise = loader.success(function(html) {
element.html(html);
}).then(function (response) {
element.replaceWith($compile(element.html())(scope));
});
}
return {
restrict: 'E',
scope: {
post:'='
},
link: linker
};
}]);
news.html (all posts view)
<h1 class="road">News</h1>
<ul id="news">
<li ng-repeat="tumblrPost in posts">
<tumblr-post post="tumblrPost"></tumblr-post>
</li>
</ul>
<p class="read-more"><a href="http://{{posts[0].blog_name}}.tumblr.com/" target="_blank">Read more news</a></p>
text.html (tumblr text post component)
<a href="{{post.post_url}}" target="_blank" class="post-date">{{post.date}}</a>
<h3>{{post.title}}</h3>
<div ng-bind-html="post.body"></div>
<hr>
video.html (tumblr video post component, for comparison)
<a href="{{post.post_url}}" target="_blank" class="post-date">{{post.date}}</a>
<div ng-bind-html-unsafe="post.player[0].embed_code" ui-jq="fitVids"></div>
<div ng-bind-html="post.caption"></div>
<hr>
Written by Devers
Related protips
11 Responses
Hi Devers,
Thanks for your nice example.
It's great helpful to me.
But, if I don't want to replace the whole html of element, how to?
I have try to using "element.html($compile(element.html())(scope))"
But it doesn't work.
Could you give me some hint?
Thank you.
Hi Devers,
I have knew how to do.
I just using "$compile(element.contents())(scope)" to replace "element.replaceWith($compile(element.html())(scope))"
Thank you.
Nice article, I was just working on something like this and this is showing me that Im on the right path.
Fran
Good read, I really like this pattern. It's pretty sweet!
Very useful, thanks ;)
Nice writeup. I'm making an angular directive where I can set the dynamic template. It works. Now I would love to update the template using broadcast events. The code works fine [1] and the new HTML is generated from $compile. The problem is that the directive does not 'refresh' the content. Any idea?
My Html binding for video template is not working Rest works like a charm :(
Using element for accessing the DOM e.g element.find(".child-class") does not work because of the element.replaceWith(). Any cleaner solution?
Nevermind. We can do something like this:
var compiled = $compile(response.data)(scope); element.replaceWith(compiled); element = compiled; </code> </pre> Thanks for the nice article though.
Awesome example! Why are you doing both success and then? Looks like almost the same is happening in there?
Thank you! This really helped me.