Skip to content

Instantly share code, notes, and snippets.

@tomhodgins
Last active April 11, 2020 15:51
Show Gist options
  • Save tomhodgins/d8e748771393bfbaab685f452daa4ebd to your computer and use it in GitHub Desktop.
Save tomhodgins/d8e748771393bfbaab685f452daa4ebd to your computer and use it in GitHub Desktop.
<!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 &lt;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 &lt;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