tangled
alpha
login
or
join now
yippee.fun
/
morphlex
0
fork
atom
Precise DOM morphing
morphing
typescript
dom
0
fork
atom
overview
issues
pulls
pipelines
Use specific operations for better performance
joel.drapper.me
4 months ago
36ccc84e
7f3af345
+71
-28
2 changed files
expand all
collapse all
unified
split
benchmark
index.html
src
morphlex.ts
+9
-1
benchmark/index.html
···
383
import morphdom from "https://unpkg.com/morphdom@2.7.7/dist/morphdom-esm.js"
384
// Try loading nanomorph from jsdelivr with ESM
385
import nanomorph from "https://cdn.jsdelivr.net/npm/nanomorph@5.4.3/+esm"
386
-
// Alpine morph requires Alpine.js to be loaded, so we'll skip it
0
0
0
0
0
0
0
387
388
const sandbox = document.getElementById("sandbox")
389
let isRunning = false
···
558
{ name: "idiomorph", fn: (from, to) => Idiomorph.morph(from, to) },
559
{ name: "morphdom", fn: (from, to) => morphdom(from, to.cloneNode(true)) },
560
{ name: "nanomorph", fn: (from, to) => nanomorph(from, to.cloneNode(true)) },
0
561
]
562
563
for (const lib of libraries) {
···
383
import morphdom from "https://unpkg.com/morphdom@2.7.7/dist/morphdom-esm.js"
384
// Try loading nanomorph from jsdelivr with ESM
385
import nanomorph from "https://cdn.jsdelivr.net/npm/nanomorph@5.4.3/+esm"
386
+
// Load Alpine.js and Alpine morph
387
+
import Alpine from "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/module.esm.js"
388
+
import Morph from "https://cdn.jsdelivr.net/npm/@alpinejs/morph@3.x.x/dist/module.esm.js"
389
+
390
+
// Initialize Alpine with morph plugin
391
+
Alpine.plugin(Morph)
392
+
window.Alpine = Alpine
393
+
Alpine.start()
394
395
const sandbox = document.getElementById("sandbox")
396
let isRunning = false
···
565
{ name: "idiomorph", fn: (from, to) => Idiomorph.morph(from, to) },
566
{ name: "morphdom", fn: (from, to) => morphdom(from, to.cloneNode(true)) },
567
{ name: "nanomorph", fn: (from, to) => nanomorph(from, to.cloneNode(true)) },
568
+
{ name: "alpine-morph", fn: (from, to) => Alpine.morph(from, to) },
569
]
570
571
for (const lib of libraries) {
+62
-27
src/morphlex.ts
···
3
const TEXT_NODE_TYPE = 3
4
const PARENT_NODE_TYPES = [false, true, false, false, false, false, false, false, false, true, false, true]
5
0
0
0
0
0
0
0
0
6
const candidateNodes: Set<number> = new Set()
7
const candidateElements: Set<number> = new Set()
8
const unmatchedNodes: Set<number> = new Set()
···
322
if (from === to) return
323
if (from.isEqualNode?.(to)) return
324
325
-
if (!(this.#options.beforeNodeVisited?.(from, to) ?? true)) return
326
-
327
if (from.nodeType === ELEMENT_NODE_TYPE && to.nodeType === ELEMENT_NODE_TYPE) {
328
if ((from as Element).localName === (to as Element).localName) {
329
this.#morphMatchingElements(from as Element, to as Element)
···
333
} else {
334
this.#morphOtherNode(from, to)
335
}
336
-
337
-
this.#options.afterNodeVisited?.(from, to)
338
}
339
340
#morphMatchingElements(from: Element, to: Element): void {
0
0
341
if (from.hasAttributes() || to.hasAttributes()) {
342
this.#visitAttributes(from, to)
343
}
···
347
} else if (from.hasChildNodes() || to.hasChildNodes()) {
348
this.visitChildNodes(from, to)
349
}
0
0
350
}
351
352
#morphNonMatchingElements(from: Element, to: Element): void {
0
0
353
this.#replaceNode(from, to)
0
0
354
}
355
356
#morphOtherNode(from: ChildNode, to: ChildNode): void {
0
0
357
if (from.nodeType === to.nodeType && from.nodeValue !== null && to.nodeValue !== null) {
358
from.nodeValue = to.nodeValue
359
} else {
360
this.#replaceNode(from, to)
361
}
0
0
362
}
363
364
#visitAttributes(from: Element, to: Element): void {
···
454
unmatchedElements.clear()
455
whitespaceNodes.clear()
456
457
-
const seq: Array<number | undefined> = []
458
-
const matches: Array<number | undefined> = []
0
459
460
for (let i = 0; i < fromChildNodes.length; i++) {
461
const candidate = fromChildNodes[i]!
···
492
493
if (candidate.isEqualNode(element)) {
494
matches[unmatchedIndex] = candidateIndex
0
495
seq[candidateIndex] = unmatchedIndex
496
candidateElements.delete(candidateIndex)
497
unmatchedElements.delete(unmatchedIndex)
···
512
candidateLoop: for (const candidateIndex of candidateElements) {
513
const candidate = fromChildNodes[candidateIndex] as Element
514
515
-
// Match by exact id
516
-
if (id !== "" && element.localName === candidate.localName && id === candidate.id) {
517
-
matches[unmatchedIndex] = candidateIndex
518
-
seq[candidateIndex] = unmatchedIndex
519
-
candidateElements.delete(candidateIndex)
520
-
unmatchedElements.delete(unmatchedIndex)
521
-
break candidateLoop
522
-
}
0
0
523
524
-
// Match by idArray (to) against idSet (from)
525
-
if (idArray) {
526
-
const candidateIdSet = this.#idSetMap.get(candidate)
527
-
if (candidateIdSet) {
528
-
for (let i = 0; i < idArray.length; i++) {
529
-
const arrayId = idArray[i]!
530
-
if (candidateIdSet.has(arrayId)) {
531
-
matches[unmatchedIndex] = candidateIndex
532
-
seq[candidateIndex] = unmatchedIndex
533
-
candidateElements.delete(candidateIndex)
534
-
unmatchedElements.delete(unmatchedIndex)
535
-
break candidateLoop
0
0
536
}
537
}
538
}
···
558
) {
559
matches[unmatchedIndex] = candidateIndex
560
seq[candidateIndex] = unmatchedIndex
0
561
candidateElements.delete(candidateIndex)
562
unmatchedElements.delete(unmatchedIndex)
563
break
···
580
}
581
matches[unmatchedIndex] = candidateIndex
582
seq[candidateIndex] = unmatchedIndex
0
583
candidateElements.delete(candidateIndex)
584
unmatchedElements.delete(unmatchedIndex)
585
break
···
595
const candidate = fromChildNodes[candidateIndex]!
596
if (candidate.isEqualNode(node)) {
597
matches[unmatchedIndex] = candidateIndex
0
598
seq[candidateIndex] = unmatchedIndex
599
candidateNodes.delete(candidateIndex)
600
unmatchedNodes.delete(unmatchedIndex)
···
613
const candidate = fromChildNodes[candidateIndex]!
614
if (nodeType === candidate.nodeType) {
615
matches[unmatchedIndex] = candidateIndex
0
616
seq[candidateIndex] = unmatchedIndex
617
candidateNodes.delete(candidateIndex)
618
unmatchedNodes.delete(unmatchedIndex)
···
641
const matchInd = matches[i]
642
if (matchInd !== undefined) {
643
const match = fromChildNodes[matchInd]!
0
644
645
if (!shouldNotMove[matchInd]) {
646
moveBefore(parent, match, insertionPoint)
647
}
648
-
this.#morphOneToOne(match, node)
0
0
0
0
0
0
0
0
649
insertionPoint = match.nextSibling
650
} else {
651
if (this.#options.beforeNodeAdded?.(parent, node, insertionPoint) ?? true) {
···
3
const TEXT_NODE_TYPE = 3
4
const PARENT_NODE_TYPES = [false, true, false, false, false, false, false, false, false, true, false, true]
5
6
+
const Operation = {
7
+
EqualNode: 0,
8
+
SameElement: 1,
9
+
SameNode: 2,
10
+
} as const
11
+
12
+
type Operation = (typeof Operation)[keyof typeof Operation]
13
+
14
const candidateNodes: Set<number> = new Set()
15
const candidateElements: Set<number> = new Set()
16
const unmatchedNodes: Set<number> = new Set()
···
330
if (from === to) return
331
if (from.isEqualNode?.(to)) return
332
0
0
333
if (from.nodeType === ELEMENT_NODE_TYPE && to.nodeType === ELEMENT_NODE_TYPE) {
334
if ((from as Element).localName === (to as Element).localName) {
335
this.#morphMatchingElements(from as Element, to as Element)
···
339
} else {
340
this.#morphOtherNode(from, to)
341
}
0
0
342
}
343
344
#morphMatchingElements(from: Element, to: Element): void {
345
+
if (!(this.#options.beforeNodeVisited?.(from, to) ?? true)) return
346
+
347
if (from.hasAttributes() || to.hasAttributes()) {
348
this.#visitAttributes(from, to)
349
}
···
353
} else if (from.hasChildNodes() || to.hasChildNodes()) {
354
this.visitChildNodes(from, to)
355
}
356
+
357
+
this.#options.afterNodeVisited?.(from, to)
358
}
359
360
#morphNonMatchingElements(from: Element, to: Element): void {
361
+
if (!(this.#options.beforeNodeVisited?.(from, to) ?? true)) return
362
+
363
this.#replaceNode(from, to)
364
+
365
+
this.#options.afterNodeVisited?.(from, to)
366
}
367
368
#morphOtherNode(from: ChildNode, to: ChildNode): void {
369
+
if (!(this.#options.beforeNodeVisited?.(from, to) ?? true)) return
370
+
371
if (from.nodeType === to.nodeType && from.nodeValue !== null && to.nodeValue !== null) {
372
from.nodeValue = to.nodeValue
373
} else {
374
this.#replaceNode(from, to)
375
}
376
+
377
+
this.#options.afterNodeVisited?.(from, to)
378
}
379
380
#visitAttributes(from: Element, to: Element): void {
···
470
unmatchedElements.clear()
471
whitespaceNodes.clear()
472
473
+
const seq: Array<number> = []
474
+
const matches: Array<number> = []
475
+
const op: Array<Operation> = []
476
477
for (let i = 0; i < fromChildNodes.length; i++) {
478
const candidate = fromChildNodes[i]!
···
509
510
if (candidate.isEqualNode(element)) {
511
matches[unmatchedIndex] = candidateIndex
512
+
op[unmatchedIndex] = Operation.EqualNode
513
seq[candidateIndex] = unmatchedIndex
514
candidateElements.delete(candidateIndex)
515
unmatchedElements.delete(unmatchedIndex)
···
530
candidateLoop: for (const candidateIndex of candidateElements) {
531
const candidate = fromChildNodes[candidateIndex] as Element
532
533
+
if (element.localName === candidate.localName) {
534
+
// Match by exact id
535
+
if (id !== "" && id === candidate.id) {
536
+
matches[unmatchedIndex] = candidateIndex
537
+
op[unmatchedIndex] = Operation.SameElement
538
+
seq[candidateIndex] = unmatchedIndex
539
+
candidateElements.delete(candidateIndex)
540
+
unmatchedElements.delete(unmatchedIndex)
541
+
break candidateLoop
542
+
}
543
544
+
// Match by idArray (to) against idSet (from)
545
+
if (idArray) {
546
+
const candidateIdSet = this.#idSetMap.get(candidate)
547
+
if (candidateIdSet) {
548
+
for (let i = 0; i < idArray.length; i++) {
549
+
const arrayId = idArray[i]!
550
+
if (candidateIdSet.has(arrayId)) {
551
+
matches[unmatchedIndex] = candidateIndex
552
+
op[unmatchedIndex] = Operation.SameElement
553
+
seq[candidateIndex] = unmatchedIndex
554
+
candidateElements.delete(candidateIndex)
555
+
unmatchedElements.delete(unmatchedIndex)
556
+
break candidateLoop
557
+
}
558
}
559
}
560
}
···
580
) {
581
matches[unmatchedIndex] = candidateIndex
582
seq[candidateIndex] = unmatchedIndex
583
+
op[unmatchedIndex] = Operation.SameElement
584
candidateElements.delete(candidateIndex)
585
unmatchedElements.delete(unmatchedIndex)
586
break
···
603
}
604
matches[unmatchedIndex] = candidateIndex
605
seq[candidateIndex] = unmatchedIndex
606
+
op[unmatchedIndex] = Operation.SameElement
607
candidateElements.delete(candidateIndex)
608
unmatchedElements.delete(unmatchedIndex)
609
break
···
619
const candidate = fromChildNodes[candidateIndex]!
620
if (candidate.isEqualNode(node)) {
621
matches[unmatchedIndex] = candidateIndex
622
+
op[unmatchedIndex] = Operation.EqualNode
623
seq[candidateIndex] = unmatchedIndex
624
candidateNodes.delete(candidateIndex)
625
unmatchedNodes.delete(unmatchedIndex)
···
638
const candidate = fromChildNodes[candidateIndex]!
639
if (nodeType === candidate.nodeType) {
640
matches[unmatchedIndex] = candidateIndex
641
+
op[unmatchedIndex] = Operation.SameNode
642
seq[candidateIndex] = unmatchedIndex
643
candidateNodes.delete(candidateIndex)
644
unmatchedNodes.delete(unmatchedIndex)
···
667
const matchInd = matches[i]
668
if (matchInd !== undefined) {
669
const match = fromChildNodes[matchInd]!
670
+
const operation = op[matchInd]!
671
672
if (!shouldNotMove[matchInd]) {
673
moveBefore(parent, match, insertionPoint)
674
}
675
+
676
+
if (operation === Operation.EqualNode) {
677
+
} else if (operation === Operation.SameElement) {
678
+
// this.#morphMatchingElements(match as Element, node as Element)
679
+
this.#morphMatchingElements(match as Element, node as Element)
680
+
} else {
681
+
this.#morphOneToOne(match, node)
682
+
}
683
+
684
insertionPoint = match.nextSibling
685
} else {
686
if (this.#options.beforeNodeAdded?.(parent, node, insertionPoint) ?? true) {