uhtml
Version:
A micro HTML/SVG render
139 lines (131 loc) • 5.22 kB
JavaScript
;
const {isArray, slice} = require('uarray');
const udomdiff = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('udomdiff'));
const {aria, attribute, boolean, event, ref, setter, text} = require('uhandlers');
const {diffable} = require('uwire');
const {reducePath} = require('./node.js');
// this helper avoid code bloat around handleAnything() callback
const diff = (comment, oldNodes, newNodes) => udomdiff(
comment.parentNode,
// TODO: there is a possible edge case where a node has been
// removed manually, or it was a keyed one, attached
// to a shared reference between renders.
// In this case udomdiff might fail at removing such node
// as its parent won't be the expected one.
// The best way to avoid this issue is to filter oldNodes
// in search of those not live, or not in the current parent
// anymore, but this would require both a change to uwire,
// exposing a parentNode from the firstChild, as example,
// but also a filter per each diff that should exclude nodes
// that are not in there, penalizing performance quite a lot.
// As this has been also a potential issue with domdiff,
// and both lighterhtml and hyperHTML might fail with this
// very specific edge case, I might as well document this possible
// "diffing shenanigan" and call it a day.
oldNodes,
newNodes,
diffable,
comment
);
// if an interpolation represents a comment, the whole
// diffing will be related to such comment.
// This helper is in charge of understanding how the new
// content for such interpolation/hole should be updated
const handleAnything = comment => {
let oldValue, text, nodes = [];
const anyContent = newValue => {
switch (typeof newValue) {
// primitives are handled as text content
case 'string':
case 'number':
case 'boolean':
if (oldValue !== newValue) {
oldValue = newValue;
if (!text)
text = document.createTextNode('');
text.data = newValue;
nodes = diff(comment, nodes, [text]);
}
break;
// null, and undefined are used to cleanup previous content
case 'object':
case 'undefined':
if (newValue == null) {
if (oldValue != newValue) {
oldValue = newValue;
nodes = diff(comment, nodes, []);
}
break;
}
// arrays and nodes have a special treatment
if (isArray(newValue)) {
oldValue = newValue;
// arrays can be used to cleanup, if empty
if (newValue.length === 0)
nodes = diff(comment, nodes, []);
// or diffed, if these contains nodes or "wires"
else if (typeof newValue[0] === 'object')
nodes = diff(comment, nodes, newValue);
// in all other cases the content is stringified as is
else
anyContent(String(newValue));
break;
}
// if the new value is a DOM node, or a wire, and it's
// different from the one already live, then it's diffed.
// if the node is a fragment, it's appended once via its childNodes
// There is no `else` here, meaning if the content
// is not expected one, nothing happens, as easy as that.
if (oldValue !== newValue && 'ELEMENT_NODE' in newValue) {
oldValue = newValue;
nodes = diff(
comment,
nodes,
newValue.nodeType === 11 ?
slice.call(newValue.childNodes) :
[newValue]
);
}
break;
case 'function':
anyContent(newValue(comment));
break;
}
};
return anyContent;
};
// attributes can be:
// * ref=${...} for hooks and other purposes
// * aria=${...} for aria attributes
// * ?boolean=${...} for boolean attributes
// * .dataset=${...} for dataset related attributes
// * .setter=${...} for Custom Elements setters or nodes with setters
// such as buttons, details, options, select, etc
// * onevent=${...} to automatically handle event listeners
// * generic=${...} to handle an attribute just like an attribute
const handleAttribute = (node, name/*, svg*/) => {
switch (name[0]) {
case '?': return boolean(node, name.slice(1), false);
case '.': return setter(node, name.slice(1));
case 'o': if (name[1] === 'n') return event(node, name);
}
switch (name) {
case 'ref': return ref(node);
case 'aria': return aria(node);
}
return attribute(node, name/*, svg*/);
};
// each mapped update carries the update type and its path
// the type is either node, attribute, or text, while
// the path is how to retrieve the related node to update.
// In the attribute case, the attribute name is also carried along.
function handlers(options) {
const {type, path} = options;
const node = path.reduceRight(reducePath, this);
return type === 'node' ?
handleAnything(node) :
(type === 'attr' ?
handleAttribute(node, options.name/*, options.svg*/) :
text(node));
}
exports.handlers = handlers;