Skip to content

Instantly share code, notes, and snippets.

@fragsalat
Created February 22, 2017 13:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fragsalat/a62da9ba9ce5996ca05164672846ecfb to your computer and use it in GitHub Desktop.
Save fragsalat/a62da9ba9ce5996ca05164672846ecfb to your computer and use it in GitHub Desktop.
Small template renderer which acts like aurelia's template engine
const INTERPOLATION_START = '\\${';
const INTERPOLATION_END = '}';
const INTERPOLATION_EXPRESSION = new RegExp(`(${INTERPOLATION_START}(.*?)${INTERPOLATION_END})`, 'g');
class TemplateEngine {
constructor(template) {
this.baseTemplate = template;
}
render(data) {
if (this.baseTemplate) {
let clone = document.importNode(this.baseTemplate, true);
this._processNode(clone, data || {});
return clone;
}
return null;
}
_processNode(node, context) {
// Attributes and children can change during the loop therefore we generate a new array using map fn
let attributeNames = Array.from(node.attributes).map(attr => attr.name);
let textNodes = Array.from(node.childNodes).filter(node => node.nodeType === Node.TEXT_NODE);
let children = Array.from(node.childNodes).filter(node => node.nodeType === Node.ELEMENT_NODE);
for (let name of attributeNames) {
// Abort of node processing can happen for an repeat placeholder or element has show/hide.if attribute
if (!this._processAttribute(node, name, context)) {
return;
}
}
// Process interpolations in text nodes
for (let text of textNodes) {
if (INTERPOLATION_EXPRESSION.test(text.textContent)) {
text.textContent = this._interpolate(text.textContent, context);
}
}
// Process child nodes
for (let child of children) {
this._processNode(child, context);
}
}
_processAttribute(node, attribute, context) {
let expression = node.getAttribute(attribute);
node.removeAttribute(attribute);
// let [attrName, command] = ... throws exception when stepping through it with debugger
let split = attribute.split('.');
let attrName = split[0];
let command = split[1];
switch (command) {
case 'trigger':
node.addEventListener(attrName, event => {
let eventContext = Object.assign({}, context, {event});
this._evaluate(expression, eventContext);
}, true);
break;
case 'if':
let value = this._evaluate(expression, context);
// Return false to abort further processing of current node
if (attrName === 'hide' ? value === 'true' : value === 'false') {
node.parentNode.removeChild(node);
return false;
}
break;
case 'bind':
node.setAttribute(attrName, this._evaluate(expression, context));
break;
case 'for':
this._processRepeatAttribute(node, context[expression], context);
// Abort further node processing because the placeholder node got removed removed
return false;
}
return true;
}
_processRepeatAttribute(node, items, context) {
let i = 0;
for (let key in items) {
if (!items.hasOwnProperty(key)) {
continue;
}
// Create a clone with children (also with text nodes which can hold interpolations)
let clone = document.importNode(node, true);
let itemContext = {index: i++, key, item: items[key], parent: context};
// Process attributes and children
this._processNode(clone, itemContext);
// Insert processed copy of placeholder
node.parentNode.insertBefore(clone, node);
}
// Remove placeholder from dom tree
node.parentNode.removeChild(node);
}
_interpolate(string, context) {
return string.replace(INTERPOLATION_EXPRESSION, (match, interpolation, expression) => {
return this._evaluate(expression, context);
});
}
_evaluate(expression, context) {
let values = Object.keys(context).map(key => context[key]);
return (new Function(Object.keys(context), 'return `${' + expression + '}`'))(...values);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment