Precise DOM morphing
morphing typescript dom

Support morphing documents

+50 -8
+1
AGENTS.md
··· 6 6 - Make sure you leave things in a good state. No diagnostics warnings. No type errors. 7 7 - We use tabs for indentation and spaces for alignment 8 8 - Never say “you’re absolutely right” 9 + - When writing new tests, put them under `test/new` and use `test` instead of `it`. Try to keep all the setup in the test itself. If you need to share setup between multiple steps, make a function that each test calls.
+49 -8
src/morphlex.ts
··· 1 - const SupportsMoveBefore = "moveBefore" in Element.prototype 2 - const ParentNodeTypes = new Set([1, 9, 11]) 1 + const PARENT_NODE_TYPES = new Set([1, 9, 11]) 2 + const SUPPORTS_MOVE_BEFORE = "moveBefore" in Element.prototype 3 3 4 4 type IdSet = Set<string> 5 5 type IdMap = WeakMap<Node, IdSet> ··· 10 10 type PairOfNodes<N extends Node> = [N, N] 11 11 type PairOfMatchingElements<E extends Element> = Branded<PairOfNodes<E>, "MatchingElementPair"> 12 12 13 - interface Options { 13 + export interface Options { 14 14 preserveModified?: boolean 15 15 beforeNodeVisited?: (fromNode: Node, toNode: Node) => boolean 16 16 afterNodeVisited?: (fromNode: Node, toNode: Node) => void ··· 28 28 moveBefore: (node: ChildNode, before: ChildNode | null) => void 29 29 } 30 30 31 + /** 32 + * Morph one document to another. If the `to` document is a string, it will be parsed with a DOMParser. 33 + * 34 + * @param from The source document to morph from. 35 + * @param to The target document or string to morph to. 36 + * @example 37 + * ```ts 38 + * morphDocument(document, newDocument) 39 + * ``` 40 + */ 41 + export function morphDocument(from: Document, to: Document | string): void { 42 + if (typeof to === "string") to = parseDocument(to) 43 + morph(from.documentElement, to.documentElement) 44 + } 45 + 46 + /** 47 + * Morph one `ChildNode` to another. If the `to` node is a string, it will be parsed with a `<template>` element. 48 + * 49 + * @param from The source node to morph from. 50 + * @param to The target node, node list or string to morph to. 51 + * @example 52 + * ```ts 53 + * morph(originalDom, newDom) 54 + * ``` 55 + */ 31 56 export function morph(from: ChildNode, to: ChildNode | NodeListOf<ChildNode> | string, options: Options = {}): void { 32 - if (typeof to === "string") to = parseString(to).childNodes 57 + if (typeof to === "string") to = parseFragment(to).childNodes 33 58 34 59 if (isParentNode(from)) flagDirtyInputs(from) 35 60 36 61 new Morph(options).morph(from, to) 37 62 } 38 63 64 + /** 65 + * Morph the inner content of one ChildNode to the inner content of another. 66 + * If the `to` node is a string, it will be parsed with a `<template>` element. 67 + * 68 + * @param from The source node to morph from. 69 + * @param to The target node, node list or string to morph to. 70 + * @example 71 + * ```ts 72 + * morphInner(originalDom, newDom) 73 + * ``` 74 + */ 39 75 export function morphInner(from: ChildNode, to: ChildNode | string, options: Options = {}): void { 40 76 if (typeof to === "string") { 41 - const fragment = parseString(to) 77 + const fragment = parseFragment(to) 42 78 43 79 if (fragment.firstChild && fragment.childNodes.length === 1 && isElement(fragment.firstChild)) { 44 80 to = fragment.firstChild ··· 80 116 } 81 117 } 82 118 83 - function parseString(string: string): DocumentFragment { 119 + function parseFragment(string: string): DocumentFragment { 84 120 const template = document.createElement("template") 85 121 template.innerHTML = string.trim() 86 122 87 123 return template.content 124 + } 125 + 126 + function parseDocument(string: string): Document { 127 + const parser = new DOMParser() 128 + return parser.parseFromString(string.trim(), "text/html") 88 129 } 89 130 90 131 function moveBefore(parent: ParentNode, node: ChildNode, insertionPoint: ChildNode | null): void { ··· 598 639 } 599 640 600 641 function supportsMoveBefore(_node: ParentNode): _node is NodeWithMoveBefore { 601 - return SupportsMoveBefore 642 + return SUPPORTS_MOVE_BEFORE 602 643 } 603 644 604 645 function isMatchingElementPair(pair: PairOfNodes<Element>): pair is PairOfMatchingElements<Element> { ··· 632 673 } 633 674 634 675 function isParentNode(node: Node): node is ParentNode { 635 - return ParentNodeTypes.has(node.nodeType) 676 + return PARENT_NODE_TYPES.has(node.nodeType) 636 677 }