UNPKG

uhtml

Version:
139 lines (131 loc) 5.22 kB
'use strict'; 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;