Skip to content

Instantly share code, notes, and snippets.

@JoshAntBrown
Last active May 15, 2024 06:27
Show Gist options
  • Save JoshAntBrown/a05d27264a2291bb87fae9e05eefa66e to your computer and use it in GitHub Desktop.
Save JoshAntBrown/a05d27264a2291bb87fae9e05eefa66e to your computer and use it in GitHub Desktop.
Template Renderer
class TemplateRenderer {
constructor(template) {
this.template = template
}
render(data) {
const clone = document.importNode(this.template.content, true)
this.#processNodes(clone, data)
return clone
}
#processNodes(root, data) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null, false);
while (walker.nextNode()) {
const node = walker.currentNode
this.#processAttributes(node, data)
}
}
#processAttributes(node, data) {
const attributes = Array.from(node.attributes)
for (const attr of attributes) {
if (attr.name.startsWith('t-')) {
const [type, prop] = attr.name.split(':')
const value = this.#evaluateExpression(attr.value, data)
this.#applyAttribute(node, type, prop, value)
node.removeAttribute(attr.name)
}
}
}
#evaluateExpression(expression, data) {
try {
const func = new Function(...Object.keys(data), `return (${expression});`)
return func(...Object.values(data))
} catch (e) {
console.error(`Error evaluating expression: ${expression}`, e)
return null;
}
}
#applyAttribute(node, type, prop, value) {
switch (type) {
case 't-text':
node.textContent = value;
break;
case 't-attr':
node.setAttribute(prop, value)
break;
case 't-if':
if (!value) node.remove()
break;
default:
break;
}
}
}
@JoshAntBrown
Copy link
Author

TemplateRenderer

On a couple of occasions, while building web apps with Hotwire and other light-weight minimal JavaScript libraries, I've needed to perform some light-weight client side rendering of a template or partial. Occasions where using a Turbo frame would have been a bit too cumbersome or complex for one reason or another.

To solve for that problem I've put together this simple TemplateRenderer class that allows for some simple template building. It's intended to be used inside a Stimulus controller, or some other similar container. Its single responsibility is to take a template element and some data model, and in turn build a clone of the template contents populated with any relevant data.

Example

<button onclick="appendArticle()">Add Article</button>	

<template id="articleTemplate">	
  <article t-attr:id="`article_${Date.now()}`">	
    <h2 t-text="title">Post Title</h2>	
    <p>By <span t-text="author.name">Author Name</span></p>	
  </article>	
</template>	

<script>	
  function appendArticle() {	
    const articleData = {	
      title: "An Article",	
      author: {	
        name: "Jane Doe",	
      }	
    }	

    const articleRenderer = new TemplateRenderer(articleTemplate)	
    const articleElement = articleRenderer.render(articleData)	
    document.body.appendChild(articleElement)	
  }	
</script>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment