Last active
October 6, 2023 13:01
-
-
Save vdsabev/71652e95666e52210aa6837993638219 to your computer and use it in GitHub Desktop.
Mutagen - a mini templating engine based on htmx and Mutation Observer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<script src="/mutagen.js"></script> | |
</head> | |
<body> | |
<mg-component src="/profile.html" data='{ "user": { "name": "John Doe", "pictureUrl": "https://placehold.co/128" } }' /> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(() => { | |
if (window.mutagen) { | |
return; | |
} | |
/** @type {Record<string, Set<HTMLElement>>} */ | |
const components = {}; // TODO: Remove component from cache after removing it from DOM | |
function observe() { | |
new MutationObserver((mutations) => { | |
for (const mutation of mutations) { | |
if (mutation.type !== 'childList') continue; | |
getAllChildNodes(mutation.addedNodes) | |
.filter((node) => node.tagName === 'MG-COMPONENT') | |
.forEach((component) => { | |
component.style.display = 'none'; // Hide component until render | |
const src = component.getAttribute('src'); | |
if (!components[src]) { | |
components[src] = new Set(); | |
loadTemplate(src); | |
} | |
components[src].add(component); | |
}); | |
} | |
}).observe(document.documentElement, { childList: true, subtree: true }); | |
} | |
/** @returns {Node[]} */ | |
function getAllChildNodes(/** @type {NodeList} */ childNodes) { | |
return [...childNodes].flatMap((childNode) => [ | |
childNode, | |
...getAllChildNodes(childNode.childNodes), | |
]); | |
} | |
function loadTemplate(/** @type {string} */ src) { | |
let container = document.getElementById('mutagen-templates'); | |
if (!container) { | |
container = document.createElement('div'); | |
container.id = 'mutagen-templates'; | |
container.style.display = 'none'; | |
document.body.prepend(container); | |
} | |
const template = document.createElement('div'); | |
template.setAttribute('hx-get', src); | |
template.setAttribute('hx-trigger', 'load'); | |
template.setAttribute('hx-on::after-settle', 'mutagen.render(this)'); | |
container.appendChild(template); | |
} | |
function render(/** @type {HTMLElement} */ template) { | |
const src = template.getAttribute('hx-get'); | |
if (!components[src]) return; | |
for (const component of components[src]) { | |
const data = parseData(component.getAttribute('data')); | |
let processedTemplate = template.innerHTML; | |
// Process {{text}} | |
let textMatch; | |
while ( | |
(textMatch = /{{(\w+(?:\.\w+|\['\w+'\])*)}}/g.exec(processedTemplate)) | |
) { | |
processedTemplate = processedTemplate.replaceAll( | |
textMatch[0], | |
get(data, textMatch[1]) | |
); | |
} | |
// Process :attributes | |
let attributeMatch; | |
while ( | |
(attributeMatch = /:(\w+)="(\w+(?:\.\w+|\['\w+'\])*)"/g.exec( | |
processedTemplate | |
)) | |
) { | |
processedTemplate = processedTemplate.replaceAll( | |
attributeMatch[0], | |
`${attributeMatch[1]}="${get(data, attributeMatch[2])}"` | |
); | |
} | |
component.innerHTML = processedTemplate; | |
component.style.display = 'contents'; // Show component after render - use `display: contents` to avoid the root element affecting styling | |
} | |
} | |
/** @returns {Record<string, any>} */ | |
function parseData(/** @type {string} */ dataString) { | |
if (dataString) { | |
try { | |
return JSON.parse(dataString); | |
} catch (error) { | |
console.error('Invalid mg-component data:', dataString); | |
} | |
} | |
return {}; | |
} | |
function get(/** Record<string, any> */ data, /** @type {string} */ path) { | |
const parts = path | |
.match(/\w+|'\w+'/g) | |
.map((part) => part.replace(/^'|'$/g, '')); | |
let result = data; | |
let index = 0; | |
while ( | |
index < parts.length && | |
typeof result === 'object' && | |
result != null | |
) { | |
result = result[parts[index]]; | |
index++; | |
} | |
return result; | |
} | |
window.mutagen = { render }; | |
observe(); | |
})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<h2>{{user.name}}</h2> | |
<img :src="user.pictureUrl" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Really cool!