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.
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
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.
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 your index.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);
}
<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!
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>
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!
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!