hyperf
Version:
Hypertext fragments with reactivity
136 lines (110 loc) • 4.98 kB
JavaScript
import htm from 'htm'
import sube, { observable } from 'sube'
import swap from 'swapdom'
import { prop as attr } from 'element-props'
export const _static = Symbol()
// configure swapper
// FIXME: modifying prev key can also make it faster
// FIXME: avoid insert, replace: do that before
swap.same = (a, b) => a === b || (a?.data != null && a.data === b?.data)
swap.insert = (a, b, parent) => b != null && parent.insertBefore(b?.nodeType ? b : doc.createTextNode(b), a)
swap.replace = (from, to, parent) => !to ? from.replace() : to.nodeType ? parent.replaceChild(to, from) :
from.nodeType === TEXT ? from.data = to : from.replaceWith(to)
const TEXT = 3, ELEM = 1, ATTR = 2, COMM = 8, FRAG = 11, COMP = 6
const isPrimitive = value => value !== Object(value);
const cache = new WeakSet,
ctx = { _init: false },
doc = document,
h = hyperscript.bind(ctx)
function flat(children) {
let out = [], i = 0, item
for (; i < children.length;)
if ((item = children[i++]) != null)
// .values is common for NodeList / Array indicator (.forEach is used by rxjs, iterator is by string)
if (item.values) for (item of item) out.push(item)
else out.push(item)
return out
}
export default function html(statics) {
if (!Array.isArray(statics)) return h(...arguments)
// HTM caches nodes that don't have attr or children fields
// eg. <a><b>${1}</b></a> - won't cache `a`,`b`, but <a>${1}<b/></a> - will cache `b`
// for that purpose we first build template with blank fields, marking all fields as tpl
// NOTE: static nodes caching is bound to htm.this (h) function, so can't substitute it
// NOTE: we can't use first non-cached call result, because it serves as source for further cloning static nodes
let result, count = 1
if (!cache.has(statics)) count++, cache.add(statics)
while (count--) {
ctx._init = count ? true : false
// init render may setup observables, which is undesirable - so we skip attributes
result = htm.apply(h, count ? [statics] : arguments)
}
return Array.isArray(result) ? h(doc.createDocumentFragment(), null, ...result) :
result?.[_static] ? result.cloneNode(true) :
result?.nodeType ? result : isPrimitive(result) ? doc.createTextNode(result ?? '') : result
}
function hyperscript(tag, props, ...children) {
let { _init } = this
if (typeof tag === 'string') {
tag = tag ? doc.createElement(tag) : doc.createDocumentFragment()
// shortcut for faster creation, static nodes are really simple
if (_init) {
tag[_static] = true
for (let name in props) attr(tag, name, props[name])
; (tag.nodeName === 'TEMPLATE' ? tag.content : tag).append(...flat(children))
return null, tag
}
}
// _init call is irrelevant for dynamic nodes
else if (_init) return null
else if (typeof tag === 'function') {
tag = tag({ children, ...props })
if (!tag) return
// directly element
if (tag.nodeType === TEXT || tag.nodeType === ELEM || tag.nodeType === FRAG) return tag
// we unwrap single-node children
if (!Array.isArray(tag)) {
if (observable(tag)) return tag
return h(doc.createDocumentFragment(), null, tag)
}
// run result via hyperf
return h(doc.createDocumentFragment(), null, ...tag)
}
// apply props
let subs = [], i, l, child, name, value, s, v, match
for (name in props) {
value = props[name]
// classname can contain these casted literals
if (typeof value === 'string' && name.startsWith('class')) value = value.replace(/\b(false|null|undefined)\b/g, '')
// primitive is more probable also less expensive than observable check
if (observable(value)) sube(value, v => attr(tag, name, v))
else if (typeof value !== 'string' && name === 'style') {
for (s in value) {
if (observable(v = value[s])) sube(v, v => tag.style.setProperty(s, v))
else if (match = v.match(/(.*)\W+!important\W*$/)) tag.style.setProperty(s, match[1], 'important')
else tag.style.setProperty(s, v)
}
}
else attr(tag, name, value)
}
if (children.length) {
children = flat(children)
// detect observables in children
for (i = 0, l = children.length; i < l; i++) {
if (child = children[i])
// static nodes (cached by HTM) must be cloned, because h is not called for them more than once
if (child[_static]) children[i] = child.cloneNode(true)
else if (observable(child)) subs[i] = child, children[i] = doc.createTextNode('')
}
// append shortcut
if (!tag.childNodes.length) (tag.nodeName === 'TEMPLATE' ? tag.content : tag).append(...children)
else swap(tag, tag.childNodes, children)
if (subs.length) subs.forEach((sub, i) => sube(sub, child => (
children[i] = child,
// NOTE: in some cases fragment is a child of another fragment, we ignore that case
swap(tag, tag.childNodes, flat(children))
)))
}
else tag.innerHTML = ''
return null, tag
}