Router: Dynamic Templates
Sat Aug 15, 2015
This post discusses how to create dynamic templates by leveraging the templateProvider configuration provided by Angular’s built-in router or the third-party UI Router.
PROBLEM
For Single Page Applications (SPAs), we often need to switch views or states inside containers. This is usually done through routers. With either Angular’s built-in router or the popular UI Router, we are able to define the relationship between states and their templates. For instance, here we defined a state home
and its template URL app/home/home.html
:
app.config(function($stateProvider) {
$stateProvider.state('home', {
url: '/',
templateUrl: 'app/home/home.html'
});
});
In some cases, this state-to-template relationship can not be determined beforehand at the config
time. The decision of what the template or template URL will be used for a state has to wait for the availability of run-time data. For example:
- User’s account type, e.g. show Home version A for members, and version B for public users.
- A/B testing, e.g. a A/B testing service randomly picks from two versions – A or B.
In either scenario, the template cannot be fixed to app/home/home.html
, and has be to resolved using run-time data.
Router’s templateUrl
configuration accepts a function which can be used to create dynamic template URL. However, we are not able to inject run-time dependencies (e.g. user services, or A/B test services) into the templateUrl function. The only available argument of the templateUrl
function is $stateParams
.
$stateProvider.state('home', {
templateUrl: function($stateParams) { // Can not inject dependencies
return 'app/home.' + $stateParams.option + '.html';
}
});
SOLUTION
The answer is templateProvider
.
Both Angular built-in router and the UI Router have a templateProvider
configuration. templateProvider
accepts a function that can be injected with run-time dependencies.
$stateProvider.state('home', {
templateProvider: function(abTestService) { // abTestService is injected here
var result = abTestService.pick('a', 'b'); // Choose version A or B
return '...'; // Return template content based on the result
}
});
templateProvider
returns template content (not an URL to the template). We can certainly embed HTML markups directly in JavaScript, but for complicate HTML, it’s better to externalize the HTML content to separate template files. Here, we created home-a.html
and home-b.html
, and ngInclude
them in the templateProvider function:
<!-- Home version A at app/home/home-a.html -->
<div ng-controller="HomeAController">Version A</div>
<!-- Home version B at app/home/home-b.html -->
<div ng-controller="HomeBController">Version B</div>
$stateProvider.state('home', {
templateProvider: function(abTestService) {
var result = abTestService.pick('a', 'b');
// ngInclude template content based on the A/B test result
return '<div ng-include="\'app/home/home-' + result + '.html\'"></div>';
}
});
templateProvider
can also return a Promise which is resolved to template content.
$stateProvider.state('home', {
templateProvider: function($http, USER_SERVICE_REST_URL) {
// Here, we return a promise instead of the template content
return $http.get(USER_SERVICE_REST_URL).then(function(data) {
var result = (data.type === 'member' ? 'a' : 'b');
// Return the template content
return '<div ng-include="\'app/home/home-' + result + '.html\'"></div>';
});
}
});
EVEN BETTER SOLUTION
Having ngInclude
in templateProvider
function feels still a bit hackish to me. The ideal solution is to specify a template URL, and then let Angular fetch the content. However, sending separate HTTP requests just to fetch templates seems to be unnecessary web traffic. It will be better if the template content can be cached in the $templateCache service; and then, all I need to do is $templateCache.get('templateUrl')
:
$stateProvider.state('home', {
templateProvider: function(abTestService, $templateCache) {
var result = abTestService.pick('a', 'b');
// Retrieve the cached template content from $templateCache service
return $templateCache.get('app/home/home-' + result + '.html');
}
});
To achieve this, we need a Gulp task to convert all HTML files under the app/ directory to JavaScript strings, and save the strings in $templateCache.
// Load gulp and its plugins
var gulp = require('gulp');
var minifyHtml = require('gulp-minify-html');
var angularTemplateCache = require('gulp-angular-templatecache');
gulp.task('templates', function() {
return cacheTemplates('src/app/**/*.html', 'app.template.js');
function cacheTemplates(input, output) {
return gulp.src(input) // Get all HTML files
.pipe(minifyHtml({ // Minify HTML content first
empty: true,
spare: true,
quotes: true
}))
.pipe(angularTemplateCache(output, { // Save minified strings to cache
module: 'myApp' // Setup $templateCache for Angular module 'myApp'
}))
.pipe(gulp.dest('.tmp/templates/'));
} // /function cacheTemplates
});
Then, import the generated template.js
in index.html
:
<script src=".tmp/templates/app.template.js"></script>
CONCLUSION
By leveraging the templateProvider
function that can be injected with dependencies, we are able to resolve template content based on run-time data. This technique is useful for switching among more than one templates for a state, for instance, A/B testing, and swappable content in limited space.