There are many template engines but my question is: Why do they still exist? Can we not achieve the same job with readily available JavaScript Template Literals?
So influenced from this blogpost, I have come up with something as follows. Do you think this is reasonable in daily JavaScript?
The following code is pure JavaScript. Lets start with a proper data.
var data = { labels: ['ID','Name']
, rows : [ {"id": "1", 'name': "richard"}
, {"id": "2", 'name': "santos"}
]
};
Now let's define a tableTemplate
function which takes the data
and gives us an HTML table.
function tableTemplate(data){
return `
<table>
<tr> ${data.labels.map(label => html`
<th>&${label}</th>`).join("")}
</tr>${data.rows.map(row => html`
<tr>
<td>&${row.id}</td>
<td>&${row.name}</td>
</tr>`).join("")}
</table>`;
}
So what is this html`
thingy and also that &$
?
- First of all
html`
is just a generic tag function that we define only once and without modifying it, we can use it for all of our template functions. &$
on the other hand is simple.&
is just a simple string character and the second one is the escaping$
character for variables in template strings such as${x}
. We can use a tag function to validate our HTML code by escaping invalid HTML characters like&
,>
,<
,"
,'
or`
. The version I give below looks a little convoluted but it's really simple and very useful. It basically escapes invalid HTML characters and provides a "raw" version of the template string.
Please follow above links for further information. Here it is:
function html(lits,...vars){
var esc = { "&": "&"
, ">": ">"
, "<": "<"
, '"': """
, "'": "'"
, "`": "`"
};
return lits.raw
.reduce( (res,lit,i) => (res += lit.endsWith("&") ? lit.slice(0,-1) + [...vars[i].toString()].reduce((s,c) => s += esc[c] || c, "")
: lit + (vars[i] || ""))
, ""
);
}
Now, it's time to put everything together for a working sample. Let's also add <thead>
and <tbody>
tags and some CSS this time.
function html(lits,...vars){
var esc = { "&": "&"
, ">": ">"
, "<": "<"
, '"': """
, "'": "'"
, "`": "`"
};
return lits.raw
.reduce( (res,lit,i) => (res += lit.endsWith("&") ? lit.slice(0,-1) + [...vars[i].toString()].reduce((s,c) => s += esc[c] || c, "")
: lit + (vars[i] || ""))
, ""
);
}
function tableTemplate(data){
return `
<table>
<thead>
<tr>${data.labels.map(label => html`
<th>&${label}</th>`).join("")}
</tr>
</thead>
<tbody>${data.rows.map(row => html`
<tr>
<td>&${row.id}</td>
<td>&${row.name}</td>
</tr>`).join("")}
</tbody>
</table>`;
}
var data = { labels: ['ID','Name']
, rows : [ {"id": 1, 'name': "richard"}
, {"id": 2, 'name': "santos"}
, {"id": 3, 'name': "<ömer & `x`>"}
]
};
document.write(tableTemplate(data));
table {
background-color: LightPink;
border : 0.2em solid #00493E;
border-radius : 0.4em;
padding : 0.2em
}
thead {
background-color: #00493E;
color : #E6FFB6
}
tbody {
background-color: #E6FFB6;
color : #00493E
}
Obviously you can now insert <style>
tag or class="whatever"
attribute into your quasi-HTML template literal in the tableTemplate
function just the way you would normally do in an HTML text file.
Also please note that if you console.log
the resulting HTML text string, thanks to the raw conversion, it would beautifully display (according to your indenting in the tableTemplate
function) just like below:
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>richard</td>
</tr>
<tr>
<td>2</td>
<td>santos</td>
</tr>
<tr>
<td>317</td>
<td><ömer & 'x'></td>
</tr>
</tbody>
</table>
2 Answers 2
There are many template engines but my question is: Why do they still exist? Can we not achieve the same job with readily available JavaScript Template Literals?
Sure, that seems like it would work ok for smaller projects, especially if you remember to keep to the string encoding guidelines to prevent XSS attacks. From my quick glance over your function, I don't see any security issues with how you're encoding strings.
Many templating libraries were popularized before template literals existed. Had template literals existed earlier, I wouldn't be surprised if these libraries would have been made to support them from the start, though, perhaps they continue to not support them because they like keeping the HTML outside of your business logic.
I'll go ahead and point out some potential design considerations you can think about.
It's pretty easy to accidentally forget to precede an interpolated value with an "&" character, which could cause the interface to be buggy, or worse, open up XSS issues. One option would be to make string-escaping the default behavior and require putting a special character before an interpolated value to explicitly state that you trust the interpolated content, and are ok with it getting inserted into the template literal untouched.
Another option would be to have the interpolated values take object literals, which explains what kind of encoding you want to be performed. This could help make things more readable, especially if you want to support other forms of encoding in the future. For example:
html`
<div id="${{ attr: 'All attr-unsafe characters will be encoded' }}">
<p>${{ content: 'All content-unsafe characters will be encoded' }}</p>
${{ raw: 'This string will be added, unchanged' }}
</div>
`
I'm not saying you should or should not switch to one of these options, just pointing out some alternative options to consider.
One more option (this one's my favorite), is to not ever use template literals. For small projects, I like to use this very simple helper function:
function el(tagName, attrs = {}, children = []) {
const newElement = document.createElement(tagName)
for (const [key, value] of Object.entries(attrs)) {
newElement.setAttribute(key, value)
}
newElement.append(...children)
return newElement
}
This allows you to use function calls to construct your HTML content. Because it's using native browser APIs, you won't have to worry about hand-writing escaping logic either, since the browser will automatically escape your data (even still, don't do something stupid like putting untrusted data into a script tag).
function el(tagName, attrs = {}, children = []) {
const newElement = document.createElement(tagName)
for (const [key, value] of Object.entries(attrs)) {
newElement.setAttribute(key, value)
}
newElement.append(...children)
return newElement
}
const renderTable = data =>
el('table', { id: 'my-table' }, [
el('thead', {}, [
el('tr', {}, [
...data.labels.map(renderTableHeader),
]),
]),
el('tbody', {}, [
...data.rows.map(renderRow)
]),
]);
const renderTableHeader = label =>
el('th', {}, [label])
const renderRow = ({ id, name }) =>
el('tr', {}, [
el('td', {}, [id]),
el('td', {}, [name]),
])
var data = { labels: ['ID','Name']
, rows : [ {"id": 1, 'name': "richard"}
, {"id": 2, 'name': "santos"}
, {"id": 3, 'name': "<ömer & `x`>"}
]
};
document.getElementById('main')
.appendChild(renderTable(data))
#my-table {
background-color: LightPink;
border : 0.2em solid #00493E;
border-radius : 0.4em;
padding : 0.2em
}
thead {
background-color: #00493E;
color : #E6FFB6
}
tbody {
background-color: #E6FFB6;
color : #00493E
}
<div id="main"></div>
A short review;
Do you think this is reasonable in daily JavaScript?
Probably not, for a few reasons
You need a function per record type since this wires metadata and styling so tightly
There is a ton of HTML tooling out there, none of it will work with embedded html
The
html
function you provide is compacter but less readable than the one in the blogThe escaping code seems bare, I prefer to use
function escapeHtml(html){ var text = document.createTextNode(html); var p = document.createElement('p'); p.appendChild(text); return p.innerHTML; }
If I had to write a templating routine myself I would have gone with
<table> <head> <tr id="id"></tr> <tr id="name"></tr> </thead> <body> <tr> <td></td> <td></td> </tr> </tbody> </table>
and call something like
tableTemplate(template, {"id":"ID","name":"Name"}, rows);
Which would have mapped the row labels to the proper id, and then put the data in the proper columns based off the location of the corresponding header id.
data
? \$\endgroup\$tableTemplate
function in the working sandbox is exactly the same one as shown in the second snippet. When i try to edit it just turns back into how i had written it and works just fine until i save it and it shows again that silly hash code<th>{cbe533db-4533-4fed-934e-f25c95923264}{row.name}</td>
. What's happening..? Perhaps the$$
is colliding with some parsing library behind the curtain..? \$\endgroup\$$$
with&$
in my code and the sandbox bug is resolved. So.. i guess somebody has to fix something. From this bug only i can conclude that the Stackoverflow and Codereview sandboxes are of different sources or versions of the same source since my code's original form works just fine @ SO. \$\endgroup\$$$
and it worked fine,. Most likely something your end (extension or the like) inserting the GUID \$\endgroup\$edit the above snippet
link and replace&$
s with$$
s in the sandbox everything still works just fine but only after you save it with$$
then things show up awry in the question. \$\endgroup\$