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