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