diff --git a/src/template/example.html b/src/template/example.html new file mode 100644 index 0000000..9ebc2ec --- /dev/null +++ b/src/template/example.html @@ -0,0 +1,48 @@ + + + + + + +
+
+
+ +
+
+ +
+
+ +
+ +
+ + +
+ \ No newline at end of file diff --git a/src/template/template-e6d.js b/src/template/template-e6d.js new file mode 100644 index 0000000..bbbee4f --- /dev/null +++ b/src/template/template-e6d.js @@ -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 " "`}); + } + } 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` }); \ No newline at end of file