A vanilla JS alternative to HandlebarsJS and MustacheJS
Handlebars and Mustache are JavaScript templating systems. They let you pass in variables (wrapped in double curly brackets: {{someVariable}}
) that get replaced with real content.
<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{{body}}}
</div>
</div>
<div class="entry">
<h1>Hello, world!</h1>
<div class="body">
<p>You look nice today!</p>
</div>
</div>
Today, I wanted to show you a lightweight vanilla JS alternative.
placeholders.js
Placeholders.js is a scant 150 bytes (that is, a fraction of a KB) after minification and gzipping.
First, I’m going to show you how to use it, then I’ll show you how it works under the hood.
Psst… placeholder.js pairs quite nicely with Reef.js templates.
How to use it
First, provide data as an object of keys and values. You can use nested objects if desired. Then, setup a template as a string (or function that returns a string).
In that template, use variables wrapped in double curly brackets ({{variable}}
) as placeholders for the key you want to replace it with from your data. Use dot notation for nested objects.
var data = {
greeting: 'Hello',
name: 'World',
time: new Date().toLocaleTimeString(),
details: {
mood: 'happy',
food: 'a turkey sandwich'
}
};
var template = function () {
return '{{greeting}}, {{name}}! It is currently {{time}}. {{not.exist}} You are {{details.mood}} to eat {{details.food}}.';
};
To replace your placeholders with real content from your data, pass both the template and the data into the placeholders()
function to get back a new string.
<div id="app"></div>
var app = document.querySelector('#app');
app.innerHTML = placeholders(template, data);
Here’s a demo you can play with.
How it works
First, I check to see if the template
variable is a string or function.
If it’s a function, I convert it into a string. Then I check to make sure there’s a valid template, and if not, throw an error.
/**
* Replaces placeholders with real content
* @private
* @param {String} template The template string
* @param {String} local A local placeholder to use, if any
*/
var placeholders = function (template, data) {
'use strict';
// Check if the template is a string or a function
template = typeof (template) === 'function' ? template() : template;
if (['string', 'number'].indexOf(typeof template) === -1) throw 'PlaceholdersJS: please provide a valid template';
};
Next, I check to make sure data was provided, and if not, return the template as-is.
/**
* Replaces placeholders with real content
* @private
* @param {String} template The template string
* @param {String} local A local placeholder to use, if any
*/
var placeholders = function (template, data) {
'use strict';
// Check if the template is a string or a function
template = typeof (template) === 'function' ? template() : template;
if (['string', 'number'].indexOf(typeof template) === -1) throw 'PlaceholdersJS: please provide a valid template';
// If no data, return template as-is
if (!data) return template;
};
Now, we need to replace the placeholders in the template with content from the data
object.
We’ll use the replace()
method to find our variables, and pass in a callback function to handle replacing them. This pattern will match double curly brackets only if they have something between them.
/\{\{([^}]+)\}\}/g
The matched item gets passed into the callback function as an argument. We’ll look at how to handle that in a second.
Once we’re done replacing things, we can return the template
string.
/**
* Replaces placeholders with real content
* @private
* @param {String} template The template string
* @param {String} local A local placeholder to use, if any
*/
var placeholders = function (template, data) {
'use strict';
// Check if the template is a string or a function
template = typeof (template) === 'function' ? template() : template;
if (['string', 'number'].indexOf(typeof template) === -1) throw 'PlaceholdersJS: please provide a valid template';
// If no data, return template as-is
if (!data) return template;
// Replace our curly braces with data
template = template.replace(/\{\{([^}]+)\}\}/g, function (match) {
// Do something with our matched placeholders...
console.log(match);
});
return template;
};
How replacing content works
If you log each matched item in the callback, you’ll get the full placeholder item.
// Replace our curly braces with data
template = template.replace(/\{\{([^}]+)\}\}/g, function (match) {
console.log(match);
});
// logs {{greeting}}, {{name}}, {{time}}, etc...
First, we need to remove the leading and trailing curly brackets. We’ll assign a new string to match
, using the slice()
method to get a substring of the original.
// Replace our curly braces with data
template = template.replace(/\{\{([^}]+)\}\}/g, function (match) {
// Remove the wrapping curly braces
match = match.slice(2, -2);
});
The way we handle each placeholder will differ depending on whether it’s a nested object or not.
We’ll split the match
into an array with the split()
method, using the dot (.
) as our delimiter. If there’s more than one item, the placeholder is for a nested object. Otherwise, it’s a top-level key in the data
object.
// Replace our curly braces with data
template = template.replace(/\{\{([^}]+)\}\}/g, function (match) {
// Remove the wrapping curly braces
match = match.slice(2, -2);
// Check if the item has sub-properties
var sub = match.split('.');
// If the item has a sub-property, loop through until you get it
if (sub.length > 1) {
// Do something...
}
// Otherwise, return the item
else {
// Do something else...
}
});
If it’s a top-level key, we’ll make sure the item exists in the data
object. If not, we’ll return the placeholder back as-is (with the curly brackets added back on). Otherwise, we’ll return it’s value in the data
object.
// Replace our curly braces with data
template = template.replace(/\{\{([^}]+)\}\}/g, function (match) {
// Remove the wrapping curly braces
match = match.slice(2, -2);
// Check if the item has sub-properties
var sub = match.split('.');
// If the item has a sub-property, loop through until you get it
if (sub.length > 1) {
// Do something...
}
// Otherwise, return the item
else {
if (!data[match]) return '{{' + match + '}}';
return data[match];
}
});
If the match
is a nested object (as in, it used dot notation like {{details.mood}}
), we need to loop through each item in the sub
array and find it’s match in the data
object.
We’ll create a temp
variable, and initially assign the data
object to it.
For each item in sub
, we’ll make sure the item
exists in temp
. If not, we’ll return back the original match, with curly brackets added back.
Otherwise, set temp
to the current items value in whatever object temp
currently is. This approach let’s us dig multiple levels deep into a nested object.
Once done looping, we can return temp
, which will now be the value of the nested object key.
// Replace our curly braces with data
template = template.replace(/\{\{([^}]+)\}\}/g, function (match) {
// Remove the wrapping curly braces
match = match.slice(2, -2);
// Check if the item has sub-properties
var sub = match.split('.');
// If the item has a sub-property, loop through until you get it
if (sub.length > 1) {
var temp = data;
sub.forEach(function (item) {
// Make sure the item exists
if (!temp[item]) {
temp = '{{' + match + '}}';
return;
}
// Update temp
temp = temp[item];
});
return temp;
}
// Otherwise, return the item
else {
if (!data[match]) return '{{' + match + '}}';
return data[match];
}
});
Browser Compatibility
Placeholders.js works in all modern browsers, and IE9 and up.