Last active
April 11, 2020 15:51
-
-
Save tomhodgins/d8e748771393bfbaab685f452daa4ebd to your computer and use it in GitHub Desktop.
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> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<title>HTML templating examples</title> | |
<script type=module> | |
// The html tagged template function | |
function html(strings = [''], ...expressions) { | |
const elements = [] | |
const functions = {} | |
if (typeof strings === 'string') { | |
strings = [strings] | |
} | |
function stringifyObject(object = '') { | |
if ([NodeList, HTMLCollection].some(type => object instanceof type)) { | |
return stringifyObject(Array.from(object)) | |
} | |
if (Array.isArray(object)) { | |
return object.map(stringifyObject).join('') | |
} | |
if (object instanceof DocumentFragment) { | |
return Array.from(object.childNodes) | |
.map(node => node.outerHTML || node.nodeValue) | |
.join('') | |
} | |
if (object instanceof Element) { | |
elements.push(object) | |
return `<element-slot></element-slot>` | |
} | |
if (typeof object === 'function') { | |
const name = `--function-${Object.keys(functions).length}` | |
functions[name] = object | |
return name | |
} | |
return object | |
} | |
const fragment = document.implementation | |
.createHTMLDocument() | |
.createRange() | |
.createContextualFragment( | |
strings.reduce((markup, string, index) => | |
markup + stringifyObject(expressions[index - 1]) + string | |
) | |
) | |
// Replace any event handlers | |
if (Object.keys(functions).length) { | |
Array.from(fragment.querySelectorAll('*')) | |
.filter(tag => Array.from(tag.attributes).some(({name}) => name.startsWith('on:'))) | |
.forEach(node => { | |
if ( | |
node.attributes | |
&& node.attributes.length | |
) { | |
Array.from(node.attributes) | |
.filter(({name}) => name.startsWith('on:')) | |
.forEach(({name, value}) => { | |
node.addEventListener( | |
name.replace(/^on:/, '').toLowerCase(), | |
functions[value] | |
) | |
node.removeAttribute(name) | |
}) | |
} | |
return node | |
}) | |
} | |
// Replace any elements | |
if (elements.length) { | |
fragment.querySelectorAll('element-slot').forEach((slot, index) => | |
slot.replaceWith(elements[index]) | |
) | |
} | |
return fragment | |
} | |
// Examples | |
[ | |
// No input is empty DocumentFragment | |
html(), | |
html``, | |
// Works with string input | |
html('<h1>Hello</h1>'), | |
// Works as a tagged template function | |
html`<h1>World</h1>`, | |
// Works with ${} for interpolating JS expressions anywhere | |
html`<p>Today's date is ${new Date}`, | |
// Note: Some types are handled specially | |
// NodeLists will be flattened in-place | |
html`<p>All of the <a> tags in this NodeList: ${ | |
html`<a>a tag</a><b>b tag</b>`.querySelectorAll('a') | |
}`, | |
// HTMLCollections will be flattened in-place | |
html`<p>All of the <a> tags in this HTMLCollection: ${ | |
new DOMParser() | |
.parseFromString('<a>a tag</a><b>b tag</b>', 'text/html') | |
.getElementsByTagName('a') | |
}`, | |
// Arrays will be flattened in-place | |
html`<p>All of the tags in this Array: ${ | |
[ | |
document.createElement('input'), | |
document.createElement('button'), | |
] | |
}`, | |
// Elements will be appended to the parsed DOM | |
html`<p>This includes an element: ${(() => { | |
const button = document.createElement('button') | |
button.textContent = 'Click me' | |
button.addEventListener('click', event => alert('I was clicked')) | |
return button | |
})()}`, | |
// You can clone nodes from other existing DOM into your new template | |
html`<p>This contains a clone of a pre-existing DOM node: ${document.querySelector('code').cloneNode(true)}`, | |
// Or even append existing elements from other DOM into your new template | |
html`<p>This contains pre-existing DOM node from the document: ${document.querySelector('code')}`, | |
// DocumentFragments and all their children can be nested as well | |
html`<p>This includes a DocumentFragment: ${html`a <b>document <em>fragment</em></b> inside`}`, | |
// One final trick — on:* attributes | |
// Standard event attributes like onclick="" will be left alone | |
// We can use on:click (or on:<event> for any other event) in our HTML | |
// with a JavaScript function as the value, and this HTML templating system | |
// will use addEventListener() to attach the function, and remove the on:* attribute | |
html` | |
<!-- left alone and stays in DOM as onclick --> | |
<button onclick="alert('I was clicked')">Click me</button> | |
<!-- turns into addEventListener('click', event => alert('I was clicked')) and is stripped --> | |
<button on:click=${event => alert('I was clicked')}>Click me</button> | |
`, | |
].forEach(example => { | |
console.log([...example.childNodes].map(node => node.outerHTML || node.data)) | |
document.body.append(example) | |
}) | |
</script> | |
<code>pre-existing element</code> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment