Adding easy-template
* Set up binding, preprocessing (to avoid multiple routing each time) and basic testwq
This commit is contained in:
@@ -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>
|
||||||
@@ -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` });
|
||||||
Reference in New Issue
Block a user