HTML Templates Without Frameworks
With no add-on library and in as little at 50 lines of code, you can write HTML that uses JavaScript template string literal notation as your templating language:
<div>
Good morning ${name}. Here are today's tasks.
</div>
<div>
<ul>
${
tasks.reduce((accum,task) =>
{
return accum += el(`${task}`,"li");
},"")
}
</ul>
</div>Although there are two libraries that support using template string literals for front end scripting, hyperHTML and tlx, sometimes you just might want something super light with limited learning overhead.
You may also find the techniques described in this article useful for other projects. One relates to resolving template literals using a specified scope. The other relates to determining if a string represents HTML.
Get AnyWhichWay’s stories in your inbox
Join Medium for free to get updates from this writer.
Here’s the code with comments. If you remove the comments there will be 50 lines that minimized and gzipped are less than .5K. After the code there is some additional commentary.
// convenience function to create elements
const el = (string,tagName,attributes={}) => {
const attrs = Object.values(attributes)
.reduce((accum,[key,val]) => accum += ` ${key}="${val}"`,"");
return `<${tagName}${attrs}>${string}</${tagName}>`;
},
// processes error messages to find undefined property names
getUndefined = error => {
if(error.message.indexOf("not defined")>=0) {
// message is of form <propertyName> not defined
const property = error.message.split(" ")[0];
try { // handles quoted value in Edge
return Function(`return ${property}`)();
} catch(e) { // handles Chrome and Firefox
return property;
}
}
},
// resolves string as template literal with context
resolve = (str,ctx) => {
const extras = {},
// put string in backquotes to turn it into template
body = "with(ctx){with(extras){ return `" + str + "`}}";
// loop until resolution succeeds without error
while(true) {
try { // resolve the template literal body
return Function("ctx","extras",body)(ctx,extras);
} catch(e) { // handle potential undefined references
const variable = getUndefined(e);
if(!variable) throw e;
// create a dummy variable
extras[variable] = "";
}
}
},
// recursively walk node tree, resolve text and attributes
resolveNode = (node,ctx) => {
if(node.nodeType===3) { // text node
const txt = resolve(node.data,ctx),
nodes = new
DOMParser().parseFromString(txt,"text/html").body.childNodes,
children = [].slice.call(nodes);
// see if returned value is parseable as HTML
if(children.some(chld => chld.nodeType===1)) {
// replace the text node with HTML
const parent = node.parentElement;
node.parentElement.removeChild(node);
children.forEach(chld => parent.appendChild(chld))
} else {
node.data = txt;
}
} else if(node.nodeType===1) { // element node
// process attributes
[].slice.call(node.attributes).forEach(attr =>
node.attributes[attr.name] = resolve(attr.value,ctx));
// recursively process children
[].slice.call(node.childNodes).forEach(chld =>
resolveNode(chld,ctx));
}
};The el function exists so that embedded scripts do not contain HTML tags. Including HTML tags in embedded scripts will break functional literals into multiple parts when in the content of an HTML document and produce invalid HTML. For example, instead of this:
${
tasks.reduce((accum,task) =>
{
return accum += `<li>${task}</li>`;
},"")
}do this
${
tasks.reduce((accum,task) =>
{
return accum += el(`${task}`,"li");
},"")
}The resolve function does most of the magic. It dynamically creates and evaluates a template literal within a provided scope by using the powerful but much maligned with construct. There is plenty of commentary elsewhere on with, but I believe the constrained use of with in this situation is warranted. Note, even if your code uses strict mode, with will work because it is referenced in Function which has root scope. If you would like to eliminate with, then change the code to the below and ensure all your embedded templates start their references with the string ctx, e.g ctx.name.
resolve = (str,ctx) => {
// copy so top level of ctx does not get polluted
// by placeholder properties when undefined error occurs
ctx = JSON.parse(JSON.stringify(ctx));
const body = "return `" + str + "`}}";
while(true) {
try {
return Function("ctx",body)(ctx);
} catch(e) {
const variable = getUndefined(e);
if(!variable) throw e;
ctx[variable] = "";
}
}
}The other powerful convention is code in the resolveNode function which checks to see if what was once Text node content in an HTML document has resolved to a string that represents valid HTML. The function parseFromString on a DOMParser does all the work! If any of the child nodes of the body of a document fragment returned by pareFromString are HTML elements, i.e. nodeType===1, then the string is valid HTML.
To resolve the templates in any node of your document, just pass in a the node to resolve and a model.
<script>
const model = {
name: "Joe",
tasks: ["make breakfast for kids",
"get groceries on the way home"]
};
resolveNode(document.getElementById("app"),model)
</script>Below is the rendered result of the example at the start of the article hosted on JSFiddle. I hope you found this useful. Give it a clap if you did. The techniques illustrated in the article were developed while implementing tlx and tlx-chart.







