Javascript template engine in just 20 lines of code
February, 2019
Nowadays we don't think about this stuff but back in the days filling markup with data was not a simple task. Today frameworks like React fill the gap and it is quite easy to render content from a given HTML. This article explains how to build your own template engine with just a few lines of JavaScript. Even though you probably don't need such logic it is interesting to see how small this could be.
We will go simple and will start by having a function that accepts a string and an object. The string will be HTML that may have or not placeholders for the data coming from the second argument of the function. Which we will define as key-value pairs.
Here is what we could have in the beginning:
var TemplateEngine = function(tpl, data) {
// magic goes here
}
var template = '<p>Hello, my name is <%name%>. I am <%age%> years old.</p>';
console.log(TemplateEngine(template, {
name: "Krasimir",
age: 34
}));
var TemplateEngine = function(tpl, data) {
// magic goes here
}
var template = '<p>Hello, my name is <%name%>. I am <%age%> years old.</p>';
console.log(TemplateEngine(template, {
name: "Krasimir",
age: 34
}));
As you may guess, the result which we want to achieve at the end is:
<p>Hello, my name is Krasimir. I'm 29 years old.</p>
Replacing the placeholders
The very first thing which we have to do is to take the dynamic blocks inside the template. Later we will replace them with the real data passed to the engine. I decided to use regular expression to achieve this. That's not my strongest part, so feel free to comment and suggest a better RegExp.
let re = /<%([^%>]+)?%>/g;
We will catch all the pieces which start with *<%* and end with %>. The flag g (global) means that we will get not one, but all the matches. There are a lot of methods which accept regular expressions. However, what we need is an array containing the strings inside our data blocks. That's what exec does.
let re = /<%([^%>]+)?%>/g;
let match = re.exec(tpl);
If we console.log the match variable we will get:
[
"<%name%>",
" name ",
index: 21,
input:
"<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>"
]
We got the data, but as you can see the returned array has only one element. And we need to process all the matches. To do this we should wrap our logic into while loop.
let re = /<%([^%>]+)?%>/g, match;
while(match = re.exec(tpl)) {
tpl = tpl.replace(match[0], data[match[1]])
}
If you run the code above you will see that the both <%name%> and <%age%> are shown.
Now it gets interesting. We have to replace placeholders with the real data passed to the function. The most simple thing which we can use is to use .replace method against the template. We should write something like this:
var TemplateEngine = function(tpl, data) {
let re = /<%([^%>]+)?%>/g, match;
while(match = re.exec(tpl)) {
tpl = tpl.replace(match[0], data[match[1]])
}
return tpl;
}
Enhancing our logic to support more complex data structures
Ok, the code so far works, but it is not enough. We have really simple object and it is easy to use data["property"]. But in practice we may have complex nested objects. Let's for example change our data to
{
name: "Krasimir Tsonev",
profile: { age: 29 }
}
This doesn't work because when if we type <%profile.age%> we will get data["profile.age"] which is actually undefined. So, we need something else. The .replace method will not work in this case. The very best thing will be to put real JavaScript code between <% and %>. It will be nice if it is evaluated against the passed data. For example:
var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';
How is this possible? Well, by using the new Function syntax. I.e. creating a function from strings. Let's see an example.
var fn = new Function("arg", "console.log(arg + 1);");
fn(2); // outputs 3
fn is a real function which takes one argument. Its body is console.log(arg + 1);. In other words the above code is equal to:
We are able to define a function, its arguments and its body from strings. That's exactly what we need. But before to create such function we need to construct its body. The method should return the final compiled template. Let's get the string used so far and try to imagine how it will look like.
return
"<p>Hello, my name is " +
this.name +
". I\'m " +
this.profile.age +
" years old.</p>";
For sure, we will split the template into text and meaningful JavaScript. As you can see above we may use a simple concatenation and produce the desired result. However, this approach doesn't align completely with our needs. Because we are passing working JavaScript sooner or later we will want to make a loop. For example:
var template =
'My skills:' +
'<%for(var index in this.skills) {%>' +
'<a href=""><%this.skills[index]%></a>' +
'<%}%>';
If we use concatenation the result will be:
return
'My skills:' +
for(var index in this.skills) { +
'<a href="">' +
this.skills[index] +
'</a>' +
}
Of course this will produce an error. We have to put all the strings in an array and join its elements at the end.
let r = [];
r.push('My skills:');
for(var index in this.skills) {
r.push('<a href="">');
r.push(this.skills[index]);
r.push('</a>');
}
return r.join('');
So, back to our TemplateEngine function. The next logical step is to collect the different lines for our customly generated function. We already have some information extracted from the template. We know the content of the placeholders and their position. By using a helper variable (cursor) we will be able to produce the desired result.
The code variable holds the body of the function. It starts with definition of the array. As we said, cursor shows us the current position in the template. We need such a variable to go through the whole string and skip the data blocks. An additional add function is created. Its job is to append lines to the code variable. And here is something tricky. We need to escape the double quotes, because otherwise the generated script will not be valid. If we run that example and check the console we will see:
var r=[];
r.push("<p>Hello, my name is ");
r.push("this.name");
r.push(". I'm ");
r.push("this.profile.age");
return r.join("");
Hm ... not what we wanted. this.name and this.profile.age should not be quoted. A little improvement of the add method solves the problem.
The placeholders' content is passed along with a boolean variable. Now this generates the correct body.
var r=[];
r.push("<p>Hello, my name is ");
r.push(this.name);
r.push(". I'm ");
r.push(this.profile.age);
return r.join("");
All we need to do is to create the function and execute it. At the end of our template engine, instead of returning tpl:
return new Function(
code.replace(/[\r\t\n]/g, '')
).apply(data);
We don't need to send any arguments to the function. We use the apply method to call it. It automatically sets the scope. That's the reason of having this.name working. The this actually points to our data.
We are almost done. One last thing. We need to support more complex operations, like if/else statements and loops. Let's get the same example from above and try the code so far.
var template =
'My skills:' +
'<%for(var index in this.skills) {%>' +
'<a href="#"><%this.skills[index]%></a>' +
'<%}%>';
console.log(TemplateEngine(template, {
skills: ["js", "html", "css"]
}));
The result is an error Uncaught SyntaxError: Unexpected token. If we debug a bit and print out the code variable we will see the problem.
var r=[];
r.push("My skills:");
r.push(for(var index in this.skills) {);
r.push("<a href=\"\">");
r.push(this.skills[index]);
r.push("</a>");
r.push(});
r.push("");
return r.join("");
The line containing the for loop should not be pushed to the array. It should be just placed inside the script. To achieve that we have to make one more check before to attach something to code.
let re = /<%([^%>]+)?%>/g,
reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
code = 'var r=[];\n',
cursor = 0,
match;
let add = function(line, js) {
if (js) {
code += line.match(reExp) ?
line + '\n' :
'r.push(' + line + ');\n';
} else {
code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
}
}
A new regular expression is added. It tells us if the javascript code starts with if, for, else, switch, case, break, { or }. If yes, then it simply adds the line. Otherwise it wraps it in a push statement. The result is:
var r=[];
r.push("My skills:");
for(var index in this.skills) {
r.push("<a href=\"#\">");
r.push(this.skills[index]);
r.push("</a>");
}
r.push("");
return r.join("");
Now everything is properly compiled.
My skills:<a href="#">js</a><a href="#">html</a><a href="#">css</a>
The latest fix gives us a lot of power actually. We may apply complex logic directly into the template. For example:
var template =
'My skills:' +
'<%if(this.showSkills) {%>' +
'<%for(var index in this.skills) {%>' +
'<a href="#"><%this.skills[index]%></a>' +
'<%}%>' +
'<%} else {%>' +
'<p>none</p>' +
'<%}%>';
We may cheat a little bit at the end and remove some new lines to make the function even smaller and finish with the impressive 15 lines of code: