Understanding Handlebars.js and Content Security Policies
If you are changing your website’s Content Security Policy(CSP) to disable unsafe-eval, or if unsafe-eval is already disabled, you might notice the following error in your Console when trying to use Handlebars.js
EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self’
This is because Handlebars uses Function()
which is gated by the CSP, and we can solve this error by using precompiled templates.
Setting Up a Basic Handlebars Demo
For this blog, we’re just going to set up a simple Handlebars demo. You can install handlebars by including the js file from CDN
<script src="https://s3.amazonaws.com/builds.handlebarsjs.com/handlebars.min-v4.7.8.js"></script>
Note that the file has the compiler + runtime. The compiler has the Function()
which won’t work with the CSP so later on, we will be switching to just the runtime file.
Make a file called index.html
, put the following code
<div class="handlebar-demo"></div>
<script id="handlebars-demo" type="text/x-handlebars-template"></script>
<script src="index.js"></script>
Create an index.js
file and paste the following code
function handlebarTest() {
var template = $('#handlebars-demo').html();
var context = { "name": "Gorman Law", "occupation": "developer" };
//Compile the template data into a function
var templateScript = Handlebars.compile(template);
var html = templateScript(context);
$('.handlebar-demo').html(html);
}
handlebarTest();
If done correctly, you should see the following
Adding a CSP
In the <head>
of your html, add the following code
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' *; script-src 'self'"> -->
This should result in the following errors in your console.
Remember this code from above? var templateScript = Handlebars.compile(template);
we are going to change it to precompile the template.
Precompiling Handlebars Templates
The Handlebars website says it’s more efficient to compile templates beforehand. You can include the smaller runtime, and the browser will be faster because it doesn’t have to compile them on the fly. A win-win all around.
-
Install Handlebars npm in your working directory
npm install handlebars
-
Create a new file in the same directory called
demo.hbs
. Cut the<script>
from yourindex.html
file and paste it into your new file. Remove the<script>
tags
<div>
My name is {{name}}. I am a {{occupation}}.
</div>
-
Open up Terminal, and navigate to your working directory, then run the following command
handlebars demo.hbs -f demo.precompiled.js
A new file called demo.precompiled.js should appear in your directory, and if you open it up, it should contain the following code.
(function() {
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['demo'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) {
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return parent[propertyName];
}
return undefined
};
return "<div>\r\n My name is "
+ alias4(((helper = (helper = lookupProperty(helpers,"name") || (depth0 != null ? lookupProperty(depth0,"name") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data,"loc":{"start":{"line":2,"column":15},"end":{"line":2,"column":23}}}) : helper)))
+ ". I am a "
+ alias4(((helper = (helper = lookupProperty(helpers,"occupation") || (depth0 != null ? lookupProperty(depth0,"occupation") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"occupation","hash":{},"data":data,"loc":{"start":{"line":2,"column":32},"end":{"line":2,"column":46}}}) : helper)))
+ ". \r\n</div>";
},"useData":true});
})();
-
Include the precompiled js file in
index.html
file<script src="demo.precompiled.js"></script>
-
Change the handlebarTest function inside index.js file
function handlebarTest() {
var context = { "name": "Gorman Law", "occupation": "developer" };
//template no longer needs to be compiled!
// Notice the templates['demo'] in the precompiled.js file. You can access it
// with Handlebars.templates.demo or Handlebars.templates['demo']
var template = Handlebars.templates.demo;
var html = template(context);
$('.handlebar-demo').html(html);
}
- Change the Handlebars source to the runtime version
<script src="[https://s3.amazonaws.com/builds.handlebarsjs.com/handlebars.min-v4.7.8.js](https://s3.amazonaws.com/builds.handlebarsjs.com/handlebars.runtime.min-v4.7.8.js)"></script>
After following all these steps, you should be able to see your Handlebars rendering appear on your page again, and no more errors in the console!
Using Gulp
If you have a lot of templates, manually compiling each one can be time consuming, and make the project bloated with so many precompiled JS files. Gulp can compile all the templates into one file for you.
First, install the dependencies
npm install --save-dev gulp-handlebars gulp-wrap gulp-declare gulp-concat
gulp-wrap
and gulp-declare
are going to be used to declare template namespaces and make templates available for use in the browser
Let’s pretend this is our working directory structure
├── gulpfile.js // gulpfile
├── index.html // html file
├── index.js // js file
└── templates/ // folder with all our hbs files for precompilation
└── demo.hbs // demo handlebar template from above
└── precompiled/ // folder for our precompiled template js
└── templates.js // our gulp output file
Enter the following in your gulpfile
var gulp = require('gulp');
var handlebars = require('gulp-handlebars');
var wrap = require('gulp-wrap');
var declare = require('gulp-declare');
var concat = require('gulp-concat');
gulp.task('templates', function() {
return gulp
.src('templates/*.hbs') //will compile every file with .hbs extension
.pipe(handlebars())
.pipe(wrap('Handlebars.template(<%= contents %>)'))
.pipe(declare({
namespace: 'Example.templates',
noRedeclare: true, // Avoid duplicate declarations
}))
.pipe(concat('templates.js'))
.pipe(gulp.dest('precompiled/'));
});
gulp.task('default', gulp.series('templates'));
If you run your gulpfile, you should notice that templates.js
is now in your precompiled folder! Opening it up you should see this:
this["Example"] = this["Example"] || {};
this["Example"]["templates"] = this["Example"]["templates"] || {};
this["Example"]["templates"]["demo"] = Handlebars.template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) {
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return parent[propertyName];
}
return undefined
};
return "<div>\r\n My name is "
+ alias4(((helper = (helper = lookupProperty(helpers,"name") || (depth0 != null ? lookupProperty(depth0,"name") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data,"loc":{"start":{"line":2,"column":15},"end":{"line":2,"column":23}}}) : helper)))
+ ". I am a "
+ alias4(((helper = (helper = lookupProperty(helpers,"occupation") || (depth0 != null ? lookupProperty(depth0,"occupation") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"occupation","hash":{},"data":data,"loc":{"start":{"line":2,"column":32},"end":{"line":2,"column":46}}}) : helper)))
+ ". \r\n</div>";
},"useData":true});
Here’s what we need to change to make your page work again
-
Inside index.html, change the .js include from
<script src="demo.precompiled.js"></script>
to
<script src="precompiled/templates.js"></script>
- Inside your index.js file, change the function again to this
function handlebarTest() {
var context = { "name": "Gorman Law", "occupation": "developer" };
// Notice how the templates['demo'] changed to this["Example"]["templates"]["demo"].
// Here's how we can use the new namespace.
var template = Example.templates.demo;
var html = template(context);
$('.handlebar-demo').html(html);
}
You should now see the page re-appear again!
Achieving Compatibility With Handlebars and CSP
To get around unsafe-eval for scripts, we have to use precompiled-templates for Handlebars.js. I hope this blog post helped you get your templates up and running fast!