Precise DOM morphing
morphing typescript dom
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Simplify tests

+1016 -1433
bun.lockb

This is a binary file and will not be displayed.

+12 -12
dist/morphlex.d.ts
··· 1 1 interface Options { 2 - ignoreActiveValue?: boolean; 3 - preserveModifiedValues?: boolean; 4 - beforeNodeMorphed?: (node: Node, referenceNode: Node) => boolean; 5 - afterNodeMorphed?: (node: Node, referenceNode: Node) => void; 6 - beforeNodeAdded?: (node: Node) => boolean; 7 - afterNodeAdded?: (node: Node) => void; 8 - beforeNodeRemoved?: (node: Node) => boolean; 9 - afterNodeRemoved?: (node: Node) => void; 10 - beforeAttributeUpdated?: (element: Element, attributeName: string, newValue: string | null) => boolean; 11 - afterAttributeUpdated?: (element: Element, attributeName: string, previousValue: string | null) => void; 12 - beforePropertyUpdated?: (node: Node, propertyName: PropertyKey, newValue: unknown) => boolean; 13 - afterPropertyUpdated?: (node: Node, propertyName: PropertyKey, previousValue: unknown) => void; 2 + ignoreActiveValue?: boolean; 3 + preserveModifiedValues?: boolean; 4 + beforeNodeMorphed?: (node: Node, referenceNode: Node) => boolean; 5 + afterNodeMorphed?: (node: Node, referenceNode: Node) => void; 6 + beforeNodeAdded?: (node: Node) => boolean; 7 + afterNodeAdded?: (node: Node) => void; 8 + beforeNodeRemoved?: (node: Node) => boolean; 9 + afterNodeRemoved?: (node: Node) => void; 10 + beforeAttributeUpdated?: (element: Element, attributeName: string, newValue: string | null) => boolean; 11 + afterAttributeUpdated?: (element: Element, attributeName: string, previousValue: string | null) => void; 12 + beforePropertyUpdated?: (node: Node, propertyName: PropertyKey, newValue: unknown) => boolean; 13 + afterPropertyUpdated?: (node: Node, propertyName: PropertyKey, previousValue: unknown) => void; 14 14 } 15 15 export declare function morph(node: ChildNode, reference: ChildNode | string, options?: Options): void; 16 16 export declare function morphInner(element: Element, reference: Element | string, options?: Options): void;
+369 -332
dist/morphlex.js
··· 1 1 export function morph(node, reference, options = {}) { 2 - if (typeof reference === "string") reference = parseChildNodeFromString(reference); 3 - new Morph(options).morph([node, reference]); 2 + if (typeof reference === "string") 3 + reference = parseChildNodeFromString(reference); 4 + new Morph(options).morph([node, reference]); 4 5 } 5 6 export function morphInner(element, reference, options = {}) { 6 - if (typeof reference === "string") reference = parseElementFromString(reference); 7 - new Morph(options).morphInner([element, reference]); 7 + if (typeof reference === "string") 8 + reference = parseElementFromString(reference); 9 + new Morph(options).morphInner([element, reference]); 8 10 } 9 11 function parseElementFromString(string) { 10 - const node = parseChildNodeFromString(string); 11 - if (isElement(node)) return node; 12 - else throw new Error("[Morphlex] The string was not a valid HTML element."); 12 + const node = parseChildNodeFromString(string); 13 + if (isElement(node)) 14 + return node; 15 + else 16 + throw new Error("[Morphlex] The string was not a valid HTML element."); 13 17 } 14 18 function parseChildNodeFromString(string) { 15 - const parser = new DOMParser(); 16 - const doc = parser.parseFromString(string, "text/html"); 17 - if (doc.childNodes.length === 1) return doc.body.firstChild; 18 - else throw new Error("[Morphlex] The string was not a valid HTML node."); 19 + const parser = new DOMParser(); 20 + const doc = parser.parseFromString(string, "text/html"); 21 + if (doc.childNodes.length === 1) 22 + return doc.body.firstChild; 23 + else 24 + throw new Error("[Morphlex] The string was not a valid HTML node."); 19 25 } 20 26 class Morph { 21 - #idMap; 22 - #sensivityMap; 23 - #ignoreActiveValue; 24 - #preserveModifiedValues; 25 - #beforeNodeMorphed; 26 - #afterNodeMorphed; 27 - #beforeNodeAdded; 28 - #afterNodeAdded; 29 - #beforeNodeRemoved; 30 - #afterNodeRemoved; 31 - #beforeAttributeUpdated; 32 - #afterAttributeUpdated; 33 - #beforePropertyUpdated; 34 - #afterPropertyUpdated; 35 - constructor(options = {}) { 36 - this.#idMap = new WeakMap(); 37 - this.#sensivityMap = new WeakMap(); 38 - this.#ignoreActiveValue = options.ignoreActiveValue || false; 39 - this.#preserveModifiedValues = options.preserveModifiedValues || false; 40 - this.#beforeNodeMorphed = options.beforeNodeMorphed; 41 - this.#afterNodeMorphed = options.afterNodeMorphed; 42 - this.#beforeNodeAdded = options.beforeNodeAdded; 43 - this.#afterNodeAdded = options.afterNodeAdded; 44 - this.#beforeNodeRemoved = options.beforeNodeRemoved; 45 - this.#afterNodeRemoved = options.afterNodeRemoved; 46 - this.#beforeAttributeUpdated = options.beforeAttributeUpdated; 47 - this.#afterAttributeUpdated = options.afterAttributeUpdated; 48 - this.#beforePropertyUpdated = options.beforePropertyUpdated; 49 - this.#afterPropertyUpdated = options.afterPropertyUpdated; 50 - Object.freeze(this); 51 - } 52 - morph(pair) { 53 - this.#withAriaBusy(pair[0], () => { 54 - if (isParentNodePair(pair)) this.#buildMaps(pair); 55 - this.#morphNode(pair); 56 - }); 57 - } 58 - morphInner(pair) { 59 - this.#withAriaBusy(pair[0], () => { 60 - if (isMatchingElementPair(pair)) { 61 - this.#buildMaps(pair); 62 - this.#morphMatchingElementContent(pair); 63 - } else { 64 - throw new Error("[Morphlex] You can only do an inner morph with matching elements."); 65 - } 66 - }); 67 - } 68 - #withAriaBusy(node, block) { 69 - if (isElement(node)) { 70 - const originalAriaBusy = node.ariaBusy; 71 - node.ariaBusy = "true"; 72 - block(); 73 - node.ariaBusy = originalAriaBusy; 74 - } else block(); 75 - } 76 - #buildMaps([node, reference]) { 77 - this.#mapIdSets(node); 78 - this.#mapIdSets(reference); 79 - this.#mapSensivity(node); 80 - Object.freeze(this.#idMap); 81 - Object.freeze(this.#sensivityMap); 82 - } 83 - #mapSensivity(node) { 84 - const sensitiveElements = node.querySelectorAll("audio,canvas,embed,iframe,input,object,textarea,video"); 85 - const sensitiveElementsLength = sensitiveElements.length; 86 - for (let i = 0; i < sensitiveElementsLength; i++) { 87 - const sensitiveElement = sensitiveElements[i]; 88 - let sensivity = 0; 89 - if (isInput(sensitiveElement) || isTextArea(sensitiveElement)) { 90 - sensivity++; 91 - if (sensitiveElement.value !== sensitiveElement.defaultValue) sensivity++; 92 - if (sensitiveElement === document.activeElement) sensivity++; 93 - } else { 94 - sensivity += 3; 95 - if (isMedia(sensitiveElement) && !sensitiveElement.ended) { 96 - if (!sensitiveElement.paused) sensivity++; 97 - if (sensitiveElement.currentTime > 0) sensivity++; 98 - } 99 - } 100 - let current = sensitiveElement; 101 - while (current) { 102 - this.#sensivityMap.set(current, (this.#sensivityMap.get(current) || 0) + sensivity); 103 - if (current === node) break; 104 - current = current.parentElement; 105 - } 106 - } 107 - } 108 - // For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements. 109 - #mapIdSets(node) { 110 - const elementsWithIds = node.querySelectorAll("[id]"); 111 - const elementsWithIdsLength = elementsWithIds.length; 112 - for (let i = 0; i < elementsWithIdsLength; i++) { 113 - const elementWithId = elementsWithIds[i]; 114 - const id = elementWithId.id; 115 - // Ignore empty IDs 116 - if (id === "") continue; 117 - let current = elementWithId; 118 - while (current) { 119 - const idSet = this.#idMap.get(current); 120 - idSet ? idSet.add(id) : this.#idMap.set(current, new Set([id])); 121 - if (current === node) break; 122 - current = current.parentElement; 123 - } 124 - } 125 - } 126 - // This is where we actually morph the nodes. The `morph` function (above) exists only to set up the `idMap`. 127 - #morphNode(pair) { 128 - if (isMatchingElementPair(pair)) this.#morphMatchingElementNode(pair); 129 - else this.#morphOtherNode(pair); 130 - } 131 - #morphMatchingElementNode(pair) { 132 - const [node, reference] = pair; 133 - if (!(this.#beforeNodeMorphed?.(node, writableNode(reference)) ?? true)) return; 134 - if (node.hasAttributes() || reference.hasAttributes()) this.#morphAttributes(pair); 135 - // TODO: Should use a branded pair here. 136 - this.#morphMatchingElementContent(pair); 137 - this.#afterNodeMorphed?.(node, writableNode(reference)); 138 - } 139 - #morphOtherNode([node, reference]) { 140 - if (!(this.#beforeNodeMorphed?.(node, writableNode(reference)) ?? true)) return; 141 - if (node.nodeType === reference.nodeType && node.nodeValue !== null && reference.nodeValue !== null) { 142 - // Handle text nodes, comments, and CDATA sections. 143 - this.#updateProperty(node, "nodeValue", reference.nodeValue); 144 - } else this.#replaceNode(node, reference.cloneNode(true)); 145 - this.#afterNodeMorphed?.(node, writableNode(reference)); 146 - } 147 - #morphMatchingElementContent(pair) { 148 - const [node, reference] = pair; 149 - if (isHead(node)) { 150 - // We can pass the reference as a head here becuase we know it's the same as the node. 151 - this.#morphHeadContents(pair); 152 - } else if (node.hasChildNodes() || reference.hasChildNodes()) this.#morphChildNodes(pair); 153 - } 154 - #morphHeadContents([node, reference]) { 155 - const refChildNodesMap = new Map(); 156 - // Generate a map of the reference head element’s child nodes, keyed by their outerHTML. 157 - const referenceChildrenLength = reference.children.length; 158 - for (let i = 0; i < referenceChildrenLength; i++) { 159 - const child = reference.children[i]; 160 - refChildNodesMap.set(child.outerHTML, child); 161 - } 162 - const nodeChildrenLength = node.children.length; 163 - for (let i = 0; i < nodeChildrenLength; i++) { 164 - const child = node.children[i]; 165 - const key = child.outerHTML; 166 - const refChild = refChildNodesMap.get(key); 167 - // If the child is in the reference map already, we don’t need to add it later. 168 - // If it’s not in the map, we need to remove it from the node. 169 - refChild ? refChildNodesMap.delete(key) : this.#removeNode(child); 170 - } 171 - // Any remaining nodes in the map should be appended to the head. 172 - for (const refChild of refChildNodesMap.values()) this.#appendChild(node, refChild.cloneNode(true)); 173 - } 174 - #morphAttributes([element, reference]) { 175 - // Remove any excess attributes from the element that aren’t present in the reference. 176 - for (const { name, value } of element.attributes) { 177 - if (!reference.hasAttribute(name) && (this.#beforeAttributeUpdated?.(element, name, null) ?? true)) { 178 - element.removeAttribute(name); 179 - this.#afterAttributeUpdated?.(element, name, value); 180 - } 181 - } 182 - // Copy attributes from the reference to the element, if they don’t already match. 183 - for (const { name, value } of reference.attributes) { 184 - const previousValue = element.getAttribute(name); 185 - if (previousValue !== value && (this.#beforeAttributeUpdated?.(element, name, value) ?? true)) { 186 - element.setAttribute(name, value); 187 - this.#afterAttributeUpdated?.(element, name, previousValue); 188 - } 189 - } 190 - // For certain types of elements, we need to do some extra work to ensure 191 - // the element’s state matches the reference elements’ state. 192 - if (isInput(element) && isInput(reference)) { 193 - this.#updateProperty(element, "checked", reference.checked); 194 - this.#updateProperty(element, "disabled", reference.disabled); 195 - this.#updateProperty(element, "indeterminate", reference.indeterminate); 196 - if ( 197 - element.type !== "file" && 198 - !(this.#ignoreActiveValue && document.activeElement === element) && 199 - !(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue) 200 - ) { 201 - this.#updateProperty(element, "value", reference.value); 202 - } 203 - } else if (isOption(element) && isOption(reference)) { 204 - this.#updateProperty(element, "selected", reference.selected); 205 - } else if ( 206 - isTextArea(element) && 207 - isTextArea(reference) && 208 - !(this.#ignoreActiveValue && document.activeElement === element) && 209 - !(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue) 210 - ) { 211 - this.#updateProperty(element, "value", reference.value); 212 - const text = element.firstElementChild; 213 - if (text) this.#updateProperty(text, "textContent", reference.value); 214 - } 215 - } 216 - // Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match. 217 - #morphChildNodes(pair) { 218 - const [element, reference] = pair; 219 - const childNodes = element.childNodes; 220 - const refChildNodes = reference.childNodes; 221 - for (let i = 0; i < refChildNodes.length; i++) { 222 - const child = childNodes[i]; 223 - const refChild = refChildNodes[i]; 224 - if (child && refChild) { 225 - const pair = [child, refChild]; 226 - if (isMatchingElementPair(pair)) { 227 - if (isHead(pair[0])) { 228 - this.#morphHeadContents(pair); 229 - } else { 230 - this.#morphChildElement(pair, element); 231 - } 232 - } else this.#morphOtherNode(pair); 233 - } else if (refChild) { 234 - this.#appendChild(element, refChild.cloneNode(true)); 235 - } else if (child) { 236 - this.#removeNode(child); 237 - } 238 - } 239 - // Clean up any excess nodes that may be left over 240 - while (childNodes.length > refChildNodes.length) { 241 - const child = element.lastChild; 242 - if (child) this.#removeNode(child); 243 - } 244 - } 245 - #morphChildElement([child, reference], parent) { 246 - if (!(this.#beforeNodeMorphed?.(child, writableNode(reference)) ?? true)) return; 247 - const refIdSet = this.#idMap.get(reference); 248 - // Generate the array in advance of the loop 249 - const refSetArray = refIdSet ? [...refIdSet] : []; 250 - let currentNode = child; 251 - let nextMatchByTagName = null; 252 - // Try find a match by idSet, while also looking out for the next best match by tagName. 253 - while (currentNode) { 254 - if (isElement(currentNode)) { 255 - const id = currentNode.id; 256 - if (!nextMatchByTagName && currentNode.localName === reference.localName) { 257 - nextMatchByTagName = currentNode; 258 - } 259 - if (id !== "") { 260 - if (id === reference.id) { 261 - this.#insertBefore(parent, currentNode, child); 262 - return this.#morphNode([currentNode, reference]); 263 - } else { 264 - const currentIdSet = this.#idMap.get(currentNode); 265 - if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) { 266 - this.#insertBefore(parent, currentNode, child); 267 - return this.#morphNode([currentNode, reference]); 268 - } 269 - } 270 - } 271 - } 272 - currentNode = currentNode.nextSibling; 273 - } 274 - if (nextMatchByTagName) { 275 - this.#insertBefore(parent, nextMatchByTagName, child); 276 - this.#morphNode([nextMatchByTagName, reference]); 277 - } else { 278 - const newNode = reference.cloneNode(true); 279 - if (this.#beforeNodeAdded?.(newNode) ?? true) { 280 - this.#insertBefore(parent, newNode, child); 281 - this.#afterNodeAdded?.(newNode); 282 - } 283 - } 284 - this.#afterNodeMorphed?.(child, writableNode(reference)); 285 - } 286 - #updateProperty(node, propertyName, newValue) { 287 - const previousValue = node[propertyName]; 288 - if (previousValue !== newValue && (this.#beforePropertyUpdated?.(node, propertyName, newValue) ?? true)) { 289 - node[propertyName] = newValue; 290 - this.#afterPropertyUpdated?.(node, propertyName, previousValue); 291 - } 292 - } 293 - #replaceNode(node, newNode) { 294 - if ((this.#beforeNodeRemoved?.(node) ?? true) && (this.#beforeNodeAdded?.(newNode) ?? true)) { 295 - node.replaceWith(newNode); 296 - this.#afterNodeAdded?.(newNode); 297 - this.#afterNodeRemoved?.(node); 298 - } 299 - } 300 - #insertBefore(parent, node, insertionPoint) { 301 - if (node === insertionPoint) return; 302 - if (isElement(node)) { 303 - const sensitivity = this.#sensivityMap.get(node) ?? 0; 304 - if (sensitivity > 0) { 305 - let previousNode = node.previousSibling; 306 - while (previousNode) { 307 - const previousNodeSensitivity = this.#sensivityMap.get(previousNode) ?? 0; 308 - if (previousNodeSensitivity < sensitivity) { 309 - parent.insertBefore(previousNode, node.nextSibling); 310 - if (previousNode === insertionPoint) return; 311 - previousNode = node.previousSibling; 312 - } else break; 313 - } 314 - } 315 - } 316 - parent.insertBefore(node, insertionPoint); 317 - } 318 - #appendChild(node, newNode) { 319 - if (this.#beforeNodeAdded?.(newNode) ?? true) { 320 - node.appendChild(newNode); 321 - this.#afterNodeAdded?.(newNode); 322 - } 323 - } 324 - #removeNode(node) { 325 - if (this.#beforeNodeRemoved?.(node) ?? true) { 326 - node.remove(); 327 - this.#afterNodeRemoved?.(node); 328 - } 329 - } 27 + #idMap; 28 + #sensivityMap; 29 + #ignoreActiveValue; 30 + #preserveModifiedValues; 31 + #beforeNodeMorphed; 32 + #afterNodeMorphed; 33 + #beforeNodeAdded; 34 + #afterNodeAdded; 35 + #beforeNodeRemoved; 36 + #afterNodeRemoved; 37 + #beforeAttributeUpdated; 38 + #afterAttributeUpdated; 39 + #beforePropertyUpdated; 40 + #afterPropertyUpdated; 41 + constructor(options = {}) { 42 + this.#idMap = new WeakMap(); 43 + this.#sensivityMap = new WeakMap(); 44 + this.#ignoreActiveValue = options.ignoreActiveValue || false; 45 + this.#preserveModifiedValues = options.preserveModifiedValues || false; 46 + this.#beforeNodeMorphed = options.beforeNodeMorphed; 47 + this.#afterNodeMorphed = options.afterNodeMorphed; 48 + this.#beforeNodeAdded = options.beforeNodeAdded; 49 + this.#afterNodeAdded = options.afterNodeAdded; 50 + this.#beforeNodeRemoved = options.beforeNodeRemoved; 51 + this.#afterNodeRemoved = options.afterNodeRemoved; 52 + this.#beforeAttributeUpdated = options.beforeAttributeUpdated; 53 + this.#afterAttributeUpdated = options.afterAttributeUpdated; 54 + this.#beforePropertyUpdated = options.beforePropertyUpdated; 55 + this.#afterPropertyUpdated = options.afterPropertyUpdated; 56 + } 57 + morph(pair) { 58 + this.#withAriaBusy(pair[0], () => { 59 + if (isParentNodePair(pair)) 60 + this.#buildMaps(pair); 61 + this.#morphNode(pair); 62 + }); 63 + } 64 + morphInner(pair) { 65 + this.#withAriaBusy(pair[0], () => { 66 + if (isMatchingElementPair(pair)) { 67 + this.#buildMaps(pair); 68 + this.#morphMatchingElementContent(pair); 69 + } 70 + else { 71 + throw new Error("[Morphlex] You can only do an inner morph with matching elements."); 72 + } 73 + }); 74 + } 75 + #withAriaBusy(node, block) { 76 + if (isElement(node)) { 77 + const originalAriaBusy = node.ariaBusy; 78 + node.ariaBusy = "true"; 79 + block(); 80 + node.ariaBusy = originalAriaBusy; 81 + } 82 + else 83 + block(); 84 + } 85 + #buildMaps([node, reference]) { 86 + this.#mapIdSets(node); 87 + this.#mapIdSets(reference); 88 + this.#mapSensivity(node); 89 + } 90 + #mapSensivity(node) { 91 + const sensitiveElements = node.querySelectorAll("audio,canvas,embed,iframe,input,object,textarea,video"); 92 + const sensitiveElementsLength = sensitiveElements.length; 93 + for (let i = 0; i < sensitiveElementsLength; i++) { 94 + const sensitiveElement = sensitiveElements[i]; 95 + let sensivity = 0; 96 + if (isInput(sensitiveElement) || isTextArea(sensitiveElement)) { 97 + sensivity++; 98 + if (sensitiveElement.value !== sensitiveElement.defaultValue) 99 + sensivity++; 100 + if (sensitiveElement === document.activeElement) 101 + sensivity++; 102 + } 103 + else { 104 + sensivity += 3; 105 + if (isMedia(sensitiveElement) && !sensitiveElement.ended) { 106 + if (!sensitiveElement.paused) 107 + sensivity++; 108 + if (sensitiveElement.currentTime > 0) 109 + sensivity++; 110 + } 111 + } 112 + let current = sensitiveElement; 113 + while (current) { 114 + this.#sensivityMap.set(current, (this.#sensivityMap.get(current) || 0) + sensivity); 115 + if (current === node) 116 + break; 117 + current = current.parentElement; 118 + } 119 + } 120 + } 121 + // For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements. 122 + #mapIdSets(node) { 123 + const elementsWithIds = node.querySelectorAll("[id]"); 124 + const elementsWithIdsLength = elementsWithIds.length; 125 + for (let i = 0; i < elementsWithIdsLength; i++) { 126 + const elementWithId = elementsWithIds[i]; 127 + const id = elementWithId.id; 128 + // Ignore empty IDs 129 + if (id === "") 130 + continue; 131 + let current = elementWithId; 132 + while (current) { 133 + const idSet = this.#idMap.get(current); 134 + idSet ? idSet.add(id) : this.#idMap.set(current, new Set([id])); 135 + if (current === node) 136 + break; 137 + current = current.parentElement; 138 + } 139 + } 140 + } 141 + // This is where we actually morph the nodes. The `morph` function (above) exists only to set up the `idMap`. 142 + #morphNode(pair) { 143 + if (isMatchingElementPair(pair)) 144 + this.#morphMatchingElementNode(pair); 145 + else 146 + this.#morphOtherNode(pair); 147 + } 148 + #morphMatchingElementNode(pair) { 149 + const [node, reference] = pair; 150 + if (!(this.#beforeNodeMorphed?.(node, writableNode(reference)) ?? true)) 151 + return; 152 + if (node.hasAttributes() || reference.hasAttributes()) 153 + this.#morphAttributes(pair); 154 + // TODO: Should use a branded pair here. 155 + this.#morphMatchingElementContent(pair); 156 + this.#afterNodeMorphed?.(node, writableNode(reference)); 157 + } 158 + #morphOtherNode([node, reference]) { 159 + if (!(this.#beforeNodeMorphed?.(node, writableNode(reference)) ?? true)) 160 + return; 161 + if (node.nodeType === reference.nodeType && node.nodeValue !== null && reference.nodeValue !== null) { 162 + // Handle text nodes, comments, and CDATA sections. 163 + this.#updateProperty(node, "nodeValue", reference.nodeValue); 164 + } 165 + else 166 + this.#replaceNode(node, reference.cloneNode(true)); 167 + this.#afterNodeMorphed?.(node, writableNode(reference)); 168 + } 169 + #morphMatchingElementContent(pair) { 170 + const [node, reference] = pair; 171 + if (isHead(node)) { 172 + // We can pass the reference as a head here becuase we know it's the same as the node. 173 + this.#morphHeadContents(pair); 174 + } 175 + else if (node.hasChildNodes() || reference.hasChildNodes()) 176 + this.#morphChildNodes(pair); 177 + } 178 + #morphHeadContents([node, reference]) { 179 + const refChildNodesMap = new Map(); 180 + // Generate a map of the reference head element’s child nodes, keyed by their outerHTML. 181 + const referenceChildrenLength = reference.children.length; 182 + for (let i = 0; i < referenceChildrenLength; i++) { 183 + const child = reference.children[i]; 184 + refChildNodesMap.set(child.outerHTML, child); 185 + } 186 + const nodeChildrenLength = node.children.length; 187 + for (let i = 0; i < nodeChildrenLength; i++) { 188 + const child = node.children[i]; 189 + const key = child.outerHTML; 190 + const refChild = refChildNodesMap.get(key); 191 + // If the child is in the reference map already, we don’t need to add it later. 192 + // If it’s not in the map, we need to remove it from the node. 193 + refChild ? refChildNodesMap.delete(key) : this.#removeNode(child); 194 + } 195 + // Any remaining nodes in the map should be appended to the head. 196 + for (const refChild of refChildNodesMap.values()) 197 + this.#appendChild(node, refChild.cloneNode(true)); 198 + } 199 + #morphAttributes([element, reference]) { 200 + // Remove any excess attributes from the element that aren’t present in the reference. 201 + for (const { name, value } of element.attributes) { 202 + if (!reference.hasAttribute(name) && (this.#beforeAttributeUpdated?.(element, name, null) ?? true)) { 203 + element.removeAttribute(name); 204 + this.#afterAttributeUpdated?.(element, name, value); 205 + } 206 + } 207 + // Copy attributes from the reference to the element, if they don’t already match. 208 + for (const { name, value } of reference.attributes) { 209 + const previousValue = element.getAttribute(name); 210 + if (previousValue !== value && (this.#beforeAttributeUpdated?.(element, name, value) ?? true)) { 211 + element.setAttribute(name, value); 212 + this.#afterAttributeUpdated?.(element, name, previousValue); 213 + } 214 + } 215 + // For certain types of elements, we need to do some extra work to ensure 216 + // the element’s state matches the reference elements’ state. 217 + if (isInput(element) && isInput(reference)) { 218 + this.#updateProperty(element, "checked", reference.checked); 219 + this.#updateProperty(element, "disabled", reference.disabled); 220 + this.#updateProperty(element, "indeterminate", reference.indeterminate); 221 + if (element.type !== "file" && 222 + !(this.#ignoreActiveValue && document.activeElement === element) && 223 + !(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)) { 224 + this.#updateProperty(element, "value", reference.value); 225 + } 226 + } 227 + else if (isOption(element) && isOption(reference)) { 228 + this.#updateProperty(element, "selected", reference.selected); 229 + } 230 + else if (isTextArea(element) && 231 + isTextArea(reference) && 232 + !(this.#ignoreActiveValue && document.activeElement === element) && 233 + !(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)) { 234 + this.#updateProperty(element, "value", reference.value); 235 + const text = element.firstElementChild; 236 + if (text) 237 + this.#updateProperty(text, "textContent", reference.value); 238 + } 239 + } 240 + // Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match. 241 + #morphChildNodes(pair) { 242 + const [element, reference] = pair; 243 + const childNodes = element.childNodes; 244 + const refChildNodes = reference.childNodes; 245 + for (let i = 0; i < refChildNodes.length; i++) { 246 + const child = childNodes[i]; 247 + const refChild = refChildNodes[i]; 248 + if (child && refChild) { 249 + const pair = [child, refChild]; 250 + if (isMatchingElementPair(pair)) { 251 + if (isHead(pair[0])) { 252 + this.#morphHeadContents(pair); 253 + } 254 + else { 255 + this.#morphChildElement(pair, element); 256 + } 257 + } 258 + else 259 + this.#morphOtherNode(pair); 260 + } 261 + else if (refChild) { 262 + this.#appendChild(element, refChild.cloneNode(true)); 263 + } 264 + else if (child) { 265 + this.#removeNode(child); 266 + } 267 + } 268 + // Clean up any excess nodes that may be left over 269 + while (childNodes.length > refChildNodes.length) { 270 + const child = element.lastChild; 271 + if (child) 272 + this.#removeNode(child); 273 + } 274 + } 275 + #morphChildElement([child, reference], parent) { 276 + if (!(this.#beforeNodeMorphed?.(child, writableNode(reference)) ?? true)) 277 + return; 278 + const refIdSet = this.#idMap.get(reference); 279 + // Generate the array in advance of the loop 280 + const refSetArray = refIdSet ? [...refIdSet] : []; 281 + let currentNode = child; 282 + let nextMatchByTagName = null; 283 + // Try find a match by idSet, while also looking out for the next best match by tagName. 284 + while (currentNode) { 285 + if (isElement(currentNode)) { 286 + const id = currentNode.id; 287 + if (!nextMatchByTagName && currentNode.localName === reference.localName) { 288 + nextMatchByTagName = currentNode; 289 + } 290 + if (id !== "") { 291 + if (id === reference.id) { 292 + this.#insertBefore(parent, currentNode, child); 293 + return this.#morphNode([currentNode, reference]); 294 + } 295 + else { 296 + const currentIdSet = this.#idMap.get(currentNode); 297 + if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) { 298 + this.#insertBefore(parent, currentNode, child); 299 + return this.#morphNode([currentNode, reference]); 300 + } 301 + } 302 + } 303 + } 304 + currentNode = currentNode.nextSibling; 305 + } 306 + if (nextMatchByTagName) { 307 + this.#insertBefore(parent, nextMatchByTagName, child); 308 + this.#morphNode([nextMatchByTagName, reference]); 309 + } 310 + else { 311 + const newNode = reference.cloneNode(true); 312 + if (this.#beforeNodeAdded?.(newNode) ?? true) { 313 + this.#insertBefore(parent, newNode, child); 314 + this.#afterNodeAdded?.(newNode); 315 + } 316 + } 317 + this.#afterNodeMorphed?.(child, writableNode(reference)); 318 + } 319 + #updateProperty(node, propertyName, newValue) { 320 + const previousValue = node[propertyName]; 321 + if (previousValue !== newValue && (this.#beforePropertyUpdated?.(node, propertyName, newValue) ?? true)) { 322 + node[propertyName] = newValue; 323 + this.#afterPropertyUpdated?.(node, propertyName, previousValue); 324 + } 325 + } 326 + #replaceNode(node, newNode) { 327 + if ((this.#beforeNodeRemoved?.(node) ?? true) && (this.#beforeNodeAdded?.(newNode) ?? true)) { 328 + node.replaceWith(newNode); 329 + this.#afterNodeAdded?.(newNode); 330 + this.#afterNodeRemoved?.(node); 331 + } 332 + } 333 + #insertBefore(parent, node, insertionPoint) { 334 + if (node === insertionPoint) 335 + return; 336 + if (isElement(node)) { 337 + const sensitivity = this.#sensivityMap.get(node) ?? 0; 338 + if (sensitivity > 0) { 339 + let previousNode = node.previousSibling; 340 + while (previousNode) { 341 + const previousNodeSensitivity = this.#sensivityMap.get(previousNode) ?? 0; 342 + if (previousNodeSensitivity < sensitivity) { 343 + parent.insertBefore(previousNode, node.nextSibling); 344 + if (previousNode === insertionPoint) 345 + return; 346 + previousNode = node.previousSibling; 347 + } 348 + else 349 + break; 350 + } 351 + } 352 + } 353 + parent.insertBefore(node, insertionPoint); 354 + } 355 + #appendChild(node, newNode) { 356 + if (this.#beforeNodeAdded?.(newNode) ?? true) { 357 + node.appendChild(newNode); 358 + this.#afterNodeAdded?.(newNode); 359 + } 360 + } 361 + #removeNode(node) { 362 + if (this.#beforeNodeRemoved?.(node) ?? true) { 363 + node.remove(); 364 + this.#afterNodeRemoved?.(node); 365 + } 366 + } 330 367 } 331 368 function writableNode(node) { 332 - return node; 369 + return node; 333 370 } 334 371 function isMatchingElementPair(pair) { 335 - const [a, b] = pair; 336 - return isElement(a) && isElement(b) && a.localName === b.localName; 372 + const [a, b] = pair; 373 + return isElement(a) && isElement(b) && a.localName === b.localName; 337 374 } 338 375 function isParentNodePair(pair) { 339 - return isParentNode(pair[0]) && isParentNode(pair[1]); 376 + return isParentNode(pair[0]) && isParentNode(pair[1]); 340 377 } 341 378 function isElement(node) { 342 - return node.nodeType === 1; 379 + return node.nodeType === 1; 343 380 } 344 381 function isMedia(element) { 345 - return element.localName === "video" || element.localName === "audio"; 382 + return element.localName === "video" || element.localName === "audio"; 346 383 } 347 384 function isInput(element) { 348 - return element.localName === "input"; 385 + return element.localName === "input"; 349 386 } 350 387 function isOption(element) { 351 - return element.localName === "option"; 388 + return element.localName === "option"; 352 389 } 353 390 function isTextArea(element) { 354 - return element.localName === "textarea"; 391 + return element.localName === "textarea"; 355 392 } 356 393 function isHead(element) { 357 - return element.localName === "head"; 394 + return element.localName === "head"; 358 395 } 359 396 const parentNodeTypes = new Set([1, 9, 11]); 360 397 function isParentNode(node) { 361 - return parentNodeTypes.has(node.nodeType); 398 + return parentNodeTypes.has(node.nodeType); 362 399 } 363 - //# sourceMappingURL=morphlex.js.map 400 + //# sourceMappingURL=morphlex.js.map
+7 -5
package.json
··· 16 16 "url": "https://github.com/sponsors/joeldrapper" 17 17 }, 18 18 "scripts": { 19 - "test": "bun run web-test-runner test/**/*.test.js --node-resolve", 19 + "test": "vitest run", 20 + "test:watch": "vitest", 21 + "test:ui": "vitest --ui", 20 22 "build": "bun run tsc && bun run prettier --write ./src ./dist", 21 23 "watch": "bun run tsc -w", 22 - "test:watch": "bun run test -- --watch", 23 24 "lint": "bun run prettier --check ./src ./dist ./test", 24 25 "minify": "bun run terser dist/morphlex.js -o dist/morphlex.min.js --config-file terser-config.json", 25 26 "prepare": "bun run build && bun run minify", ··· 28 29 "size": "bun run prepare && bun run gzip-size ./dist/morphlex.min.js --raw --include-original" 29 30 }, 30 31 "devDependencies": { 31 - "@open-wc/testing": "^3.0.0-next.5", 32 - "@web/test-runner": "^0.18.0", 32 + "@vitest/ui": "^4.0.5", 33 33 "gzip-size-cli": "^5.1.0", 34 + "happy-dom": "^20.0.10", 34 35 "prettier": "^3.2.5", 35 36 "terser": "^5.28.1", 36 37 "typescript": "^5.4.2", 37 - "typescript-eslint": "^7.0.2" 38 + "typescript-eslint": "^7.0.2", 39 + "vitest": "^4.0.5" 38 40 } 39 41 }
-5
src/morphlex.ts
··· 107 107 this.#afterAttributeUpdated = options.afterAttributeUpdated; 108 108 this.#beforePropertyUpdated = options.beforePropertyUpdated; 109 109 this.#afterPropertyUpdated = options.afterPropertyUpdated; 110 - 111 - Object.freeze(this); 112 110 } 113 111 114 112 morph(pair: NodeReferencePair<ChildNode>): void { ··· 142 140 this.#mapIdSets(node); 143 141 this.#mapIdSets(reference); 144 142 this.#mapSensivity(node); 145 - 146 - Object.freeze(this.#idMap); 147 - Object.freeze(this.#sensivityMap); 148 143 } 149 144 150 145 #mapSensivity(node: ReadonlyNode<ParentNode>): void {
-78
test/alpine-morph.test.js
··· 1 - import { fixture, html, expect } from "@open-wc/testing"; 2 - import { morph } from "../"; 3 - 4 - // adapted from: https://github.com/alpinejs/alpine/blob/891d68503960a39826e89f2f666d9b1e7ce3f0c9/tests/jest/morph/external.spec.js 5 - describe("alpine-morph", () => { 6 - it("updates text content", async () => { 7 - const a = await fixture(html`<div>foo</div>`); 8 - const b = await fixture(html`<div>bar</div>`); 9 - 10 - morph(a, b); 11 - 12 - expect(a.outerHTML).to.equal(b.outerHTML); 13 - }); 14 - 15 - it("changes inner tag", async () => { 16 - const a = await fixture(html`<div><div>foo</div></div>`); 17 - const b = await fixture(html`<div><span>foo</span></div>`); 18 - 19 - morph(a, b); 20 - 21 - expect(a.outerHTML).to.equal(b.outerHTML); 22 - }); 23 - 24 - it("adds child", async () => { 25 - const a = await fixture(html`<div>foo</div>`); 26 - const b = await fixture( 27 - html`<div> 28 - foo 29 - <h1>baz</h1> 30 - </div>`, 31 - ); 32 - 33 - morph(a, b); 34 - 35 - expect(a.outerHTML).to.equal(b.outerHTML); 36 - }); 37 - 38 - it("removes child", async () => { 39 - const a = await fixture( 40 - html`<div> 41 - foo 42 - <h1>baz</h1> 43 - </div>`, 44 - ); 45 - const b = await fixture(html`<div>foo</div>`); 46 - 47 - morph(a, b); 48 - 49 - expect(a.outerHTML).to.equal(b.outerHTML); 50 - }); 51 - 52 - it("adds attribute", async () => { 53 - const a = await fixture(html`<div>foo</div>`); 54 - const b = await fixture(html`<div foo="bar">foo</div>`); 55 - 56 - morph(a, b); 57 - 58 - expect(a.outerHTML).to.equal(b.outerHTML); 59 - }); 60 - 61 - it("removes attribute", async () => { 62 - const a = await fixture(html`<div foo="bar">foo</div>`); 63 - const b = await fixture(html`<div>foo</div>`); 64 - 65 - morph(a, b); 66 - 67 - expect(a.outerHTML).to.equal(b.outerHTML); 68 - }); 69 - 70 - it("changes attribute", async () => { 71 - const a = await fixture(html`<div foo="bar">foo</div>`); 72 - const b = await fixture(html`<div foo="baz">foo</div>`); 73 - 74 - morph(a, b); 75 - 76 - expect(a.outerHTML).to.equal(b.outerHTML); 77 - }); 78 - });
-134
test/morphdom.test.js
··· 1 - import { fixture, html, expect } from "@open-wc/testing"; 2 - import { morph } from "../"; 3 - 4 - // adapted from: https://github.com/patrick-steele-idem/morphdom/blob/e98d69e125cda814dd6d1ba71d6c7c9d93edc01e/test/browser/test.js 5 - describe("morphdom", () => { 6 - it("should transform a simple el", async () => { 7 - const a = await fixture(html`<div class="foo"></div>`); 8 - const b = await fixture(html`<div class="bar"></div>`); 9 - 10 - morph(a, b); 11 - 12 - expect(a.outerHTML).to.equal(b.outerHTML); 13 - expect(a.className).to.equal("bar"); 14 - }); 15 - 16 - // This test is broken — `a` is just a `<div>` and `b` is `null` 17 - it.skip("can wipe out body", async () => { 18 - const a = await fixture( 19 - html`<body> 20 - <div></div> 21 - </body>`, 22 - ); 23 - const b = await fixture(html`<body></body>`); 24 - 25 - morph(a, b); 26 - 27 - expect(a.outerHTML).to.equal(b.outerHTML); 28 - expect(a.nodeName).to.equal("BODY"); 29 - expect(a.children.length).to.equal(0); 30 - }); 31 - 32 - it("does morph child with dup id", async () => { 33 - const a = await fixture(html`<div id="el-1" class="foo"><div id="el-1">A</div></div>`); 34 - const b = await fixture(html`<div id="el-1" class="bar"><div id="el-1">B</div></div>`); 35 - 36 - morph(a, b); 37 - 38 - expect(a.outerHTML).to.equal(b.outerHTML); 39 - expect(a.className).to.equal("bar"); 40 - expect(a.id).to.equal("el-1"); 41 - expect(a.firstElementChild.id).to.equal("el-1"); 42 - expect(a.firstElementChild.textContent).to.equal("B"); 43 - }); 44 - 45 - it.skip("does keep inner dup id", async () => { 46 - const a = await fixture(html`<div id="el-1" class="foo"><div id="el-1">A</div></div>`); 47 - const b = await fixture(html`<div id="el-1" class="zoo"><div id="el-inner">B</div></div>`); 48 - 49 - morph(a, b); 50 - 51 - expect(a.outerHTML).to.equal(b.outerHTML); 52 - expect(a.className).to.equal("zoo"); 53 - expect(a.id).to.equal("el-1"); 54 - expect(a.children[0].id).to.equal("el-1"); 55 - expect(a.children[0].textContent).to.equal("A"); 56 - expect(a.children[1].id).to.equal("el-inner"); 57 - expect(a.children[1].textContent).to.equal("B"); 58 - }); 59 - 60 - it.skip("nested duplicate ids are morphed correctly", async () => { 61 - const a = await fixture( 62 - html`<div> 63 - <p id="hi" class="foo">A</p> 64 - <p id="hi" class="bar">B</p> 65 - </div>`, 66 - ); 67 - const b = await fixture(html`<div><p id="hi" class="foo">A</p></div>`); 68 - 69 - morph(a, b); 70 - 71 - expect(a.outerHTML).to.equal(b.outerHTML); 72 - expect(a.children.length).to.equal(2); 73 - expect(a.children[0].id).to.equal("hi"); 74 - expect(a.children[0].className).to.equal("foo"); 75 - expect(a.children[0].textContent).to.equal("A"); 76 - // TODO: these should not be here 77 - expect(a.children[1].id).to.equal("hi"); 78 - expect(a.children[1].className).to.equal("bar"); 79 - expect(a.children[1].textContent).to.equal("B"); 80 - }); 81 - 82 - it("incompatible matching ids are morphed correctly", async () => { 83 - const a = await fixture( 84 - html`<div> 85 - <h1 id="foo" class="foo">A</h1> 86 - <h2 id="matching" class="bar">B</h2> 87 - </div>`, 88 - ); 89 - const b = await fixture(html`<div><h1 id="matching" class="baz">C</h1></div>`); 90 - 91 - morph(a, b); 92 - 93 - expect(a.outerHTML).to.equal(b.outerHTML); 94 - expect(a.children.length).to.equal(1); 95 - expect(a.children[0].id).to.equal("matching"); 96 - expect(a.children[0].className).to.equal("baz"); 97 - expect(a.children[0].textContent).to.equal("C"); 98 - }); 99 - 100 - it("should transform a text input el", async () => { 101 - const a = await fixture(html`<input type="text" value="Hello World" />`); 102 - const b = await fixture(html`<input type="text" value="Hello World 2" />`); 103 - 104 - morph(a, b); 105 - 106 - expect(a.outerHTML).to.equal(b.outerHTML); 107 - expect(a.value).to.equal("Hello World 2"); 108 - }); 109 - 110 - it("should transform a checkbox input type attribute", async () => { 111 - const a = await fixture(html`<input type="checkbox" checked="" />`); 112 - a.checked = false; 113 - const b = await fixture(html`<input type="text" checked="" />`); 114 - 115 - morph(a, b); 116 - 117 - expect(a.outerHTML).to.equal(b.outerHTML); 118 - expect(a.checked).to.equal(true); 119 - expect(a.type).to.equal("text"); 120 - }); 121 - 122 - it("should transform a checkbox input property", async () => { 123 - const a = await fixture(html`<input type="checkbox" />`); 124 - a.checked = false; 125 - const b = await fixture(html`<input type="checkbox" />`); 126 - b.checked = true; 127 - 128 - morph(a, b); 129 - 130 - expect(a.outerHTML).to.equal(b.outerHTML); 131 - expect(a.checked).to.equal(true); 132 - expect(a.type).to.equal("checkbox"); 133 - }); 134 - });
-62
test/morphlex.test.js
··· 1 - import { fixture, html, expect } from "@open-wc/testing"; 2 - import { morph } from "../"; 3 - 4 - describe("morph", () => { 5 - it("doesn't cause iframes to reload", async () => { 6 - const original = await fixture( 7 - `<div> 8 - <h1></h1> 9 - <iframe id="1" src="https://www.youtube.com/embed/dQw4w9WgXcQ"></iframe> 10 - </div>`, 11 - ); 12 - 13 - const reference = await fixture( 14 - `<div> 15 - <iframe id="1" src="https://www.youtube.com/embed/dQw4w9WgXcQ"></iframe> 16 - <h1></h1> 17 - </div>`, 18 - ); 19 - 20 - const originalIframe = original.querySelector("iframe"); 21 - morph(original, reference); 22 - 23 - expect(original.outerHTML).to.equal(reference.outerHTML); 24 - }); 25 - 26 - it("supports nodes from iframes", async () => { 27 - const iframe = await fixture(html`<iframe></iframe>`); 28 - 29 - const original = await fixture(html`<h1>Hello World</h1>`); 30 - const eventual = iframe.contentDocument.createElement("h1"); 31 - 32 - eventual.textContent = "Hello Joel"; 33 - 34 - iframe.contentDocument.body.appendChild(eventual); 35 - 36 - morph(original, eventual); 37 - 38 - expect(original.textContent).to.equal("Hello Joel"); 39 - }); 40 - 41 - it("syncs text content", async () => { 42 - const a = await fixture(html`<h1></h1>`); 43 - const b = await fixture(html`<h1>Hello</h1>`); 44 - 45 - new MutationObserver(() => { 46 - throw new Error("The to node was mutated."); 47 - }).observe(b, { attributes: true, childList: true, subtree: true }); 48 - 49 - morph(a, b); 50 - 51 - expect(a.textContent).to.equal(b.textContent); 52 - }); 53 - 54 - it("removes excess elements", async () => { 55 - const a = await fixture(html`<h1><div></div></h1>`); 56 - const b = await fixture(html`<h1></h1>`); 57 - 58 - morph(a, b); 59 - 60 - expect(a.outerHTML).to.equal(b.outerHTML); 61 - }); 62 - });
+618
test/morphlex.test.ts
··· 1 + import { describe, it, expect, beforeEach, afterEach } from "vitest"; 2 + import { morph, morphInner } from "../src/morphlex"; 3 + 4 + describe("Morphlex Vitest Suite", () => { 5 + let container: HTMLElement; 6 + 7 + beforeEach(() => { 8 + container = document.createElement("div"); 9 + document.body.appendChild(container); 10 + }); 11 + 12 + afterEach(() => { 13 + if (container && container.parentNode) { 14 + container.parentNode.removeChild(container); 15 + } 16 + }); 17 + 18 + describe("morph() - Basic functionality", () => { 19 + it("should update text content", () => { 20 + const original = document.createElement("div"); 21 + original.textContent = "Hello"; 22 + 23 + const reference = document.createElement("div"); 24 + reference.textContent = "World"; 25 + 26 + morph(original, reference); 27 + 28 + expect(original.textContent).toBe("World"); 29 + }); 30 + 31 + it("should accept HTML string as reference", () => { 32 + const original = document.createElement("div"); 33 + original.textContent = "Old"; 34 + 35 + morph(original, "<div>New</div>"); 36 + 37 + expect(original.textContent).toBe("New"); 38 + }); 39 + 40 + it("should preserve element when morphing matching tags", () => { 41 + const original = document.createElement("div"); 42 + original.id = "test"; 43 + const elementRef = original; 44 + 45 + const reference = document.createElement("div"); 46 + reference.textContent = "Updated"; 47 + 48 + morph(original, reference); 49 + 50 + expect(original).toBe(elementRef); 51 + expect(original.textContent).toBe("Updated"); 52 + }); 53 + 54 + it("should replace element when morphing different tags", () => { 55 + const original = document.createElement("div"); 56 + const parent = document.createElement("section"); 57 + parent.appendChild(original); 58 + 59 + const reference = document.createElement("span"); 60 + reference.textContent = "Updated"; 61 + 62 + morph(original, reference); 63 + 64 + expect(parent.querySelector("span")).toBeTruthy(); 65 + expect(parent.querySelector("div")).toBeFalsy(); 66 + }); 67 + }); 68 + 69 + describe("morph() - Attribute handling", () => { 70 + it("should add attributes", () => { 71 + const original = document.createElement("button"); 72 + 73 + const reference = document.createElement("button"); 74 + reference.setAttribute("class", "btn-primary"); 75 + reference.setAttribute("disabled", ""); 76 + 77 + morph(original, reference); 78 + 79 + expect(original.className).toBe("btn-primary"); 80 + expect(original.hasAttribute("disabled")).toBe(true); 81 + }); 82 + 83 + it("should remove attributes", () => { 84 + const original = document.createElement("div"); 85 + original.setAttribute("data-test", "value"); 86 + 87 + const reference = document.createElement("div"); 88 + 89 + morph(original, reference); 90 + 91 + expect(original.hasAttribute("data-test")).toBe(false); 92 + }); 93 + 94 + it("should update attributes", () => { 95 + const original = document.createElement("div"); 96 + original.setAttribute("data-value", "old"); 97 + 98 + const reference = document.createElement("div"); 99 + reference.setAttribute("data-value", "new"); 100 + 101 + morph(original, reference); 102 + 103 + expect(original.getAttribute("data-value")).toBe("new"); 104 + }); 105 + 106 + it("should update class attribute", () => { 107 + const original = document.createElement("div"); 108 + original.className = "old-class"; 109 + 110 + const reference = document.createElement("div"); 111 + reference.className = "new-class"; 112 + 113 + morph(original, reference); 114 + 115 + expect(original.className).toBe("new-class"); 116 + }); 117 + }); 118 + 119 + describe("morph() - Child elements", () => { 120 + it("should add child elements", () => { 121 + const original = document.createElement("ul"); 122 + 123 + const reference = document.createElement("ul"); 124 + const li1 = document.createElement("li"); 125 + li1.textContent = "Item 1"; 126 + const li2 = document.createElement("li"); 127 + li2.textContent = "Item 2"; 128 + reference.appendChild(li1); 129 + reference.appendChild(li2); 130 + 131 + morph(original, reference); 132 + 133 + expect(original.children.length).toBe(2); 134 + expect(original.children[0].textContent).toBe("Item 1"); 135 + }); 136 + 137 + it("should remove excess child elements", () => { 138 + const original = document.createElement("ul"); 139 + original.innerHTML = "<li>A</li><li>B</li><li>C</li>"; 140 + 141 + const reference = document.createElement("ul"); 142 + reference.innerHTML = "<li>A</li>"; 143 + 144 + morph(original, reference); 145 + 146 + expect(original.children.length).toBe(1); 147 + }); 148 + 149 + it("should morph existing child elements", () => { 150 + const original = document.createElement("div"); 151 + const child = document.createElement("span"); 152 + child.textContent = "old"; 153 + original.appendChild(child); 154 + 155 + const reference = document.createElement("div"); 156 + const refChild = document.createElement("span"); 157 + refChild.textContent = "new"; 158 + reference.appendChild(refChild); 159 + 160 + morph(original, reference); 161 + 162 + expect(original.children[0].textContent).toBe("new"); 163 + }); 164 + 165 + it("should handle text nodes", () => { 166 + const original = document.createElement("div"); 167 + original.appendChild(document.createTextNode("Hello")); 168 + 169 + const reference = document.createElement("div"); 170 + reference.appendChild(document.createTextNode("World")); 171 + 172 + morph(original, reference); 173 + 174 + expect(original.textContent).toBe("World"); 175 + }); 176 + 177 + it("should handle mixed text and element nodes", () => { 178 + const original = document.createElement("div"); 179 + original.appendChild(document.createTextNode("Start ")); 180 + const span = document.createElement("span"); 181 + span.textContent = "middle"; 182 + original.appendChild(span); 183 + original.appendChild(document.createTextNode(" end")); 184 + 185 + const reference = document.createElement("div"); 186 + reference.appendChild(document.createTextNode("Start ")); 187 + const refSpan = document.createElement("span"); 188 + refSpan.textContent = "updated"; 189 + reference.appendChild(refSpan); 190 + reference.appendChild(document.createTextNode(" end")); 191 + 192 + morph(original, reference); 193 + 194 + expect(original.textContent).toBe("Start updated end"); 195 + }); 196 + }); 197 + 198 + describe("morph() - Element identity and IDs", () => { 199 + it("should preserve element identity when using IDs", () => { 200 + const original = document.createElement("div"); 201 + original.innerHTML = '<p id="p1">Para 1</p><p id="p2">Para 2</p>'; 202 + 203 + const reference = document.createElement("div"); 204 + reference.innerHTML = '<p id="p2">Para 2</p><p id="p1">Para 1</p>'; 205 + 206 + const p1Original = original.querySelector("#p1"); 207 + 208 + morph(original, reference); 209 + 210 + const p1After = original.querySelector("#p1"); 211 + 212 + expect(p1After).toBe(p1Original); 213 + }); 214 + 215 + it("should reorder elements with IDs correctly", () => { 216 + const original = document.createElement("div"); 217 + original.innerHTML = '<span id="a">A</span><span id="b">B</span><span id="c">C</span>'; 218 + 219 + const reference = document.createElement("div"); 220 + reference.innerHTML = '<span id="c">C</span><span id="a">A</span><span id="b">B</span>'; 221 + 222 + const originalA = original.querySelector("#a"); 223 + 224 + morph(original, reference); 225 + 226 + const newA = original.querySelector("#a"); 227 + 228 + expect(newA).toBe(originalA); 229 + expect(original.children[1]).toBe(newA); 230 + }); 231 + }); 232 + 233 + describe("morph() - Callbacks", () => { 234 + it("should call beforeNodeMorphed and afterNodeMorphed", () => { 235 + const original = document.createElement("div"); 236 + original.textContent = "Before"; 237 + 238 + const reference = document.createElement("div"); 239 + reference.textContent = "After"; 240 + 241 + let beforeCalled = false; 242 + let afterCalled = false; 243 + 244 + morph(original, reference, { 245 + beforeNodeMorphed: () => { 246 + beforeCalled = true; 247 + return true; 248 + }, 249 + afterNodeMorphed: () => { 250 + afterCalled = true; 251 + }, 252 + }); 253 + 254 + expect(beforeCalled).toBe(true); 255 + expect(afterCalled).toBe(true); 256 + }); 257 + 258 + it("should cancel morphing if beforeNodeMorphed returns false", () => { 259 + const original = document.createElement("div"); 260 + original.textContent = "Original"; 261 + 262 + const reference = document.createElement("div"); 263 + reference.textContent = "Reference"; 264 + 265 + morph(original, reference, { 266 + beforeNodeMorphed: () => false, 267 + }); 268 + 269 + expect(original.textContent).toBe("Original"); 270 + }); 271 + 272 + it("should call beforeNodeAdded and afterNodeAdded", () => { 273 + const original = document.createElement("div"); 274 + 275 + const reference = document.createElement("div"); 276 + const newChild = document.createElement("p"); 277 + newChild.textContent = "New"; 278 + reference.appendChild(newChild); 279 + 280 + let beforeAddCalled = false; 281 + let afterAddCalled = false; 282 + 283 + morph(original, reference, { 284 + beforeNodeAdded: (node) => { 285 + beforeAddCalled = true; 286 + return true; 287 + }, 288 + afterNodeAdded: (node) => { 289 + afterAddCalled = true; 290 + }, 291 + }); 292 + 293 + expect(beforeAddCalled).toBe(true); 294 + expect(afterAddCalled).toBe(true); 295 + }); 296 + 297 + it("should call beforeNodeRemoved and afterNodeRemoved", () => { 298 + const original = document.createElement("div"); 299 + const child = document.createElement("p"); 300 + child.textContent = "To remove"; 301 + original.appendChild(child); 302 + 303 + const reference = document.createElement("div"); 304 + 305 + let beforeRemoveCalled = false; 306 + let afterRemoveCalled = false; 307 + 308 + morph(original, reference, { 309 + beforeNodeRemoved: (node) => { 310 + beforeRemoveCalled = true; 311 + return true; 312 + }, 313 + afterNodeRemoved: (node) => { 314 + afterRemoveCalled = true; 315 + }, 316 + }); 317 + 318 + expect(beforeRemoveCalled).toBe(true); 319 + expect(afterRemoveCalled).toBe(true); 320 + }); 321 + 322 + it("should call attribute update callbacks", () => { 323 + const original = document.createElement("div"); 324 + 325 + const reference = document.createElement("div"); 326 + reference.setAttribute("data-test", "value"); 327 + 328 + let callbackCalled = false; 329 + 330 + morph(original, reference, { 331 + afterAttributeUpdated: (element, attrName, prevValue) => { 332 + if (attrName === "data-test") { 333 + callbackCalled = true; 334 + } 335 + }, 336 + }); 337 + 338 + expect(callbackCalled).toBe(true); 339 + }); 340 + }); 341 + 342 + describe("morph() - Form elements", () => { 343 + it("should update input value", () => { 344 + const original = document.createElement("input") as HTMLInputElement; 345 + original.type = "text"; 346 + original.value = "old"; 347 + 348 + const reference = document.createElement("input") as HTMLInputElement; 349 + reference.type = "text"; 350 + reference.value = "new"; 351 + 352 + morph(original, reference); 353 + 354 + expect(original.value).toBe("new"); 355 + }); 356 + 357 + it("should update checkbox checked state", () => { 358 + const original = document.createElement("input") as HTMLInputElement; 359 + original.type = "checkbox"; 360 + original.checked = false; 361 + 362 + const reference = document.createElement("input") as HTMLInputElement; 363 + reference.type = "checkbox"; 364 + reference.checked = true; 365 + 366 + morph(original, reference); 367 + 368 + expect(original.checked).toBe(true); 369 + }); 370 + 371 + it("should update textarea value", () => { 372 + const original = document.createElement("textarea") as HTMLTextAreaElement; 373 + original.textContent = "old text"; 374 + 375 + const reference = document.createElement("textarea") as HTMLTextAreaElement; 376 + reference.textContent = "new text"; 377 + 378 + morph(original, reference); 379 + 380 + expect(original.textContent).toBe("new text"); 381 + }); 382 + }); 383 + 384 + describe("morph() - Options", () => { 385 + it("should preserve modified values with preserveModifiedValues option", () => { 386 + const original = document.createElement("input") as HTMLInputElement; 387 + original.value = "user-input"; 388 + 389 + const reference = document.createElement("input") as HTMLInputElement; 390 + reference.value = "from-server"; 391 + 392 + morph(original, reference, { preserveModifiedValues: true }); 393 + 394 + expect(original.value).toBe("user-input"); 395 + }); 396 + 397 + it("should ignore active value with ignoreActiveValue option", () => { 398 + const original = document.createElement("input") as HTMLInputElement; 399 + original.value = "active"; 400 + 401 + const reference = document.createElement("input") as HTMLInputElement; 402 + reference.value = "inactive"; 403 + 404 + morph(original, reference, { ignoreActiveValue: true }); 405 + 406 + expect(original).toBeDefined(); 407 + }); 408 + }); 409 + 410 + describe("morphInner() - Basic functionality", () => { 411 + it("should morph inner content only", () => { 412 + const original = document.createElement("div"); 413 + original.id = "container"; 414 + original.innerHTML = "<p>Old</p>"; 415 + 416 + const reference = document.createElement("div"); 417 + reference.innerHTML = "<p>New</p>"; 418 + 419 + morphInner(original, reference); 420 + 421 + expect(original.id).toBe("container"); 422 + expect(original.innerHTML).toBe("<p>New</p>"); 423 + }); 424 + 425 + it("should accept string reference for morphInner", () => { 426 + const original = document.createElement("div"); 427 + original.innerHTML = "<span>Old</span>"; 428 + 429 + const reference = document.createElement("div"); 430 + reference.innerHTML = "<span>New</span>"; 431 + 432 + morphInner(original, reference); 433 + 434 + expect(original.innerHTML).toBe("<span>New</span>"); 435 + }); 436 + 437 + it("should preserve outer element attributes with morphInner", () => { 438 + const original = document.createElement("div"); 439 + original.setAttribute("class", "container"); 440 + original.setAttribute("data-id", "123"); 441 + original.innerHTML = "<p>Old</p>"; 442 + 443 + const reference = document.createElement("div"); 444 + reference.setAttribute("class", "different"); 445 + reference.innerHTML = "<p>New</p>"; 446 + 447 + morphInner(original, reference); 448 + 449 + expect(original.getAttribute("class")).toBe("container"); 450 + expect(original.getAttribute("data-id")).toBe("123"); 451 + expect(original.innerHTML).toBe("<p>New</p>"); 452 + }); 453 + 454 + it("should update multiple children with morphInner", () => { 455 + const original = document.createElement("ul"); 456 + original.innerHTML = "<li>Item 1</li><li>Item 2</li>"; 457 + 458 + const reference = document.createElement("ul"); 459 + reference.innerHTML = "<li>Item A</li><li>Item B</li><li>Item C</li>"; 460 + 461 + morphInner(original, reference); 462 + 463 + expect(original.children.length).toBe(3); 464 + expect(original.children[0].textContent).toBe("Item A"); 465 + expect(original.children[2].textContent).toBe("Item C"); 466 + }); 467 + 468 + it("should empty contents with morphInner when reference has no children", () => { 469 + const original = document.createElement("div"); 470 + original.innerHTML = "<span>Content</span><p>More</p>"; 471 + 472 + const reference = document.createElement("div"); 473 + 474 + morphInner(original, reference); 475 + 476 + expect(original.children.length).toBe(0); 477 + }); 478 + }); 479 + 480 + describe("Edge cases and complex scenarios", () => { 481 + it("should handle empty elements", () => { 482 + const original = document.createElement("div"); 483 + const reference = document.createElement("div"); 484 + 485 + expect(() => morph(original, reference)).not.toThrow(); 486 + expect(original.children.length).toBe(0); 487 + }); 488 + 489 + it("should handle deeply nested structures", () => { 490 + const original = document.createElement("div"); 491 + original.innerHTML = "<div><div><div><span>Deep</span></div></div></div>"; 492 + 493 + const reference = document.createElement("div"); 494 + reference.innerHTML = "<div><div><div><span>Updated</span></div></div></div>"; 495 + 496 + morph(original, reference); 497 + 498 + expect(original.querySelector("span")?.textContent).toBe("Updated"); 499 + }); 500 + 501 + it("should handle special characters in text", () => { 502 + const original = document.createElement("div"); 503 + original.textContent = "Hello & goodbye"; 504 + 505 + const reference = document.createElement("div"); 506 + reference.textContent = 'Special <> characters "test"'; 507 + 508 + morph(original, reference); 509 + 510 + expect(original.textContent).toBe('Special <> characters "test"'); 511 + }); 512 + 513 + it("should handle multiple class names", () => { 514 + const original = document.createElement("div"); 515 + original.classList.add("class1", "class2", "class3"); 516 + 517 + const reference = document.createElement("div"); 518 + reference.classList.add("class2", "class3", "class4"); 519 + 520 + morph(original, reference); 521 + 522 + expect(original.classList.contains("class2")).toBe(true); 523 + expect(original.classList.contains("class3")).toBe(true); 524 + expect(original.classList.contains("class4")).toBe(true); 525 + expect(original.classList.contains("class1")).toBe(false); 526 + }); 527 + 528 + it("should handle element replacement", () => { 529 + const original = document.createElement("div"); 530 + const span = document.createElement("span"); 531 + span.textContent = "Span"; 532 + original.appendChild(span); 533 + 534 + const reference = document.createElement("div"); 535 + const p = document.createElement("p"); 536 + p.textContent = "Paragraph"; 537 + reference.appendChild(p); 538 + 539 + morph(original, reference); 540 + 541 + expect(original.children[0].nodeName).toBe("P"); 542 + expect(original.children[0].textContent).toBe("Paragraph"); 543 + }); 544 + 545 + it("should handle list updates with ID preservation", () => { 546 + const original = document.createElement("ul"); 547 + original.innerHTML = '<li id="item-1">Item 1</li><li id="item-2">Item 2</li><li id="item-3">Item 3</li>'; 548 + 549 + const item2Ref = original.querySelector("#item-2"); 550 + 551 + const reference = document.createElement("ul"); 552 + reference.innerHTML = 553 + '<li id="item-1">Item 1</li><li id="item-3">Item 3</li><li id="item-2">Item 2</li><li id="item-4">Item 4</li>'; 554 + 555 + morph(original, reference); 556 + 557 + expect(original.querySelector("#item-2")).toBe(item2Ref); 558 + expect(original.children.length).toBe(4); 559 + }); 560 + 561 + it("should handle complex page-like structure", () => { 562 + const original = document.createElement("main"); 563 + original.innerHTML = ` 564 + <header id="header"> 565 + <h1>Title</h1> 566 + </header> 567 + <article> 568 + <p>Old paragraph</p> 569 + </article> 570 + `; 571 + 572 + const reference = document.createElement("main"); 573 + reference.innerHTML = ` 574 + <header id="header"> 575 + <h1>New Title</h1> 576 + </header> 577 + <article> 578 + <p>New paragraph</p> 579 + <p>Another paragraph</p> 580 + </article> 581 + `; 582 + 583 + const headerRef = original.querySelector("#header"); 584 + 585 + morph(original, reference); 586 + 587 + expect(original.querySelector("#header")).toBe(headerRef); 588 + expect(original.querySelector("h1")?.textContent).toBe("New Title"); 589 + expect(original.querySelectorAll("article p").length).toBe(2); 590 + }); 591 + 592 + it("should handle nested element morphing with updates", () => { 593 + const original = document.createElement("div"); 594 + original.innerHTML = "<p>Old <span>content</span></p>"; 595 + 596 + const reference = document.createElement("div"); 597 + reference.innerHTML = "<p>New <span>text</span></p>"; 598 + 599 + morph(original, reference); 600 + 601 + expect(original.innerHTML).toBe("<p>New <span>text</span></p>"); 602 + }); 603 + 604 + it("should preserve element reference through morph", () => { 605 + const original = document.createElement("div"); 606 + original.textContent = "Original"; 607 + 608 + const reference = document.createElement("div"); 609 + reference.textContent = "Updated"; 610 + 611 + const originalRef = original; 612 + morph(original, reference); 613 + 614 + expect(original).toBe(originalRef); 615 + expect(original.textContent).toBe("Updated"); 616 + }); 617 + }); 618 + });
-789
test/nanomorph.test.js
··· 1 - import { fixture, html, expect } from "@open-wc/testing"; 2 - import { morph } from "../"; 3 - 4 - // adapted from: https://github.com/choojs/nanomorph/blob/b8088d03b1113bddabff8aa0e44bd8db88d023c7/test/diff.js 5 - describe("nanomorph", () => { 6 - describe("root level", () => { 7 - it.skip("should replace a node", async () => { 8 - // Note: I have a feeling this actually works, but the test is wrong, 9 - // since the test asserts on the original node, which is replaced. 10 - const a = await fixture(html`<p>hello world</p>`); 11 - const b = await fixture(html`<div>hello world</div>`); 12 - 13 - morph(a, b); 14 - 15 - expect(a.outerHTML).to.equal(b.outerHTML); 16 - }); 17 - 18 - it("should replace a component", async () => { 19 - const a = await fixture(html`<div data-nanomorph-component-id="a">hello world</div>`); 20 - const b = await fixture(html`<div data-nanomorph-component-id="b">bye moon</div>`); 21 - 22 - morph(a, b); 23 - 24 - expect(a.outerHTML).to.equal(b.outerHTML); 25 - }); 26 - 27 - it("should morph a node", async () => { 28 - const a = await fixture(html`<p>hello world</p>`); 29 - const b = await fixture(html`<p>hello you</p>`); 30 - 31 - morph(a, b); 32 - 33 - expect(a.outerHTML).to.equal(b.outerHTML); 34 - }); 35 - 36 - it("should morph a node with namespaced attribute", async () => { 37 - const a = await fixture(html`<svg><use xlink:href="#heybooboo"></use></svg>`); 38 - const b = await fixture(html`<svg><use xlink:href="#boobear"></use></svg>`); 39 - 40 - morph(a, b); 41 - 42 - expect(a.outerHTML).to.equal(b.outerHTML); 43 - }); 44 - 45 - it("should ignore if node is same", async () => { 46 - const a = await fixture(html`<p>hello world</p>`); 47 - 48 - morph(a, a); 49 - 50 - expect(a.outerHTML).to.equal(a.outerHTML); 51 - }); 52 - }); 53 - 54 - describe("nested", () => { 55 - it("should replace a node", async () => { 56 - const a = await fixture(html`<main><p>hello world</p></main>`); 57 - const b = await fixture(html`<main><div>hello world</div></main>`); 58 - 59 - morph(a, b); 60 - 61 - expect(a.outerHTML).to.equal(b.outerHTML); 62 - }); 63 - 64 - it("should replace a node", async () => { 65 - const a = await fixture(html`<main><p>hello world</p></main>`); 66 - const b = await fixture(html`<main><p>hello you</p></main>`); 67 - 68 - morph(a, b); 69 - 70 - expect(a.outerHTML).to.equal(b.outerHTML); 71 - }); 72 - 73 - it("should replace a node", async () => { 74 - const a = await fixture(html`<main><p>hello world</p></main>`); 75 - 76 - morph(a, a); 77 - 78 - expect(a.outerHTML).to.equal(a.outerHTML); 79 - }); 80 - 81 - it("should append a node", async () => { 82 - const a = await fixture(html`<main></main>`); 83 - const b = await fixture(html`<main><p>hello you</p></main>`); 84 - 85 - morph(a, b); 86 - 87 - expect(a.outerHTML).to.equal(b.outerHTML); 88 - }); 89 - 90 - it("should remove a node", async () => { 91 - const a = await fixture(html`<main><p>hello you</p></main>`); 92 - const b = await fixture(html`<main></main>`); 93 - 94 - morph(a, b); 95 - 96 - expect(a.outerHTML).to.equal(b.outerHTML); 97 - }); 98 - 99 - it.skip("should update child nodes", async () => { 100 - const a = await fixture(html`<main><p>hello world</p></main>`); 101 - const b = await fixture(html`<section><p>hello you</p></section>`); 102 - 103 - morph(a, b, { childrenOnly: true }); 104 - 105 - expect(a.outerHTML).to.equal("<main><p>hello you</p></main>"); 106 - }); 107 - }); 108 - 109 - describe("values", () => { 110 - it("if new tree has no value and old tree does, remove value", async () => { 111 - const a = await fixture(html`<input type="text" value="howdy" />`); 112 - const b = await fixture(html`<input type="text" />`); 113 - 114 - morph(a, b); 115 - 116 - expect(a.outerHTML).to.equal(b.outerHTML); 117 - expect(a.getAttribute("value")).to.equal(null); 118 - expect(a.value).to.equal(""); 119 - }); 120 - 121 - it.skip("if new tree has null value and old tree does, remove value", async () => { 122 - const a = await fixture(html`<input type="text" value="howdy" />`); 123 - const b = await fixture(html`<input type="text" value=${null} />`); 124 - 125 - morph(a, b); 126 - 127 - expect(a.outerHTML).to.equal(b.outerHTML); 128 - expect(a.getAttribute("value")).to.equal(null); 129 - expect(a.value).to.equal(""); 130 - }); 131 - 132 - it("if new tree has value in HTML and old tree does too, set value from new tree", async () => { 133 - const a = await fixture(html`<input type="text" value="howdy" />`); 134 - const b = await fixture(html`<input type="text" value="hi" />`); 135 - 136 - morph(a, b); 137 - 138 - expect(a.outerHTML).to.equal(b.outerHTML); 139 - expect(a.value).to.equal("hi"); 140 - }); 141 - 142 - it("if new tree has value from mutation and old tree does too, set value from new tree", async () => { 143 - const a = await fixture(html`<input type="text" />`); 144 - a.value = "howdy"; 145 - const b = await fixture(html`<input type="text" />`); 146 - b.value = "hi"; 147 - 148 - morph(a, b); 149 - 150 - expect(a.outerHTML).to.equal(b.outerHTML); 151 - expect(a.value).to.equal("hi"); 152 - }); 153 - 154 - it("if new tree has value in HTML and old tree does from mutation, set value from new tree", async () => { 155 - const a = await fixture(html`<input type="text" value="howdy" />`); 156 - const b = await fixture(html`<input type="text" />`); 157 - b.value = "hi"; 158 - 159 - morph(a, b); 160 - 161 - expect(a.outerHTML).to.equal(b.outerHTML); 162 - expect(a.value).to.equal("hi"); 163 - }); 164 - 165 - it("if new tree has value from mutation and old tree does in HTML, set value from new tree", async () => { 166 - const a = await fixture(html`<input type="text" value="howdy" />`); 167 - a.value = "howdy"; 168 - const b = await fixture(html`<input type="text" value="hi" />`); 169 - 170 - morph(a, b); 171 - 172 - expect(a.outerHTML).to.equal(b.outerHTML); 173 - expect(a.value).to.equal("hi"); 174 - }); 175 - }); 176 - 177 - describe("boolean properties", () => { 178 - describe("checked", () => { 179 - it("if new tree has no checked and old tree does, remove value", async () => { 180 - const a = await fixture(html`<input type="checkbox" checked=${true} />`); 181 - const b = await fixture(html`<input type="checkbox" />`); 182 - 183 - morph(a, b); 184 - 185 - expect(a.outerHTML).to.equal(b.outerHTML); 186 - expect(a.checked).to.equal(false); 187 - }); 188 - 189 - it("if new tree has checked and old tree does not, add value", async () => { 190 - const a = await fixture(html`<input type="checkbox" />`); 191 - const b = await fixture(html`<input type="checkbox" checked=${true} />`); 192 - 193 - morph(a, b); 194 - 195 - expect(a.outerHTML).to.equal(b.outerHTML); 196 - expect(a.checked).to.equal(true); 197 - }); 198 - 199 - it("if new tree has checked=false and old tree has checked=true, set value from new tree", async () => { 200 - const a = await fixture(html`<input type="checkbox" checked=${false} />`); 201 - const b = await fixture(html`<input type="checkbox" checked=${true} />`); 202 - 203 - morph(a, b); 204 - 205 - expect(a.outerHTML).to.equal(b.outerHTML); 206 - expect(a.checked).to.equal(true); 207 - }); 208 - 209 - it.skip("if new tree has checked=true and old tree has checked=false, set value from new tree", async () => { 210 - const a = await fixture(html`<input type="checkbox" checked=${true} />`); 211 - const b = await fixture(html`<input type="checkbox" checked=${false} />`); 212 - 213 - morph(a, b); 214 - 215 - expect(a.outerHTML).to.equal(b.outerHTML); 216 - expect(a.checked).to.equal(false); 217 - }); 218 - 219 - it("if new tree has no checked and old tree has checked mutated to true, set value from new tree", async () => { 220 - const a = await fixture(html`<input type="checkbox" />`); 221 - const b = await fixture(html`<input type="checkbox" />`); 222 - b.checked = true; 223 - 224 - morph(a, b); 225 - 226 - expect(a.outerHTML).to.equal(b.outerHTML); 227 - expect(a.checked).to.equal(true); 228 - }); 229 - 230 - it("if new tree has checked=false and old tree has checked mutated to true, set value from new tree", async () => { 231 - const a = await fixture(html`<input type="checkbox" checked=${false} />`); 232 - const b = await fixture(html`<input type="checkbox" />`); 233 - b.checked = true; 234 - 235 - morph(a, b); 236 - 237 - expect(a.outerHTML).to.equal(b.outerHTML); 238 - expect(a.checked).to.equal(true); 239 - }); 240 - 241 - it("if new tree has checked=true and old tree has checked mutated to false, set value from new tree", async () => { 242 - const a = await fixture(html`<input type="checkbox" checked=${true} />`); 243 - const b = await fixture(html`<input type="checkbox" />`); 244 - b.checked = false; 245 - 246 - morph(a, b); 247 - 248 - expect(a.outerHTML).to.equal(b.outerHTML); 249 - expect(a.checked).to.equal(false); 250 - }); 251 - 252 - it("if new tree has no checked and old tree has checked=true, set value from new tree", async () => { 253 - const a = await fixture(html`<input type="checkbox" />`); 254 - const b = await fixture(html`<input type="checkbox" checked=${true} />`); 255 - 256 - morph(a, b); 257 - 258 - expect(a.outerHTML).to.equal(b.outerHTML); 259 - expect(a.checked).to.equal(true); 260 - }); 261 - }); 262 - 263 - describe("disabled", () => { 264 - it("if new tree has no disabled and old tree does, remove value", async () => { 265 - const a = await fixture(html`<input type="checkbox" disabled=${true} />`); 266 - const b = await fixture(html`<input type="checkbox" />`); 267 - 268 - morph(a, b); 269 - 270 - expect(a.outerHTML).to.equal(b.outerHTML); 271 - expect(a.disabled).to.equal(false); 272 - }); 273 - 274 - it("if new tree has disabled and old tree does not, add value", async () => { 275 - const a = await fixture(html`<input type="checkbox" />`); 276 - const b = await fixture(html`<input type="checkbox" disabled=${true} />`); 277 - 278 - morph(a, b); 279 - 280 - expect(a.outerHTML).to.equal(b.outerHTML); 281 - expect(a.disabled).to.equal(true); 282 - }); 283 - 284 - it("if new tree has disabled=false and old tree has disabled=true, set value from new tree", async () => { 285 - const a = await fixture(html`<input type="checkbox" disabled=${false} />`); 286 - const b = await fixture(html`<input type="checkbox" disabled=${true} />`); 287 - 288 - morph(a, b); 289 - 290 - expect(a.outerHTML).to.equal(b.outerHTML); 291 - expect(a.disabled).to.equal(true); 292 - }); 293 - 294 - it.skip("if new tree has disabled=true and old tree has disabled=false, set value from new tree", async () => { 295 - const a = await fixture(html`<input type="checkbox" disabled=${true} />`); 296 - const b = await fixture(html`<input type="checkbox" disabled=${false} />`); 297 - 298 - morph(a, b); 299 - 300 - expect(a.outerHTML).to.equal(b.outerHTML); 301 - expect(a.disabled).to.equal(false); 302 - }); 303 - 304 - it("if new tree has no disabled and old tree has disabled mutated to true, set value from new tree", async () => { 305 - const a = await fixture(html`<input type="checkbox" />`); 306 - const b = await fixture(html`<input type="checkbox" />`); 307 - b.disabled = true; 308 - 309 - morph(a, b); 310 - 311 - expect(a.outerHTML).to.equal(b.outerHTML); 312 - expect(a.disabled).to.equal(true); 313 - }); 314 - 315 - it("if new tree has disabled=false and old tree has disabled mutated to true, set value from new tree", async () => { 316 - const a = await fixture(html`<input type="checkbox" disabled=${false} />`); 317 - const b = await fixture(html`<input type="checkbox" />`); 318 - b.disabled = true; 319 - 320 - morph(a, b); 321 - 322 - expect(a.outerHTML).to.equal(b.outerHTML); 323 - expect(a.disabled).to.equal(true); 324 - }); 325 - 326 - it("if new tree has disabled=true and old tree has disabled mutated to false, set value from new tree", async () => { 327 - const a = await fixture(html`<input type="checkbox" disabled=${true} />`); 328 - const b = await fixture(html`<input type="checkbox" />`); 329 - b.disabled = false; 330 - 331 - morph(a, b); 332 - 333 - expect(a.outerHTML).to.equal(b.outerHTML); 334 - expect(a.disabled).to.equal(false); 335 - }); 336 - 337 - it("if new tree has no disabled and old tree has disabled=true, set value from new tree", async () => { 338 - const a = await fixture(html`<input type="checkbox" />`); 339 - const b = await fixture(html`<input type="checkbox" disabled=${true} />`); 340 - 341 - morph(a, b); 342 - 343 - expect(a.outerHTML).to.equal(b.outerHTML); 344 - expect(a.disabled).to.equal(true); 345 - }); 346 - }); 347 - 348 - describe("indeterminate", () => { 349 - it("if new tree has no indeterminate and old tree has indeterminate mutated to true, set value from new tree", async () => { 350 - const a = await fixture(html`<input type="checkbox" />`); 351 - const b = await fixture(html`<input type="checkbox" />`); 352 - b.indeterminate = true; 353 - 354 - morph(a, b); 355 - 356 - expect(a.outerHTML).to.equal(b.outerHTML); 357 - expect(a.indeterminate).to.equal(true); 358 - }); 359 - 360 - it("if new tree has no indeterminate and old tree has indeterminate mutated to false, set value from new tree", async () => { 361 - const a = await fixture(html`<input type="checkbox" />`); 362 - const b = await fixture(html`<input type="checkbox" />`); 363 - b.indeterminate = false; 364 - 365 - morph(a, b); 366 - 367 - expect(a.outerHTML).to.equal(b.outerHTML); 368 - expect(a.indeterminate).to.equal(false); 369 - }); 370 - }); 371 - }); 372 - 373 - describe("lists", () => { 374 - it("should append nodes", async () => { 375 - const a = await fixture(html`<ul></ul>`); 376 - const b = await fixture( 377 - html`<ul> 378 - <li>1</li> 379 - <li>2</li> 380 - <li>3</li> 381 - <li>4</li> 382 - <li>5</li> 383 - </ul>`, 384 - ); 385 - 386 - morph(a, b); 387 - 388 - expect(a.outerHTML).to.equal(b.outerHTML); 389 - }); 390 - 391 - it("should remove nodes", async () => { 392 - const a = await fixture( 393 - html`<ul> 394 - <li>1</li> 395 - <li>2</li> 396 - <li>3</li> 397 - <li>4</li> 398 - <li>5</li> 399 - </ul>`, 400 - ); 401 - const b = await fixture(html`<ul></ul>`); 402 - 403 - morph(a, b); 404 - 405 - expect(a.outerHTML).to.equal(b.outerHTML); 406 - }); 407 - }); 408 - 409 - describe("selectables", () => { 410 - it("should append nodes", async () => { 411 - const a = await fixture(html`<select></select>`); 412 - const b = await fixture( 413 - html`<select> 414 - <option>1</option> 415 - <option>2</option> 416 - <option>3</option> 417 - <option>4</option> 418 - </select>`, 419 - ); 420 - 421 - morph(a, b); 422 - 423 - expect(a.outerHTML).to.equal(b.outerHTML); 424 - }); 425 - 426 - it("should append nodes (including optgroups)", async () => { 427 - const a = await fixture(html`<select></select>`); 428 - const b = await fixture( 429 - html`<select> 430 - <optgroup> 431 - <option>1</option> 432 - <option>2</option> 433 - </optgroup> 434 - <option>3</option> 435 - <option>4</option> 436 - </select>`, 437 - ); 438 - 439 - morph(a, b); 440 - 441 - expect(a.outerHTML).to.equal(b.outerHTML); 442 - }); 443 - 444 - it("should remove nodes", async () => { 445 - const a = await fixture( 446 - html`<select> 447 - <option>1</option> 448 - <option>2</option> 449 - <option>3</option> 450 - <option>4</option> 451 - </select>`, 452 - ); 453 - const b = await fixture(html`<select></select>`); 454 - 455 - morph(a, b); 456 - 457 - expect(a.outerHTML).to.equal(b.outerHTML); 458 - }); 459 - 460 - it("should remove nodes (including optgroups)", async () => { 461 - const a = await fixture( 462 - html`<select> 463 - <optgroup> 464 - <option>1</option> 465 - <option>2</option> 466 - </optgroup> 467 - <option>3</option> 468 - <option>4</option> 469 - </select>`, 470 - ); 471 - const b = await fixture(html`<select></select>`); 472 - 473 - morph(a, b); 474 - 475 - expect(a.outerHTML).to.equal(b.outerHTML); 476 - }); 477 - 478 - it("should add selected", async () => { 479 - const a = await fixture( 480 - html`<select> 481 - <option>1</option> 482 - <option>2</option> 483 - </select>`, 484 - ); 485 - const b = await fixture( 486 - html`<select> 487 - <option>1</option> 488 - <option selected>2</option> 489 - </select>`, 490 - ); 491 - 492 - morph(a, b); 493 - 494 - expect(a.outerHTML).to.equal(b.outerHTML); 495 - }); 496 - 497 - it("should add selected (xhtml)", async () => { 498 - const a = await fixture( 499 - html`<select> 500 - <option>1</option> 501 - <option>2</option> 502 - </select>`, 503 - ); 504 - const b = await fixture( 505 - html`<select> 506 - <option>1</option> 507 - <option selected="selected">2</option> 508 - </select>`, 509 - ); 510 - 511 - morph(a, b); 512 - 513 - expect(a.outerHTML).to.equal(b.outerHTML); 514 - }); 515 - 516 - it("should switch selected", async () => { 517 - const a = await fixture( 518 - html`<select> 519 - <option selected="selected">1</option> 520 - <option>2</option> 521 - </select>`, 522 - ); 523 - const b = await fixture( 524 - html`<select> 525 - <option>1</option> 526 - <option selected="selected">2</option> 527 - </select>`, 528 - ); 529 - 530 - morph(a, b); 531 - 532 - expect(a.outerHTML).to.equal(b.outerHTML); 533 - }); 534 - }); 535 - 536 - it("should replace nodes", async () => { 537 - const a = await fixture( 538 - html`<ul> 539 - <li>1</li> 540 - <li>2</li> 541 - <li>3</li> 542 - <li>4</li> 543 - <li>5</li> 544 - </ul>`, 545 - ); 546 - const b = await fixture( 547 - html`<ul> 548 - <div>1</div> 549 - <li>2</li> 550 - <p>3</p> 551 - <li>4</li> 552 - <li>5</li> 553 - </ul>`, 554 - ); 555 - 556 - morph(a, b); 557 - 558 - expect(a.outerHTML).to.equal(b.outerHTML); 559 - }); 560 - 561 - it("should replace nodes after multiple iterations", async () => { 562 - const a = await fixture(html`<ul></ul>`); 563 - const b = await fixture( 564 - html`<ul> 565 - <li>1</li> 566 - <li>2</li> 567 - <li>3</li> 568 - <li>4</li> 569 - <li>5</li> 570 - </ul>`, 571 - ); 572 - 573 - morph(a, b); 574 - 575 - expect(a.outerHTML).to.equal(b.outerHTML); 576 - 577 - const c = await fixture( 578 - html`<ul> 579 - <div>1</div> 580 - <li>2</li> 581 - <p>3</p> 582 - <li>4</li> 583 - <li>5</li> 584 - </ul>`, 585 - ); 586 - 587 - morph(a, c); 588 - 589 - expect(a.outerHTML).to.equal(c.outerHTML); 590 - }); 591 - 592 - describe("use id as a key hint", () => { 593 - it("appends an element", async () => { 594 - const a = await fixture( 595 - html`<ul> 596 - <li id="a"></li> 597 - <li id="b"></li> 598 - <li id="c"></li> 599 - </ul>`, 600 - ); 601 - const b = await fixture( 602 - html`<ul> 603 - <li id="a"></li> 604 - <li id="new"></li> 605 - <li id="b"></li> 606 - <li id="c"></li> 607 - </ul>`, 608 - ); 609 - 610 - const oldFirst = a.children[0]; 611 - const oldSecond = a.children[1]; 612 - const oldThird = a.children[2]; 613 - 614 - morph(a, b); 615 - 616 - expect(a.outerHTML).to.equal(b.outerHTML); 617 - expect(a.children[0]).to.equal(oldFirst); 618 - expect(a.children[1]).to.equal(oldSecond); 619 - expect(a.children[2]).to.equal(oldThird); 620 - }); 621 - 622 - it.skip("handles non-id elements", async () => { 623 - const a = await fixture( 624 - html`<ul> 625 - <li></li> 626 - <li id="a"></li> 627 - <li id="b"></li> 628 - <li id="c"></li> 629 - <li></li> 630 - </ul>`, 631 - ); 632 - const b = await fixture( 633 - html`<ul> 634 - <li></li> 635 - <li id="a"></li> 636 - <li id="new"></li> 637 - <li id="b"></li> 638 - <li id="c"></li> 639 - <li></li> 640 - </ul>`, 641 - ); 642 - 643 - const oldSecond = a.children[1]; 644 - const oldThird = a.children[2]; 645 - const oldFourth = a.children[3]; 646 - 647 - morph(a, b); 648 - 649 - expect(a.outerHTML).to.equal(b.outerHTML); 650 - expect(a.children[1]).to.equal(oldSecond); 651 - expect(a.children[3]).to.equal(oldThird); 652 - expect(a.children[4]).to.equal(oldFourth); 653 - }); 654 - 655 - it("copies over children", async () => { 656 - const a = await fixture( 657 - html`<section> 658 - 'hello' 659 - <section></section> 660 - </section>`, 661 - ); 662 - const b = await fixture( 663 - html`<section> 664 - <div></div> 665 - <section></section> 666 - </section>`, 667 - ); 668 - 669 - morph(a, b); 670 - 671 - expect(a.outerHTML).to.equal(b.outerHTML); 672 - }); 673 - 674 - it.skip("removes an element", async () => { 675 - const a = await fixture( 676 - html`<ul> 677 - <li id="a"></li> 678 - <li id="b"></li> 679 - <li id="c"></li> 680 - </ul>`, 681 - ); 682 - const b = await fixture( 683 - html`<ul> 684 - <li id="a"></li> 685 - <li id="c"></li> 686 - </ul>`, 687 - ); 688 - 689 - const oldFirst = a.children[0]; 690 - const oldThird = a.children[2]; 691 - 692 - morph(a, b); 693 - 694 - expect(a.outerHTML).to.equal(b.outerHTML); 695 - expect(a.children[0]).to.equal(oldFirst); 696 - expect(a.children[1]).to.equal(oldThird); 697 - }); 698 - 699 - it("id match still morphs", async () => { 700 - const a = await fixture(html`<li id="12">FOO</li>`); 701 - const b = await fixture(html`<li id="12">BAR</li>`); 702 - 703 - morph(a, b); 704 - 705 - expect(a.outerHTML).to.equal(b.outerHTML); 706 - }); 707 - 708 - it("removes orphaned keyed nodes", async () => { 709 - const a = await fixture(html` 710 - <div> 711 - <div>1</div> 712 - <li id="a">a</li> 713 - </div> 714 - `); 715 - const b = await fixture(html` 716 - <div> 717 - <div>2</div> 718 - <li id="b">b</li> 719 - </div> 720 - `); 721 - 722 - morph(a, b); 723 - 724 - expect(a.outerHTML).to.equal(b.outerHTML); 725 - }); 726 - 727 - it("whitespace", async () => { 728 - const a = await fixture(html`<ul></ul>`); 729 - const b = await fixture( 730 - html`<ul> 731 - <li></li> 732 - <li></li> 733 - </ul>`, 734 - ); 735 - 736 - morph(a, b); 737 - 738 - expect(a.outerHTML).to.equal(b.outerHTML); 739 - }); 740 - }); 741 - 742 - it("allows morphing from Node to NodeList", async () => { 743 - const a = await fixture(html`<div><div>a</div></div>`); 744 - const b = await fixture( 745 - html`<div>a</div> 746 - <div>b</div>`, 747 - ); 748 - 749 - morph(a, b); 750 - 751 - expect(a.outerHTML).to.equal(b.outerHTML); 752 - }); 753 - 754 - it("allows morphing from NodeList to Node", async () => { 755 - const a = await fixture( 756 - html`<div>a</div> 757 - <div>b</div>`, 758 - ); 759 - const b = await fixture(html`<div><div>a</div></div>`); 760 - 761 - morph(a, b); 762 - 763 - expect(a.outerHTML).to.equal(b.outerHTML); 764 - }); 765 - 766 - it("allows morphing from NodeList to NodeList", async () => { 767 - const a = await fixture( 768 - html`<div>a</div> 769 - <div>b</div>`, 770 - ); 771 - const b = await fixture( 772 - html`<div>z</div> 773 - <div>y</div>`, 774 - ); 775 - 776 - morph(a, b); 777 - 778 - expect(a.outerHTML).to.equal(b.outerHTML); 779 - }); 780 - 781 - it("allows morphing from Node to Node", async () => { 782 - const a = await fixture(html`<div><div>a</div></div>`); 783 - const b = await fixture(html`<div><div>b</div></div>`); 784 - 785 - morph(a, b); 786 - 787 - expect(a.outerHTML).to.equal(b.outerHTML); 788 - }); 789 - });
+10
vitest.config.ts
··· 1 + import { defineConfig } from "vitest/config"; 2 + 3 + export default defineConfig({ 4 + test: { 5 + environment: "happy-dom", 6 + globals: true, 7 + testTimeout: 10000, 8 + hookTimeout: 10000, 9 + }, 10 + });
-16
web-test-runner.config.mjs
··· 1 - const filteredLogs = [ 2 - "Lit is in dev mode. Not recommended for production! See https://lit.dev/msg/dev-mode for more information.", 3 - ]; 4 - 5 - const filterBrowserLogs = (log) => { 6 - for (const arg of log.args) { 7 - if (typeof arg === "string" && filteredLogs.some((l) => arg.includes(l))) { 8 - return false; 9 - } 10 - } 11 - return true; 12 - }; 13 - 14 - export default { 15 - filterBrowserLogs, 16 - };