Precise DOM morphing
morphing typescript dom

Use node pairs for better type safety

- Update TypeScript
- Introduce `innerMorph` method

+161 -248
+71 -42
dist/morphlex.js
··· 9 } 10 if (isElement(node)) { 11 node.ariaBusy = "true"; 12 - new Morph(options).morph(node, reference); 13 node.ariaBusy = null; 14 } else { 15 - new Morph(options).morph(node, reference); 16 } 17 } 18 class Morph { ··· 47 this.#afterPropertyUpdated = options.afterPropertyUpdated; 48 Object.freeze(this); 49 } 50 - morph(node, reference) { 51 - if (isParentNode(node) && isParentNode(reference)) { 52 - this.#mapIdSets(node); 53 - this.#mapIdSets(reference); 54 - this.#mapSensivity(node); 55 - Object.freeze(this.#idMap); 56 - Object.freeze(this.#sensivityMap); 57 } 58 - requestAnimationFrame(() => { 59 - this.#morphNode(node, reference); 60 - }); 61 } 62 #mapSensivity(node) { 63 const sensitiveElements = node.querySelectorAll("audio,canvas,embed,iframe,input,object,textarea,video"); ··· 99 } 100 } 101 // This is where we actually morph the nodes. The `morph` function (above) exists only to set up the `idMap`. 102 - #morphNode(node, reference) { 103 - if (isElement(node) && isElement(reference) && node.localName === reference.localName) { 104 - this.#morphMatchingElementNode(node, reference); 105 - } else { 106 - this.#morphOtherNode(node, reference); 107 - } 108 } 109 - #morphMatchingElementNode(node, reference) { 110 - if (!(this.#beforeNodeMorphed?.(node, reference) ?? true)) return; 111 - if (node.hasAttributes() || reference.hasAttributes()) this.#morphAttributes(node, reference); 112 - if (isHead(node)) { 113 - this.#morphHead(node, reference); 114 - } else if (node.hasChildNodes() || reference.hasChildNodes()) this.#morphChildNodes(node, reference); 115 - this.#afterNodeMorphed?.(node, reference); 116 } 117 - #morphOtherNode(node, reference) { 118 - if (!(this.#beforeNodeMorphed?.(node, reference) ?? true)) return; 119 if (node.nodeType === reference.nodeType && node.nodeValue !== null && reference.nodeValue !== null) { 120 // Handle text nodes, comments, and CDATA sections. 121 this.#updateProperty(node, "nodeValue", reference.nodeValue); 122 } else this.#replaceNode(node, reference.cloneNode(true)); 123 - this.#afterNodeMorphed?.(node, reference); 124 } 125 - #morphHead(node, reference) { 126 const refChildNodesMap = new Map(); 127 // Generate a map of the reference head element’s child nodes, keyed by their outerHTML. 128 for (const child of reference.children) refChildNodesMap.set(child.outerHTML, child); ··· 136 // Any remaining nodes in the map should be appended to the head. 137 for (const refChild of refChildNodesMap.values()) this.#appendChild(node, refChild.cloneNode(true)); 138 } 139 - #morphAttributes(element, reference) { 140 // Remove any excess attributes from the element that aren’t present in the reference. 141 for (const { name, value } of element.attributes) { 142 if (!reference.hasAttribute(name) && (this.#beforeAttributeUpdated?.(element, name, null) ?? true)) { ··· 179 } 180 } 181 // Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match. 182 - #morphChildNodes(element, reference) { 183 const childNodes = element.childNodes; 184 const refChildNodes = reference.childNodes; 185 for (let i = 0; i < refChildNodes.length; i++) { 186 const child = childNodes[i]; 187 const refChild = refChildNodes[i]; 188 if (child && refChild) { 189 - if (isElement(child) && isElement(refChild) && child.localName === refChild.localName) { 190 - if (isHead(child)) { 191 - this.#morphHead(child, refChild); 192 - } else this.#morphChildElement(child, refChild, element); 193 - } else this.#morphOtherNode(child, refChild); 194 } else if (refChild) { 195 this.#appendChild(element, refChild.cloneNode(true)); 196 } else if (child) { ··· 203 if (child) this.#removeNode(child); 204 } 205 } 206 - #morphChildElement(child, reference, parent) { 207 - if (!(this.#beforeNodeMorphed?.(child, reference) ?? true)) return; 208 const refIdSet = this.#idMap.get(reference); 209 // Generate the array in advance of the loop 210 const refSetArray = refIdSet ? [...refIdSet] : []; ··· 220 if (id !== "") { 221 if (id === reference.id) { 222 this.#insertBefore(parent, currentNode, child); 223 - return this.#morphNode(currentNode, reference); 224 } else { 225 const currentIdSet = this.#idMap.get(currentNode); 226 if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) { 227 this.#insertBefore(parent, currentNode, child); 228 - return this.#morphNode(currentNode, reference); 229 } 230 } 231 } ··· 234 } 235 if (nextMatchByTagName) { 236 this.#insertBefore(parent, nextMatchByTagName, child); 237 - this.#morphNode(nextMatchByTagName, reference); 238 } else { 239 const newNode = reference.cloneNode(true); 240 if (this.#beforeNodeAdded?.(newNode) ?? true) { ··· 242 this.#afterNodeAdded?.(newNode); 243 } 244 } 245 - this.#afterNodeMorphed?.(child, reference); 246 } 247 #updateProperty(node, propertyName, newValue) { 248 const previousValue = node[propertyName]; ··· 288 this.#afterNodeRemoved?.(node); 289 } 290 } 291 } 292 function isElement(node) { 293 return node.nodeType === 1;
··· 9 } 10 if (isElement(node)) { 11 node.ariaBusy = "true"; 12 + new Morph(options).morph([node, reference]); 13 node.ariaBusy = null; 14 } else { 15 + new Morph(options).morph([node, reference]); 16 } 17 } 18 class Morph { ··· 47 this.#afterPropertyUpdated = options.afterPropertyUpdated; 48 Object.freeze(this); 49 } 50 + morph(pair) { 51 + if (isParentNodePair(pair)) this.#buildMaps(pair); 52 + this.#morphNode(pair); 53 + } 54 + morphInner(pair) { 55 + this.#buildMaps(pair); 56 + if (isMatchingElementPair(pair)) { 57 + this.#morphMatchingElementContent(pair); 58 + } else { 59 + throw new Error("You can only do an inner morph with matching elements."); 60 } 61 + } 62 + #buildMaps([node, reference]) { 63 + this.#mapIdSets(node); 64 + this.#mapIdSets(reference); 65 + this.#mapSensivity(node); 66 + Object.freeze(this.#idMap); 67 + Object.freeze(this.#sensivityMap); 68 } 69 #mapSensivity(node) { 70 const sensitiveElements = node.querySelectorAll("audio,canvas,embed,iframe,input,object,textarea,video"); ··· 106 } 107 } 108 // This is where we actually morph the nodes. The `morph` function (above) exists only to set up the `idMap`. 109 + #morphNode(pair) { 110 + if (isMatchingElementPair(pair)) this.#morphMatchingElementNode(pair); 111 + else this.#morphOtherNode(pair); 112 } 113 + #morphMatchingElementNode(pair) { 114 + const [node, reference] = pair; 115 + if (!(this.#beforeNodeMorphed?.(node, writableNode(reference)) ?? true)) return; 116 + if (node.hasAttributes() || reference.hasAttributes()) this.#morphAttributes(pair); 117 + // TODO: Should use a branded pair here. 118 + this.#morphMatchingElementContent(pair); 119 + this.#afterNodeMorphed?.(node, writableNode(reference)); 120 } 121 + #morphOtherNode([node, reference]) { 122 + if (!(this.#beforeNodeMorphed?.(node, writableNode(reference)) ?? true)) return; 123 if (node.nodeType === reference.nodeType && node.nodeValue !== null && reference.nodeValue !== null) { 124 // Handle text nodes, comments, and CDATA sections. 125 this.#updateProperty(node, "nodeValue", reference.nodeValue); 126 } else this.#replaceNode(node, reference.cloneNode(true)); 127 + this.#afterNodeMorphed?.(node, writableNode(reference)); 128 } 129 + #morphMatchingElementContent(pair) { 130 + const [node, reference] = pair; 131 + if (isHead(node)) { 132 + // We can pass the reference as a head here becuase we know it's the same as the node. 133 + this.#morphHeadContents(pair); 134 + } else if (node.hasChildNodes() || reference.hasChildNodes()) this.#morphChildNodes(pair); 135 + } 136 + #morphHeadContents([node, reference]) { 137 const refChildNodesMap = new Map(); 138 // Generate a map of the reference head element’s child nodes, keyed by their outerHTML. 139 for (const child of reference.children) refChildNodesMap.set(child.outerHTML, child); ··· 147 // Any remaining nodes in the map should be appended to the head. 148 for (const refChild of refChildNodesMap.values()) this.#appendChild(node, refChild.cloneNode(true)); 149 } 150 + #morphAttributes([element, reference]) { 151 // Remove any excess attributes from the element that aren’t present in the reference. 152 for (const { name, value } of element.attributes) { 153 if (!reference.hasAttribute(name) && (this.#beforeAttributeUpdated?.(element, name, null) ?? true)) { ··· 190 } 191 } 192 // Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match. 193 + #morphChildNodes(pair) { 194 + const [element, reference] = pair; 195 const childNodes = element.childNodes; 196 const refChildNodes = reference.childNodes; 197 for (let i = 0; i < refChildNodes.length; i++) { 198 const child = childNodes[i]; 199 const refChild = refChildNodes[i]; 200 if (child && refChild) { 201 + const pair = [child, refChild]; 202 + if (isMatchingElementPair(pair)) { 203 + if (isHead(pair[0])) { 204 + this.#morphHeadContents(pair); 205 + } else { 206 + this.#morphChildElement(pair, element); 207 + } 208 + } else this.#morphOtherNode(pair); 209 } else if (refChild) { 210 this.#appendChild(element, refChild.cloneNode(true)); 211 } else if (child) { ··· 218 if (child) this.#removeNode(child); 219 } 220 } 221 + #morphChildElement([child, reference], parent) { 222 + if (!(this.#beforeNodeMorphed?.(child, writableNode(reference)) ?? true)) return; 223 const refIdSet = this.#idMap.get(reference); 224 // Generate the array in advance of the loop 225 const refSetArray = refIdSet ? [...refIdSet] : []; ··· 235 if (id !== "") { 236 if (id === reference.id) { 237 this.#insertBefore(parent, currentNode, child); 238 + return this.#morphNode([currentNode, reference]); 239 } else { 240 const currentIdSet = this.#idMap.get(currentNode); 241 if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) { 242 this.#insertBefore(parent, currentNode, child); 243 + return this.#morphNode([currentNode, reference]); 244 } 245 } 246 } ··· 249 } 250 if (nextMatchByTagName) { 251 this.#insertBefore(parent, nextMatchByTagName, child); 252 + this.#morphNode([nextMatchByTagName, reference]); 253 } else { 254 const newNode = reference.cloneNode(true); 255 if (this.#beforeNodeAdded?.(newNode) ?? true) { ··· 257 this.#afterNodeAdded?.(newNode); 258 } 259 } 260 + this.#afterNodeMorphed?.(child, writableNode(reference)); 261 } 262 #updateProperty(node, propertyName, newValue) { 263 const previousValue = node[propertyName]; ··· 303 this.#afterNodeRemoved?.(node); 304 } 305 } 306 + } 307 + // We cannot use `instanceof` when nodes might be from different documents, 308 + // so we use type guards instead. This keeps TypeScript happy, while doing 309 + // the necessary checks at runtime. 310 + function writableNode(node) { 311 + return node; 312 + } 313 + function isMatchingElementPair(pair) { 314 + const [a, b] = pair; 315 + return isElement(a) && isElement(b) && a.localName === b.localName; 316 + } 317 + function isParentNodePair(pair) { 318 + const [a, b] = pair; 319 + return isParentNode(a) && isParentNode(b); 320 } 321 function isElement(node) { 322 return node.nodeType === 1;
+3 -3
package-lock.json
··· 1 { 2 "name": "morphlex", 3 - "version": "0.0.11", 4 "lockfileVersion": 3, 5 "requires": true, 6 "packages": { 7 "": { 8 "name": "morphlex", 9 - "version": "0.0.11", 10 "license": "MIT", 11 "devDependencies": { 12 "@open-wc/testing": "^3.0.0-next.5", ··· 14 "gzip-size-cli": "^5.1.0", 15 "prettier": "^3.2.5", 16 "terser": "^5.28.1", 17 - "typescript": "^5.3.3", 18 "typescript-eslint": "^7.0.2" 19 }, 20 "funding": {
··· 1 { 2 "name": "morphlex", 3 + "version": "0.0.14", 4 "lockfileVersion": 3, 5 "requires": true, 6 "packages": { 7 "": { 8 "name": "morphlex", 9 + "version": "0.0.14", 10 "license": "MIT", 11 "devDependencies": { 12 "@open-wc/testing": "^3.0.0-next.5", ··· 14 "gzip-size-cli": "^5.1.0", 15 "prettier": "^3.2.5", 16 "terser": "^5.28.1", 17 + "typescript": "^5.4.2", 18 "typescript-eslint": "^7.0.2" 19 }, 20 "funding": {
+3 -3
package.json
··· 18 "scripts": { 19 "test": "web-test-runner test/**/*.test.js --node-resolve", 20 "t": "web-test-runner --node-resolve", 21 - "build": "tsc && prettier --write ./src ./dist", 22 - "watch": "tsc -w", 23 "test:watch": "npm run test -- --watch", 24 "lint": "prettier --check ./src ./dist ./test", 25 "minify": "terser dist/morphlex.js -o dist/morphlex.min.js --config-file terser-config.json", ··· 34 "gzip-size-cli": "^5.1.0", 35 "prettier": "^3.2.5", 36 "terser": "^5.28.1", 37 - "typescript": "^5.3.3", 38 "typescript-eslint": "^7.0.2" 39 } 40 }
··· 18 "scripts": { 19 "test": "web-test-runner test/**/*.test.js --node-resolve", 20 "t": "web-test-runner --node-resolve", 21 + "build": "npx tsc && prettier --write ./src ./dist", 22 + "watch": "npx tsc -w", 23 "test:watch": "npm run test -- --watch", 24 "lint": "prettier --check ./src ./dist ./test", 25 "minify": "terser dist/morphlex.js -o dist/morphlex.min.js --config-file terser-config.json", ··· 34 "gzip-size-cli": "^5.1.0", 35 "prettier": "^3.2.5", 36 "terser": "^5.28.1", 37 + "typescript": "^5.4.2", 38 "typescript-eslint": "^7.0.2" 39 } 40 }
+84 -42
src/morphlex.ts
··· 5 // Maps to a type that can only read properties 6 type StrongReadonly<T> = { readonly [K in keyof T as T[K] extends Function ? never : K]: T[K] }; 7 8 // Maps a Node to a type limited to read-only properties and methods for that Node 9 type ReadonlyNode<T extends Node> = 10 | T ··· 55 56 if (isElement(node)) { 57 node.ariaBusy = "true"; 58 - new Morph(options).morph(node, reference); 59 node.ariaBusy = null; 60 } else { 61 - new Morph(options).morph(node, reference); 62 } 63 } 64 ··· 99 Object.freeze(this); 100 } 101 102 - morph(node: ChildNode, reference: ChildNode): void { 103 - if (isParentNode(node) && isParentNode(reference)) { 104 - this.#mapIdSets(node); 105 - this.#mapIdSets(reference); 106 - this.#mapSensivity(node); 107 108 - Object.freeze(this.#idMap); 109 - Object.freeze(this.#sensivityMap); 110 } 111 112 - requestAnimationFrame(() => { 113 - this.#morphNode(node, reference); 114 - }); 115 } 116 117 #mapSensivity(node: ReadonlyNode<ParentNode>): void { ··· 164 } 165 166 // This is where we actually morph the nodes. The `morph` function (above) exists only to set up the `idMap`. 167 - #morphNode(node: ChildNode, reference: ReadonlyNode<ChildNode>): void { 168 - if (isElement(node) && isElement(reference) && node.localName === reference.localName) { 169 - this.#morphMatchingElementNode(node, reference); 170 - } else { 171 - this.#morphOtherNode(node, reference); 172 - } 173 } 174 175 - #morphMatchingElementNode(node: Element, reference: ReadonlyNode<Element>): void { 176 - if (!(this.#beforeNodeMorphed?.(node, reference as ChildNode) ?? true)) return; 177 178 - if (node.hasAttributes() || reference.hasAttributes()) this.#morphAttributes(node, reference); 179 180 - if (isHead(node)) { 181 - this.#morphHead(node, reference as ReadonlyNode<HTMLHeadElement>); 182 - } else if (node.hasChildNodes() || reference.hasChildNodes()) this.#morphChildNodes(node, reference); 183 184 - this.#afterNodeMorphed?.(node, reference as ChildNode); 185 } 186 187 - #morphOtherNode(node: ChildNode, reference: ReadonlyNode<ChildNode>): void { 188 - if (!(this.#beforeNodeMorphed?.(node, reference as ChildNode) ?? true)) return; 189 190 if (node.nodeType === reference.nodeType && node.nodeValue !== null && reference.nodeValue !== null) { 191 // Handle text nodes, comments, and CDATA sections. 192 this.#updateProperty(node, "nodeValue", reference.nodeValue); 193 } else this.#replaceNode(node, reference.cloneNode(true)); 194 195 - this.#afterNodeMorphed?.(node, reference as ChildNode); 196 } 197 198 - #morphHead(node: HTMLHeadElement, reference: ReadonlyNode<HTMLHeadElement>): void { 199 const refChildNodesMap: Map<string, ReadonlyNode<Element>> = new Map(); 200 201 // Generate a map of the reference head element’s child nodes, keyed by their outerHTML. ··· 214 for (const refChild of refChildNodesMap.values()) this.#appendChild(node, refChild.cloneNode(true)); 215 } 216 217 - #morphAttributes(element: Element, reference: ReadonlyNode<Element>): void { 218 // Remove any excess attributes from the element that aren’t present in the reference. 219 for (const { name, value } of element.attributes) { 220 if (!reference.hasAttribute(name) && (this.#beforeAttributeUpdated?.(element, name, null) ?? true)) { ··· 261 } 262 263 // Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match. 264 - #morphChildNodes(element: Element, reference: ReadonlyNode<Element>): void { 265 const childNodes = element.childNodes; 266 const refChildNodes = reference.childNodes; 267 ··· 270 const refChild = refChildNodes[i] as ReadonlyNode<ChildNode> | null; 271 272 if (child && refChild) { 273 - if (isElement(child) && isElement(refChild) && child.localName === refChild.localName) { 274 - if (isHead(child)) { 275 - this.#morphHead(child, refChild as ReadonlyNode<HTMLHeadElement>); 276 - } else this.#morphChildElement(child, refChild, element); 277 - } else this.#morphOtherNode(child, refChild); 278 } else if (refChild) { 279 this.#appendChild(element, refChild.cloneNode(true)); 280 } else if (child) { ··· 289 } 290 } 291 292 - #morphChildElement(child: Element, reference: ReadonlyNode<Element>, parent: Element): void { 293 - if (!(this.#beforeNodeMorphed?.(child, reference as ChildNode) ?? true)) return; 294 295 const refIdSet = this.#idMap.get(reference); 296 ··· 312 if (id !== "") { 313 if (id === reference.id) { 314 this.#insertBefore(parent, currentNode, child); 315 - return this.#morphNode(currentNode, reference); 316 } else { 317 const currentIdSet = this.#idMap.get(currentNode); 318 319 if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) { 320 this.#insertBefore(parent, currentNode, child); 321 - return this.#morphNode(currentNode, reference); 322 } 323 } 324 } ··· 329 330 if (nextMatchByTagName) { 331 this.#insertBefore(parent, nextMatchByTagName, child); 332 - this.#morphNode(nextMatchByTagName, reference); 333 } else { 334 const newNode = reference.cloneNode(true); 335 if (this.#beforeNodeAdded?.(newNode) ?? true) { ··· 338 } 339 } 340 341 - this.#afterNodeMorphed?.(child, reference as ChildNode); 342 } 343 344 #updateProperty<N extends Node, P extends keyof N>(node: N, propertyName: P, newValue: N[P]): void { ··· 401 // We cannot use `instanceof` when nodes might be from different documents, 402 // so we use type guards instead. This keeps TypeScript happy, while doing 403 // the necessary checks at runtime. 404 405 function isElement(node: Node): node is Element; 406 function isElement(node: ReadonlyNode<Node>): node is ReadonlyNode<Element>;
··· 5 // Maps to a type that can only read properties 6 type StrongReadonly<T> = { readonly [K in keyof T as T[K] extends Function ? never : K]: T[K] }; 7 8 + declare const brand: unique symbol; 9 + type Branded<T, B extends string> = T & { [brand]: B }; 10 + 11 + type NodeReferencePair<N extends Node> = Readonly<[N, ReadonlyNode<N>]>; 12 + type MatchingElementReferencePair<E extends Element> = Branded<NodeReferencePair<E>, "MatchingElementPair">; 13 + 14 // Maps a Node to a type limited to read-only properties and methods for that Node 15 type ReadonlyNode<T extends Node> = 16 | T ··· 61 62 if (isElement(node)) { 63 node.ariaBusy = "true"; 64 + new Morph(options).morph([node, reference]); 65 node.ariaBusy = null; 66 } else { 67 + new Morph(options).morph([node, reference]); 68 } 69 } 70 ··· 105 Object.freeze(this); 106 } 107 108 + morph(pair: NodeReferencePair<ChildNode>): void { 109 + if (isParentNodePair(pair)) this.#buildMaps(pair); 110 + this.#morphNode(pair); 111 + } 112 113 + morphInner(pair: NodeReferencePair<Element>): void { 114 + this.#buildMaps(pair); 115 + 116 + if (isMatchingElementPair(pair)) { 117 + this.#morphMatchingElementContent(pair); 118 + } else { 119 + throw new Error("You can only do an inner morph with matching elements."); 120 } 121 + } 122 123 + #buildMaps([node, reference]: NodeReferencePair<ParentNode>): void { 124 + this.#mapIdSets(node); 125 + this.#mapIdSets(reference); 126 + this.#mapSensivity(node); 127 + 128 + Object.freeze(this.#idMap); 129 + Object.freeze(this.#sensivityMap); 130 } 131 132 #mapSensivity(node: ReadonlyNode<ParentNode>): void { ··· 179 } 180 181 // This is where we actually morph the nodes. The `morph` function (above) exists only to set up the `idMap`. 182 + #morphNode(pair: NodeReferencePair<ChildNode>): void { 183 + if (isMatchingElementPair(pair)) this.#morphMatchingElementNode(pair); 184 + else this.#morphOtherNode(pair); 185 } 186 187 + #morphMatchingElementNode(pair: MatchingElementReferencePair<Element>): void { 188 + const [node, reference] = pair; 189 190 + if (!(this.#beforeNodeMorphed?.(node, writableNode(reference)) ?? true)) return; 191 192 + if (node.hasAttributes() || reference.hasAttributes()) this.#morphAttributes(pair); 193 194 + // TODO: Should use a branded pair here. 195 + this.#morphMatchingElementContent(pair); 196 + 197 + this.#afterNodeMorphed?.(node, writableNode(reference)); 198 } 199 200 + #morphOtherNode([node, reference]: NodeReferencePair<ChildNode>): void { 201 + if (!(this.#beforeNodeMorphed?.(node, writableNode(reference)) ?? true)) return; 202 203 if (node.nodeType === reference.nodeType && node.nodeValue !== null && reference.nodeValue !== null) { 204 // Handle text nodes, comments, and CDATA sections. 205 this.#updateProperty(node, "nodeValue", reference.nodeValue); 206 } else this.#replaceNode(node, reference.cloneNode(true)); 207 208 + this.#afterNodeMorphed?.(node, writableNode(reference)); 209 } 210 211 + #morphMatchingElementContent(pair: MatchingElementReferencePair<Element>): void { 212 + const [node, reference] = pair; 213 + 214 + if (isHead(node)) { 215 + // We can pass the reference as a head here becuase we know it's the same as the node. 216 + this.#morphHeadContents(pair as MatchingElementReferencePair<HTMLHeadElement>); 217 + } else if (node.hasChildNodes() || reference.hasChildNodes()) this.#morphChildNodes(pair); 218 + } 219 + 220 + #morphHeadContents([node, reference]: MatchingElementReferencePair<HTMLHeadElement>): void { 221 const refChildNodesMap: Map<string, ReadonlyNode<Element>> = new Map(); 222 223 // Generate a map of the reference head element’s child nodes, keyed by their outerHTML. ··· 236 for (const refChild of refChildNodesMap.values()) this.#appendChild(node, refChild.cloneNode(true)); 237 } 238 239 + #morphAttributes([element, reference]: MatchingElementReferencePair<Element>): void { 240 // Remove any excess attributes from the element that aren’t present in the reference. 241 for (const { name, value } of element.attributes) { 242 if (!reference.hasAttribute(name) && (this.#beforeAttributeUpdated?.(element, name, null) ?? true)) { ··· 283 } 284 285 // Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match. 286 + #morphChildNodes(pair: MatchingElementReferencePair<Element>): void { 287 + const [element, reference] = pair; 288 + 289 const childNodes = element.childNodes; 290 const refChildNodes = reference.childNodes; 291 ··· 294 const refChild = refChildNodes[i] as ReadonlyNode<ChildNode> | null; 295 296 if (child && refChild) { 297 + const pair: NodeReferencePair<ChildNode> = [child, refChild]; 298 + 299 + if (isMatchingElementPair(pair)) { 300 + if (isHead(pair[0])) { 301 + this.#morphHeadContents(pair as MatchingElementReferencePair<HTMLHeadElement>); 302 + } else { 303 + this.#morphChildElement(pair, element); 304 + } 305 + } else this.#morphOtherNode(pair); 306 } else if (refChild) { 307 this.#appendChild(element, refChild.cloneNode(true)); 308 } else if (child) { ··· 317 } 318 } 319 320 + #morphChildElement([child, reference]: MatchingElementReferencePair<Element>, parent: Element): void { 321 + if (!(this.#beforeNodeMorphed?.(child, writableNode(reference)) ?? true)) return; 322 323 const refIdSet = this.#idMap.get(reference); 324 ··· 340 if (id !== "") { 341 if (id === reference.id) { 342 this.#insertBefore(parent, currentNode, child); 343 + return this.#morphNode([currentNode, reference]); 344 } else { 345 const currentIdSet = this.#idMap.get(currentNode); 346 347 if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) { 348 this.#insertBefore(parent, currentNode, child); 349 + return this.#morphNode([currentNode, reference]); 350 } 351 } 352 } ··· 357 358 if (nextMatchByTagName) { 359 this.#insertBefore(parent, nextMatchByTagName, child); 360 + this.#morphNode([nextMatchByTagName, reference]); 361 } else { 362 const newNode = reference.cloneNode(true); 363 if (this.#beforeNodeAdded?.(newNode) ?? true) { ··· 366 } 367 } 368 369 + this.#afterNodeMorphed?.(child, writableNode(reference)); 370 } 371 372 #updateProperty<N extends Node, P extends keyof N>(node: N, propertyName: P, newValue: N[P]): void { ··· 429 // We cannot use `instanceof` when nodes might be from different documents, 430 // so we use type guards instead. This keeps TypeScript happy, while doing 431 // the necessary checks at runtime. 432 + 433 + function writableNode<N extends Node>(node: ReadonlyNode<N>): N { 434 + return node as N; 435 + } 436 + 437 + function isMatchingElementPair(pair: NodeReferencePair<Node>): pair is MatchingElementReferencePair<Element> { 438 + const [a, b] = pair; 439 + return isElement(a) && isElement(b) && a.localName === b.localName; 440 + } 441 + 442 + function isParentNodePair(pair: NodeReferencePair<Node>): pair is NodeReferencePair<ParentNode> { 443 + const [a, b] = pair; 444 + return isParentNode(a) && isParentNode(b); 445 + } 446 447 function isElement(node: Node): node is Element; 448 function isElement(node: ReadonlyNode<Node>): node is ReadonlyNode<Element>;
-15
test/alpine-morph.test.js
··· 1 import { fixture, html, expect } from "@open-wc/testing"; 2 import { morph } from "../"; 3 - import { nextFrame } from "./helpers"; 4 5 // adapted from: https://github.com/alpinejs/alpine/blob/891d68503960a39826e89f2f666d9b1e7ce3f0c9/tests/jest/morph/external.spec.js 6 describe("alpine-morph", () => { ··· 10 11 morph(a, b); 12 13 - await nextFrame(); 14 - 15 expect(a.outerHTML).to.equal(b.outerHTML); 16 }); 17 ··· 21 22 morph(a, b); 23 24 - await nextFrame(); 25 - 26 expect(a.outerHTML).to.equal(b.outerHTML); 27 }); 28 ··· 37 38 morph(a, b); 39 40 - await nextFrame(); 41 - 42 expect(a.outerHTML).to.equal(b.outerHTML); 43 }); 44 ··· 53 54 morph(a, b); 55 56 - await nextFrame(); 57 - 58 expect(a.outerHTML).to.equal(b.outerHTML); 59 }); 60 ··· 64 65 morph(a, b); 66 67 - await nextFrame(); 68 - 69 expect(a.outerHTML).to.equal(b.outerHTML); 70 }); 71 ··· 75 76 morph(a, b); 77 78 - await nextFrame(); 79 - 80 expect(a.outerHTML).to.equal(b.outerHTML); 81 }); 82 ··· 85 const b = await fixture(html`<div foo="baz">foo</div>`); 86 87 morph(a, b); 88 - 89 - await nextFrame(); 90 91 expect(a.outerHTML).to.equal(b.outerHTML); 92 });
··· 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", () => { ··· 9 10 morph(a, b); 11 12 expect(a.outerHTML).to.equal(b.outerHTML); 13 }); 14 ··· 18 19 morph(a, b); 20 21 expect(a.outerHTML).to.equal(b.outerHTML); 22 }); 23 ··· 32 33 morph(a, b); 34 35 expect(a.outerHTML).to.equal(b.outerHTML); 36 }); 37 ··· 46 47 morph(a, b); 48 49 expect(a.outerHTML).to.equal(b.outerHTML); 50 }); 51 ··· 55 56 morph(a, b); 57 58 expect(a.outerHTML).to.equal(b.outerHTML); 59 }); 60 ··· 64 65 morph(a, b); 66 67 expect(a.outerHTML).to.equal(b.outerHTML); 68 }); 69 ··· 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 });
-7
test/helpers.js
··· 1 - export function nextFrame() { 2 - return new Promise((resolve) => { 3 - requestAnimationFrame(() => { 4 - resolve(); 5 - }); 6 - }); 7 - }
···
-10
test/morphdom.test.js
··· 1 import { fixture, html, expect } from "@open-wc/testing"; 2 import { morph } from "../"; 3 - import { nextFrame } from "./helpers"; 4 5 // adapted from: https://github.com/patrick-steele-idem/morphdom/blob/e98d69e125cda814dd6d1ba71d6c7c9d93edc01e/test/browser/test.js 6 describe("morphdom", () => { ··· 9 const b = await fixture(html`<div class="bar"></div>`); 10 11 morph(a, b); 12 - await nextFrame(); 13 14 expect(a.outerHTML).to.equal(b.outerHTML); 15 expect(a.className).to.equal("bar"); ··· 25 const b = await fixture(html`<body></body>`); 26 27 morph(a, b); 28 - await nextFrame(); 29 30 expect(a.outerHTML).to.equal(b.outerHTML); 31 expect(a.nodeName).to.equal("BODY"); ··· 37 const b = await fixture(html`<div id="el-1" class="bar"><div id="el-1">B</div></div>`); 38 39 morph(a, b); 40 - await nextFrame(); 41 42 expect(a.outerHTML).to.equal(b.outerHTML); 43 expect(a.className).to.equal("bar"); ··· 51 const b = await fixture(html`<div id="el-1" class="zoo"><div id="el-inner">B</div></div>`); 52 53 morph(a, b); 54 - await nextFrame(); 55 56 expect(a.outerHTML).to.equal(b.outerHTML); 57 expect(a.className).to.equal("zoo"); ··· 72 const b = await fixture(html`<div><p id="hi" class="foo">A</p></div>`); 73 74 morph(a, b); 75 - await nextFrame(); 76 77 expect(a.outerHTML).to.equal(b.outerHTML); 78 expect(a.children.length).to.equal(2); ··· 95 const b = await fixture(html`<div><h1 id="matching" class="baz">C</h1></div>`); 96 97 morph(a, b); 98 - await nextFrame(); 99 100 expect(a.outerHTML).to.equal(b.outerHTML); 101 expect(a.children.length).to.equal(1); ··· 109 const b = await fixture(html`<input type="text" value="Hello World 2" />`); 110 111 morph(a, b); 112 - await nextFrame(); 113 114 expect(a.outerHTML).to.equal(b.outerHTML); 115 expect(a.value).to.equal("Hello World 2"); ··· 121 const b = await fixture(html`<input type="text" checked="" />`); 122 123 morph(a, b); 124 - await nextFrame(); 125 126 expect(a.outerHTML).to.equal(b.outerHTML); 127 expect(a.checked).to.equal(true); ··· 135 b.checked = true; 136 137 morph(a, b); 138 - await nextFrame(); 139 140 expect(a.outerHTML).to.equal(b.outerHTML); 141 expect(a.checked).to.equal(true);
··· 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", () => { ··· 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"); ··· 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"); ··· 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"); ··· 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"); ··· 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); ··· 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); ··· 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"); ··· 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); ··· 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);
-9
test/morphlex.test.js
··· 1 import { fixture, html, expect } from "@open-wc/testing"; 2 import { morph } from "../"; 3 - import { nextFrame } from "./helpers"; 4 5 describe("morph", () => { 6 it("doesn't cause iframes to reload", async () => { ··· 21 const originalIframe = original.querySelector("iframe"); 22 morph(original, reference); 23 24 - await nextFrame(); 25 - 26 expect(original.outerHTML).to.equal(reference.outerHTML); 27 }); 28 ··· 37 iframe.contentDocument.body.appendChild(eventual); 38 39 morph(original, eventual); 40 - 41 - await nextFrame(); 42 43 expect(original.textContent).to.equal("Hello Joel"); 44 }); ··· 53 54 morph(a, b); 55 56 - await nextFrame(); 57 - 58 expect(a.textContent).to.equal(b.textContent); 59 }); 60 ··· 63 const b = await fixture(html`<h1></h1>`); 64 65 morph(a, b); 66 - 67 - await nextFrame(); 68 69 expect(a.outerHTML).to.equal(b.outerHTML); 70 });
··· 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 () => { ··· 20 const originalIframe = original.querySelector("iframe"); 21 morph(original, reference); 22 23 expect(original.outerHTML).to.equal(reference.outerHTML); 24 }); 25 ··· 34 iframe.contentDocument.body.appendChild(eventual); 35 36 morph(original, eventual); 37 38 expect(original.textContent).to.equal("Hello Joel"); 39 }); ··· 48 49 morph(a, b); 50 51 expect(a.textContent).to.equal(b.textContent); 52 }); 53 ··· 56 const b = await fixture(html`<h1></h1>`); 57 58 morph(a, b); 59 60 expect(a.outerHTML).to.equal(b.outerHTML); 61 });
-117
test/nanomorph.test.js
··· 1 import { fixture, html, expect } from "@open-wc/testing"; 2 import { morph } from "../"; 3 - import { nextFrame } from "./helpers"; 4 5 // adapted from: https://github.com/choojs/nanomorph/blob/b8088d03b1113bddabff8aa0e44bd8db88d023c7/test/diff.js 6 describe("nanomorph", () => { ··· 13 14 morph(a, b); 15 16 - await nextFrame(); 17 - 18 expect(a.outerHTML).to.equal(b.outerHTML); 19 }); 20 ··· 24 25 morph(a, b); 26 27 - await nextFrame(); 28 - 29 expect(a.outerHTML).to.equal(b.outerHTML); 30 }); 31 ··· 35 36 morph(a, b); 37 38 - await nextFrame(); 39 - 40 expect(a.outerHTML).to.equal(b.outerHTML); 41 }); 42 ··· 46 47 morph(a, b); 48 49 - await nextFrame(); 50 - 51 expect(a.outerHTML).to.equal(b.outerHTML); 52 }); 53 ··· 55 const a = await fixture(html`<p>hello world</p>`); 56 57 morph(a, a); 58 - 59 - await nextFrame(); 60 61 expect(a.outerHTML).to.equal(a.outerHTML); 62 }); ··· 69 70 morph(a, b); 71 72 - await nextFrame(); 73 - 74 expect(a.outerHTML).to.equal(b.outerHTML); 75 }); 76 ··· 79 const b = await fixture(html`<main><p>hello you</p></main>`); 80 81 morph(a, b); 82 - 83 - await nextFrame(); 84 85 expect(a.outerHTML).to.equal(b.outerHTML); 86 }); ··· 90 91 morph(a, a); 92 93 - await nextFrame(); 94 - 95 expect(a.outerHTML).to.equal(a.outerHTML); 96 }); 97 ··· 100 const b = await fixture(html`<main><p>hello you</p></main>`); 101 102 morph(a, b); 103 - 104 - await nextFrame(); 105 106 expect(a.outerHTML).to.equal(b.outerHTML); 107 }); ··· 112 113 morph(a, b); 114 115 - await nextFrame(); 116 - 117 expect(a.outerHTML).to.equal(b.outerHTML); 118 }); 119 ··· 122 const b = await fixture(html`<section><p>hello you</p></section>`); 123 124 morph(a, b, { childrenOnly: true }); 125 - 126 - await nextFrame(); 127 128 expect(a.outerHTML).to.equal("<main><p>hello you</p></main>"); 129 }); ··· 136 137 morph(a, b); 138 139 - await nextFrame(); 140 - 141 expect(a.outerHTML).to.equal(b.outerHTML); 142 expect(a.getAttribute("value")).to.equal(null); 143 expect(a.value).to.equal(""); ··· 148 const b = await fixture(html`<input type="text" value=${null} />`); 149 150 morph(a, b); 151 - 152 - await nextFrame(); 153 154 expect(a.outerHTML).to.equal(b.outerHTML); 155 expect(a.getAttribute("value")).to.equal(null); ··· 162 163 morph(a, b); 164 165 - await nextFrame(); 166 - 167 expect(a.outerHTML).to.equal(b.outerHTML); 168 expect(a.value).to.equal("hi"); 169 }); ··· 176 177 morph(a, b); 178 179 - await nextFrame(); 180 - 181 expect(a.outerHTML).to.equal(b.outerHTML); 182 expect(a.value).to.equal("hi"); 183 }); ··· 189 190 morph(a, b); 191 192 - await nextFrame(); 193 - 194 expect(a.outerHTML).to.equal(b.outerHTML); 195 expect(a.value).to.equal("hi"); 196 }); ··· 202 203 morph(a, b); 204 205 - await nextFrame(); 206 - 207 expect(a.outerHTML).to.equal(b.outerHTML); 208 expect(a.value).to.equal("hi"); 209 }); ··· 217 218 morph(a, b); 219 220 - await nextFrame(); 221 - 222 expect(a.outerHTML).to.equal(b.outerHTML); 223 expect(a.checked).to.equal(false); 224 }); ··· 229 230 morph(a, b); 231 232 - await nextFrame(); 233 - 234 expect(a.outerHTML).to.equal(b.outerHTML); 235 expect(a.checked).to.equal(true); 236 }); ··· 241 242 morph(a, b); 243 244 - await nextFrame(); 245 - 246 expect(a.outerHTML).to.equal(b.outerHTML); 247 expect(a.checked).to.equal(true); 248 }); ··· 253 254 morph(a, b); 255 256 - await nextFrame(); 257 - 258 expect(a.outerHTML).to.equal(b.outerHTML); 259 expect(a.checked).to.equal(false); 260 }); ··· 265 b.checked = true; 266 267 morph(a, b); 268 - 269 - await nextFrame(); 270 271 expect(a.outerHTML).to.equal(b.outerHTML); 272 expect(a.checked).to.equal(true); ··· 279 280 morph(a, b); 281 282 - await nextFrame(); 283 - 284 expect(a.outerHTML).to.equal(b.outerHTML); 285 expect(a.checked).to.equal(true); 286 }); ··· 291 b.checked = false; 292 293 morph(a, b); 294 - 295 - await nextFrame(); 296 297 expect(a.outerHTML).to.equal(b.outerHTML); 298 expect(a.checked).to.equal(false); ··· 304 305 morph(a, b); 306 307 - await nextFrame(); 308 - 309 expect(a.outerHTML).to.equal(b.outerHTML); 310 expect(a.checked).to.equal(true); 311 }); ··· 318 319 morph(a, b); 320 321 - await nextFrame(); 322 - 323 expect(a.outerHTML).to.equal(b.outerHTML); 324 expect(a.disabled).to.equal(false); 325 }); ··· 330 331 morph(a, b); 332 333 - await nextFrame(); 334 - 335 expect(a.outerHTML).to.equal(b.outerHTML); 336 expect(a.disabled).to.equal(true); 337 }); ··· 342 343 morph(a, b); 344 345 - await nextFrame(); 346 - 347 expect(a.outerHTML).to.equal(b.outerHTML); 348 expect(a.disabled).to.equal(true); 349 }); ··· 353 const b = await fixture(html`<input type="checkbox" disabled=${false} />`); 354 355 morph(a, b); 356 - 357 - await nextFrame(); 358 359 expect(a.outerHTML).to.equal(b.outerHTML); 360 expect(a.disabled).to.equal(false); ··· 367 368 morph(a, b); 369 370 - await nextFrame(); 371 - 372 expect(a.outerHTML).to.equal(b.outerHTML); 373 expect(a.disabled).to.equal(true); 374 }); ··· 379 b.disabled = true; 380 381 morph(a, b); 382 - 383 - await nextFrame(); 384 385 expect(a.outerHTML).to.equal(b.outerHTML); 386 expect(a.disabled).to.equal(true); ··· 393 394 morph(a, b); 395 396 - await nextFrame(); 397 - 398 expect(a.outerHTML).to.equal(b.outerHTML); 399 expect(a.disabled).to.equal(false); 400 }); ··· 404 const b = await fixture(html`<input type="checkbox" disabled=${true} />`); 405 406 morph(a, b); 407 - 408 - await nextFrame(); 409 410 expect(a.outerHTML).to.equal(b.outerHTML); 411 expect(a.disabled).to.equal(true); ··· 420 421 morph(a, b); 422 423 - await nextFrame(); 424 - 425 expect(a.outerHTML).to.equal(b.outerHTML); 426 expect(a.indeterminate).to.equal(true); 427 }); ··· 432 b.indeterminate = false; 433 434 morph(a, b); 435 - 436 - await nextFrame(); 437 438 expect(a.outerHTML).to.equal(b.outerHTML); 439 expect(a.indeterminate).to.equal(false); ··· 456 457 morph(a, b); 458 459 - await nextFrame(); 460 - 461 expect(a.outerHTML).to.equal(b.outerHTML); 462 }); 463 ··· 475 476 morph(a, b); 477 478 - await nextFrame(); 479 - 480 expect(a.outerHTML).to.equal(b.outerHTML); 481 }); 482 }); ··· 495 496 morph(a, b); 497 498 - await nextFrame(); 499 - 500 expect(a.outerHTML).to.equal(b.outerHTML); 501 }); 502 ··· 515 516 morph(a, b); 517 518 - await nextFrame(); 519 - 520 expect(a.outerHTML).to.equal(b.outerHTML); 521 }); 522 ··· 532 const b = await fixture(html`<select></select>`); 533 534 morph(a, b); 535 - 536 - await nextFrame(); 537 538 expect(a.outerHTML).to.equal(b.outerHTML); 539 }); ··· 553 554 morph(a, b); 555 556 - await nextFrame(); 557 - 558 expect(a.outerHTML).to.equal(b.outerHTML); 559 }); 560 ··· 573 ); 574 575 morph(a, b); 576 - 577 - await nextFrame(); 578 579 expect(a.outerHTML).to.equal(b.outerHTML); 580 }); ··· 595 596 morph(a, b); 597 598 - await nextFrame(); 599 - 600 expect(a.outerHTML).to.equal(b.outerHTML); 601 }); 602 ··· 616 617 morph(a, b); 618 619 - await nextFrame(); 620 - 621 expect(a.outerHTML).to.equal(b.outerHTML); 622 }); 623 }); ··· 644 645 morph(a, b); 646 647 - await nextFrame(); 648 - 649 expect(a.outerHTML).to.equal(b.outerHTML); 650 }); 651 ··· 663 664 morph(a, b); 665 666 - await nextFrame(); 667 - 668 expect(a.outerHTML).to.equal(b.outerHTML); 669 670 const c = await fixture( ··· 678 ); 679 680 morph(a, c); 681 - 682 - await nextFrame(); 683 684 expect(a.outerHTML).to.equal(c.outerHTML); 685 }); ··· 708 709 morph(a, b); 710 711 - await nextFrame(); 712 - 713 expect(a.outerHTML).to.equal(b.outerHTML); 714 expect(a.children[0]).to.equal(oldFirst); 715 expect(a.children[1]).to.equal(oldSecond); ··· 743 744 morph(a, b); 745 746 - await nextFrame(); 747 - 748 expect(a.outerHTML).to.equal(b.outerHTML); 749 expect(a.children[1]).to.equal(oldSecond); 750 expect(a.children[3]).to.equal(oldThird); ··· 767 768 morph(a, b); 769 770 - await nextFrame(); 771 - 772 expect(a.outerHTML).to.equal(b.outerHTML); 773 }); 774 ··· 791 const oldThird = a.children[2]; 792 793 morph(a, b); 794 - 795 - await nextFrame(); 796 797 expect(a.outerHTML).to.equal(b.outerHTML); 798 expect(a.children[0]).to.equal(oldFirst); ··· 805 806 morph(a, b); 807 808 - await nextFrame(); 809 - 810 expect(a.outerHTML).to.equal(b.outerHTML); 811 }); 812 ··· 825 `); 826 827 morph(a, b); 828 - 829 - await nextFrame(); 830 831 expect(a.outerHTML).to.equal(b.outerHTML); 832 }); ··· 842 843 morph(a, b); 844 845 - await nextFrame(); 846 - 847 expect(a.outerHTML).to.equal(b.outerHTML); 848 }); 849 }); ··· 857 858 morph(a, b); 859 860 - await nextFrame(); 861 - 862 expect(a.outerHTML).to.equal(b.outerHTML); 863 }); 864 ··· 870 const b = await fixture(html`<div><div>a</div></div>`); 871 872 morph(a, b); 873 - 874 - await nextFrame(); 875 876 expect(a.outerHTML).to.equal(b.outerHTML); 877 }); ··· 888 889 morph(a, b); 890 891 - await nextFrame(); 892 - 893 expect(a.outerHTML).to.equal(b.outerHTML); 894 }); 895 ··· 898 const b = await fixture(html`<div><div>b</div></div>`); 899 900 morph(a, b); 901 - 902 - await nextFrame(); 903 904 expect(a.outerHTML).to.equal(b.outerHTML); 905 });
··· 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", () => { ··· 12 13 morph(a, b); 14 15 expect(a.outerHTML).to.equal(b.outerHTML); 16 }); 17 ··· 21 22 morph(a, b); 23 24 expect(a.outerHTML).to.equal(b.outerHTML); 25 }); 26 ··· 30 31 morph(a, b); 32 33 expect(a.outerHTML).to.equal(b.outerHTML); 34 }); 35 ··· 39 40 morph(a, b); 41 42 expect(a.outerHTML).to.equal(b.outerHTML); 43 }); 44 ··· 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 }); ··· 58 59 morph(a, b); 60 61 expect(a.outerHTML).to.equal(b.outerHTML); 62 }); 63 ··· 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 }); ··· 75 76 morph(a, a); 77 78 expect(a.outerHTML).to.equal(a.outerHTML); 79 }); 80 ··· 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 }); ··· 93 94 morph(a, b); 95 96 expect(a.outerHTML).to.equal(b.outerHTML); 97 }); 98 ··· 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 }); ··· 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(""); ··· 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); ··· 135 136 morph(a, b); 137 138 expect(a.outerHTML).to.equal(b.outerHTML); 139 expect(a.value).to.equal("hi"); 140 }); ··· 147 148 morph(a, b); 149 150 expect(a.outerHTML).to.equal(b.outerHTML); 151 expect(a.value).to.equal("hi"); 152 }); ··· 158 159 morph(a, b); 160 161 expect(a.outerHTML).to.equal(b.outerHTML); 162 expect(a.value).to.equal("hi"); 163 }); ··· 169 170 morph(a, b); 171 172 expect(a.outerHTML).to.equal(b.outerHTML); 173 expect(a.value).to.equal("hi"); 174 }); ··· 182 183 morph(a, b); 184 185 expect(a.outerHTML).to.equal(b.outerHTML); 186 expect(a.checked).to.equal(false); 187 }); ··· 192 193 morph(a, b); 194 195 expect(a.outerHTML).to.equal(b.outerHTML); 196 expect(a.checked).to.equal(true); 197 }); ··· 202 203 morph(a, b); 204 205 expect(a.outerHTML).to.equal(b.outerHTML); 206 expect(a.checked).to.equal(true); 207 }); ··· 212 213 morph(a, b); 214 215 expect(a.outerHTML).to.equal(b.outerHTML); 216 expect(a.checked).to.equal(false); 217 }); ··· 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); ··· 234 235 morph(a, b); 236 237 expect(a.outerHTML).to.equal(b.outerHTML); 238 expect(a.checked).to.equal(true); 239 }); ··· 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); ··· 255 256 morph(a, b); 257 258 expect(a.outerHTML).to.equal(b.outerHTML); 259 expect(a.checked).to.equal(true); 260 }); ··· 267 268 morph(a, b); 269 270 expect(a.outerHTML).to.equal(b.outerHTML); 271 expect(a.disabled).to.equal(false); 272 }); ··· 277 278 morph(a, b); 279 280 expect(a.outerHTML).to.equal(b.outerHTML); 281 expect(a.disabled).to.equal(true); 282 }); ··· 287 288 morph(a, b); 289 290 expect(a.outerHTML).to.equal(b.outerHTML); 291 expect(a.disabled).to.equal(true); 292 }); ··· 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); ··· 308 309 morph(a, b); 310 311 expect(a.outerHTML).to.equal(b.outerHTML); 312 expect(a.disabled).to.equal(true); 313 }); ··· 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); ··· 330 331 morph(a, b); 332 333 expect(a.outerHTML).to.equal(b.outerHTML); 334 expect(a.disabled).to.equal(false); 335 }); ··· 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); ··· 353 354 morph(a, b); 355 356 expect(a.outerHTML).to.equal(b.outerHTML); 357 expect(a.indeterminate).to.equal(true); 358 }); ··· 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); ··· 385 386 morph(a, b); 387 388 expect(a.outerHTML).to.equal(b.outerHTML); 389 }); 390 ··· 402 403 morph(a, b); 404 405 expect(a.outerHTML).to.equal(b.outerHTML); 406 }); 407 }); ··· 420 421 morph(a, b); 422 423 expect(a.outerHTML).to.equal(b.outerHTML); 424 }); 425 ··· 438 439 morph(a, b); 440 441 expect(a.outerHTML).to.equal(b.outerHTML); 442 }); 443 ··· 453 const b = await fixture(html`<select></select>`); 454 455 morph(a, b); 456 457 expect(a.outerHTML).to.equal(b.outerHTML); 458 }); ··· 472 473 morph(a, b); 474 475 expect(a.outerHTML).to.equal(b.outerHTML); 476 }); 477 ··· 490 ); 491 492 morph(a, b); 493 494 expect(a.outerHTML).to.equal(b.outerHTML); 495 }); ··· 510 511 morph(a, b); 512 513 expect(a.outerHTML).to.equal(b.outerHTML); 514 }); 515 ··· 529 530 morph(a, b); 531 532 expect(a.outerHTML).to.equal(b.outerHTML); 533 }); 534 }); ··· 555 556 morph(a, b); 557 558 expect(a.outerHTML).to.equal(b.outerHTML); 559 }); 560 ··· 572 573 morph(a, b); 574 575 expect(a.outerHTML).to.equal(b.outerHTML); 576 577 const c = await fixture( ··· 585 ); 586 587 morph(a, c); 588 589 expect(a.outerHTML).to.equal(c.outerHTML); 590 }); ··· 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); ··· 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); ··· 668 669 morph(a, b); 670 671 expect(a.outerHTML).to.equal(b.outerHTML); 672 }); 673 ··· 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); ··· 702 703 morph(a, b); 704 705 expect(a.outerHTML).to.equal(b.outerHTML); 706 }); 707 ··· 720 `); 721 722 morph(a, b); 723 724 expect(a.outerHTML).to.equal(b.outerHTML); 725 }); ··· 735 736 morph(a, b); 737 738 expect(a.outerHTML).to.equal(b.outerHTML); 739 }); 740 }); ··· 748 749 morph(a, b); 750 751 expect(a.outerHTML).to.equal(b.outerHTML); 752 }); 753 ··· 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 }); ··· 775 776 morph(a, b); 777 778 expect(a.outerHTML).to.equal(b.outerHTML); 779 }); 780 ··· 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 });