Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tomhodgins/22bf6c9624237ef5a840672d44e314ad to your computer and use it in GitHub Desktop.
Save tomhodgins/22bf6c9624237ef5a840672d44e314ad to your computer and use it in GitHub Desktop.
This tagged template for HTML literals works in all modern browsers and Deno
if ('Deno' in globalThis) {
globalThis.DOMParser = (await import('https://deno.land/x/deno_dom/deno-dom-wasm.ts')).DOMParser
}
function html(taggedTemplateString = [''], ...expressions) {
const nodes = []
const functions = new Map
const strings = typeof taggedTemplateString === 'string'
? [taggedTemplateString]
: taggedTemplateString
function stringify(object = '') {
// Null
if (object === null) {
return ''
}
// String
if (typeof object === 'string') {
return object
}
// Array
if (Array.isArray(object)) {
return object.map(stringify).join('')
}
// Non-Array Iterables: e.g. NodeList, HTMLCollection, Set, etc.
if (object[Symbol.iterator]) {
return stringify([Array.from(object)])
}
// Document, DocumentFragment
if ([Document, DocumentFragment].some(type => object instanceof type)) {
return Array.from(object.childNodes).reduce(
(slots, child) => {
nodes.push(child)
return slots += `<template data-dom-slot="${nodes.length - 1}"></template>`
},
''
)
}
// All other DOM node types: e.g. #text, #comment, Element, etc.
if (object instanceof Node) {
nodes.push(object)
return `<template data-dom-slot="${nodes.length - 1}"></template>`
}
// Functions (for use with on:* attributes)
if (typeof object === 'function') {
const name = `--function-reference-${functions.size}`
functions.set(name, object)
return name
}
// Otherwise stringify anything else yourself, where you interpolate it, before gets here
return String(object)
}
// Parse a string of text as a fragment of an HTML Document
const fragment = new DOMParser().parseFromString(
strings.reduce((markup, string, index) =>
markup + stringify(expressions[index - 1]) + string
),
'text/html'
).body
// Replace any DOM nodes with stored nodes
if (nodes.length) {
fragment.querySelectorAll('template[data-dom-slot]').forEach(tag =>
tag.replaceWith(nodes[tag.dataset.domSlot])
)
}
// Replace any on:* attribute event handlers
if (functions.size) {
fragment.querySelectorAll('*').forEach(node => {
const onAttributes = Array.from(node.attributes).filter(({name}) =>
name.startsWith('on:')
)
if (onAttributes.length) {
onAttributes.forEach(({name, value}) => {
node.addEventListener(
name.replace(/^on:/, '').toLowerCase(),
functions.get(value)
)
node.removeAttribute(name)
})
}
})
}
return fragment
}
console.log(
Array.from(
html`one<p><b>two<p>three`.childNodes,
({outerHTML, data}) => outerHTML ?? data
).join('')
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment