Adding easy-template

* Set up binding, preprocessing (to avoid multiple routing each time) and basic testwq
This commit is contained in:
Piper
2026-08-12 23:44:07 +02:00
parent 8cf2bada06
commit bb77cd1e90
2 changed files with 232 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<head>
<script src="./template-e6d.js"></script>
</head>
<body>
<template id="template-example" is="template-e6d">
<div>
<h2>Easy template</h2>
<p class="class-a class-b $class">Here's some text $textinterp interpolation, and some dynamic "$dynamicInterp".</p>
<p>$textinterp</p>
<button title="$title" data-e6d-event-click="click buttonclick">A button with tooltip and onclick $textinterpfunc</button>
<p>Oh, look: a normal dollar sign: $. And then a dollar sign with no vars (does not resolve, emits error): $dollar</p>
</div>
</template>
<main>
<div>
<div>
<button onclick="doExample()">Bind!</button>
</div>
<div>
<input id="text-interp" placeholder="Text to interpolate" />
</div>
</div>
<section id="template-add">
</section>
<script>
document.getElementById(`template-example`).addEventListener(`e6d-error`, console.log);
const doExample = () => {
const bind = document.getElementById(`template-example`).bind({
textinterp: `String!`,
textinterpfunc: () => {
return Math.random().toFixed(5);
},
class: `test-class`,
buttonclick: (eventTrigger) => {
alert(`Clicked ${ eventTrigger }`);
},
title: `A title, injected`,
dynamicInterp: document.getElementById(`text-interp`).value,
});
document.getElementById(`template-add`).appendChild(bind);
};
</script>
</main>
</body>
+184
View File
@@ -0,0 +1,184 @@
class TemplateE6d extends HTMLTemplateElement {
constructor() {
super();
this.preprocessed = false;
}
dispatchError(type, detail) {
const evtBroad = new CustomEvent(`e6d-error`, {detail,});
const evtSpecific = new CustomEvent(`e6d-error-${ type }`, {detail,});
this.dispatchEvent(evtBroad);
this.dispatchEvent(evtSpecific);
}
preprocess() {
if (this.preprocessed) {
return;
}
const preprocessNode = document.importNode(this.content, true);
const interpRegex = /([^\$]+|\$([a-zA-Z0-9]*))/g;
const eventRegex = /([^\s]+) ([a-zA-Z0-9]+)/g;
const allRoutes = [];
const buildTextTemplate = (text) => {
let anyInterp = false;
const template = [];
text.matchAll(interpRegex)
.forEach(m => {
if (m[2]) {
// Interpolation match
anyInterp = true;
template.push({
reference: m[2],
});
return;
}
// Text match
template.push({
text: m[1],
});
});
if (anyInterp) {
return template;
}
return null;
};
const dfsExplore = (node, trail) => {
const bindAttr = [];
const bindEvent = [];
let bindContent = false;
if (node.getAttributeNames) {
for (const attr of node.getAttributeNames()) {
if (attr.indexOf(`data-e6d-event-`) >= 0) {
const matches = [];
node.getAttribute(attr).matchAll(eventRegex).forEach(f => matches.push(f));
if (matches.length === 1 && matches[0].length === 3) {
bindEvent.push({
event: matches[0][1],
reference: matches[0][2],
});
} else {
this.dispatchError(`bad-data-e6d-event`, {node: node, message: `${ attr } requires format "<event-listener-trigger> <function-reference>"`});
}
} else {
const textTemplate = buildTextTemplate(node.getAttribute(attr));
if (textTemplate) {
bindAttr.push({
attr: attr,
template: textTemplate,
});
}
}
}
}
if (node.nodeType === 3) {
const textTemplate = buildTextTemplate(node.data);
if (textTemplate) {
bindContent = textTemplate;
}
}
if (bindAttr.length !== 0 || bindEvent.length !== 0 || bindContent) {
// Add a leaf!
allRoutes.push({
route: trail,
bind: {
attr: bindAttr,
event: bindEvent,
content: bindContent,
},
});
}
// Extend trail into children
for (let c = 0; c < node.childNodes.length; c++) {
const child = node.childNodes[c];
const thisTrail = trail.slice();
thisTrail.push(c);
dfsExplore(node.childNodes[c], thisTrail);
}
};
for (let t = 0; t < preprocessNode.childNodes.length; t++) {
dfsExplore(preprocessNode.childNodes[t], [t]);
}
this.bindRoute = allRoutes;
this.preprocessed = true;
}
connectedCallback() {
}
bind(obj) {
this.preprocess();
const templated = document.importNode(this.content, true);
if (!obj) {
return templated;
}
const resolve = (root, route) => {
let ptr = root;
for (const r of route) {
ptr = ptr.childNodes[r];
}
return ptr;
};
const templateString = (textTemplate, obj) => {
let result = ``;
for (const tt of textTemplate) {
if (tt.text) {
result += tt.text;
} else {
const reference = obj[tt.reference];
let value;
if (typeof reference === 'function') {
value = reference();
} else {
value = reference;
}
if (value === null || value === undefined) {
this.dispatchError(`empty-reference`, {message: `Attempted to find value for $${ tt.reference } but was <${ value }>`});
}
result += value;
}
}
return result;
};
for (const route of this.bindRoute) {
const injectTarget = resolve(templated, route.route);
for (const attr of route.bind.attr) {
const attrValue = templateString(attr.template, obj);
injectTarget.setAttribute(attr.attr, attrValue);
}
for (const evt of route.bind.event) {
const trigger = evt.event;
const refFunc = obj[evt.reference];
if (typeof refFunc === `function`) {
injectTarget.addEventListener(trigger, refFunc);
} else {
this.dispatchError(`ref-not-function`, {message: `Expected to find function for value $${ route.bind.event.reference } but found <${ refFunc }>`});
}
}
if (injectTarget.nodeType === 3 && route.bind.content) {
const dataValue = templateString(route.bind.content, obj);
injectTarget.data = dataValue;
}
}
return templated;
}
}
customElements.define(`template-e6d`, TemplateE6d, { extends: `template` });