Precise DOM morphing
morphing typescript dom

Add options and callbacks (#9)

authored by joel.drapper.me and committed by

GitHub 272963f8 82a09d71

+230 -110
+1 -1
.prettierrc
··· 1 1 { 2 - "printWidth": 120, 2 + "printWidth": 130, 3 3 "useTabs": true, 4 4 }
+17 -1
dist/morphlex.d.ts
··· 1 - export declare function morph(node: ChildNode, reference: ChildNode): void; 1 + type ObjectKey = string | number | symbol; 2 + interface Options { 3 + ignoreActiveValue?: boolean; 4 + preserveModifiedValues?: boolean; 5 + beforeNodeMorphed?: (node: Node, referenceNode: Node) => boolean; 6 + afterNodeMorphed?: (node: Node) => void; 7 + beforeNodeAdded?: (newNode: Node, parentNode: ParentNode | null) => boolean; 8 + afterNodeAdded?: (newNode: Node) => void; 9 + beforeNodeRemoved?: (oldNode: Node) => boolean; 10 + afterNodeRemoved?: (oldNode: Node) => void; 11 + beforeAttributeUpdated?: (attributeName: string, newValue: string, element: Element) => boolean; 12 + afterAttributeUpdated?: (attributeName: string, previousValue: string | null, element: Element) => void; 13 + beforePropertyUpdated?: (propertyName: ObjectKey, newValue: unknown, node: Node) => boolean; 14 + afterPropertyUpdated?: (propertyName: ObjectKey, previousValue: unknown, node: Node) => void; 15 + } 16 + export declare function morph(node: ChildNode, reference: ChildNode, options?: Options): void; 17 + export {};
+76 -37
dist/morphlex.js
··· 1 - export function morph(node, reference) { 1 + export function morph(node, reference, options = {}) { 2 2 const readonlyReference = reference; 3 3 const idMap = new WeakMap(); 4 4 if (isParentNode(node) && isParentNode(readonlyReference)) { 5 5 populateIdSets(node, idMap); 6 6 populateIdSets(readonlyReference, idMap); 7 7 } 8 - morphNodes(node, readonlyReference, idMap); 8 + morphNode(node, readonlyReference, { ...options, idMap }); 9 9 } 10 10 // For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements. 11 11 function populateIdSets(node, idMap) { ··· 24 24 } 25 25 } 26 26 // This is where we actually morph the nodes. The `morph` function exists to set up the `idMap`. 27 - function morphNodes(node, ref, idMap) { 27 + function morphNode(node, ref, context) { 28 + const writableRef = ref; 29 + if (!(context.beforeNodeMorphed?.(node, writableRef) ?? true)) return; 28 30 if (isElement(node) && isElement(ref) && node.tagName === ref.tagName) { 29 - if (node.hasAttributes() || ref.hasAttributes()) morphAttributes(node, ref); 31 + if (node.hasAttributes() || ref.hasAttributes()) morphAttributes(node, ref, context); 30 32 if (isHead(node) && isHead(ref)) { 31 33 const refChildNodes = new Map(); 32 34 for (const child of ref.children) refChildNodes.set(child.outerHTML, child); 33 35 for (const child of node.children) { 34 36 const key = child.outerHTML; 35 37 const refChild = refChildNodes.get(key); 36 - refChild ? refChildNodes.delete(key) : child.remove(); 38 + refChild ? refChildNodes.delete(key) : child.remove(); // TODO add callback 37 39 } 38 - for (const refChild of refChildNodes.values()) node.appendChild(refChild.cloneNode(true)); 39 - } else if (node.hasChildNodes() || ref.hasChildNodes()) morphChildNodes(node, ref, idMap); 40 + for (const refChild of refChildNodes.values()) appendChild(node, refChild.cloneNode(true), context); 41 + } else if (node.hasChildNodes() || ref.hasChildNodes()) morphChildNodes(node, ref, context); 40 42 } else { 41 43 if (isText(node) && isText(ref)) { 42 - if (node.textContent !== ref.textContent) node.textContent = ref.textContent; 44 + updateProperty(node, "textContent", ref.textContent, context); 43 45 } else if (isComment(node) && isComment(ref)) { 44 - if (node.nodeValue !== ref.nodeValue) node.nodeValue = ref.nodeValue; 45 - } else node.replaceWith(ref.cloneNode(true)); 46 + updateProperty(node, "nodeValue", ref.nodeValue, context); 47 + } else replaceNode(node, ref.cloneNode(true), context); 46 48 } 49 + context.afterNodeMorphed?.(node); 47 50 } 48 - function morphAttributes(elm, ref) { 51 + function morphAttributes(element, ref, context) { 49 52 // Remove any excess attributes from the element that aren’t present in the reference. 50 - for (const { name } of elm.attributes) ref.hasAttribute(name) || elm.removeAttribute(name); 53 + for (const { name } of element.attributes) ref.hasAttribute(name) || element.removeAttribute(name); 51 54 // Copy attributes from the reference to the element, if they don’t already match. 52 - for (const { name, value } of ref.attributes) elm.getAttribute(name) === value || elm.setAttribute(name, value); 55 + for (const { name, value } of ref.attributes) { 56 + const previousValue = element.getAttribute(name); 57 + if (previousValue !== value && (context.beforeAttributeUpdated?.(name, value, element) ?? true)) { 58 + element.setAttribute(name, value); 59 + context.afterAttributeUpdated?.(name, previousValue, element); 60 + } 61 + } 53 62 // For certain types of elements, we need to do some extra work to ensure 54 63 // the element’s state matches the reference elements’ state. 55 - if (isInput(elm) && isInput(ref)) { 56 - if (elm.checked !== ref.checked) elm.checked = ref.checked; 57 - if (elm.disabled !== ref.disabled) elm.disabled = ref.disabled; 58 - if (elm.indeterminate !== ref.indeterminate) elm.indeterminate = ref.indeterminate; 59 - if (elm.type !== "file" && elm.value !== ref.value) elm.value = ref.value; 60 - } else if (isOption(elm) && isOption(ref) && elm.selected !== ref.selected) elm.selected = ref.selected; 61 - else if (isTextArea(elm) && isTextArea(ref)) { 62 - if (elm.value !== ref.value) elm.value = ref.value; 63 - const text = elm.firstChild; 64 - if (text && isText(text) && text.textContent !== ref.value) text.textContent = ref.value; 64 + if (isInput(element) && isInput(ref)) { 65 + updateProperty(element, "checked", ref.checked, context); 66 + updateProperty(element, "disabled", ref.disabled, context); 67 + updateProperty(element, "indeterminate", ref.indeterminate, context); 68 + if ( 69 + element.type !== "file" && 70 + !(context.ignoreActiveValue && document.activeElement === element) && 71 + !(context.preserveModifiedValues && element.value !== element.defaultValue) 72 + ) 73 + updateProperty(element, "value", ref.value, context); 74 + } else if (isOption(element) && isOption(ref)) updateProperty(element, "selected", ref.selected, context); 75 + else if (isTextArea(element) && isTextArea(ref)) { 76 + updateProperty(element, "value", ref.value, context); 77 + // TODO: Do we need this? If so, how do we integrate with the callback? 78 + const text = element.firstChild; 79 + if (text && isText(text)) updateProperty(text, "textContent", ref.value, context); 65 80 } 66 81 } 67 82 // Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match. 68 - function morphChildNodes(element, ref, idMap) { 83 + function morphChildNodes(element, ref, context) { 69 84 const childNodes = [...element.childNodes]; 70 85 const refChildNodes = [...ref.childNodes]; 71 86 for (let i = 0; i < refChildNodes.length; i++) { 72 87 const child = childNodes.at(i); 73 88 const refChild = refChildNodes.at(i); 74 - if (child && refChild) morphChildNode(child, refChild, element, idMap); 75 - else if (refChild) element.appendChild(refChild.cloneNode(true)); 76 - else if (child) child.remove(); 89 + if (child && refChild) morphChildNode(child, refChild, element, context); 90 + else if (refChild) { 91 + appendChild(element, refChild.cloneNode(true), context); 92 + } else if (child && (context.beforeNodeRemoved?.(child) ?? true)) { 93 + child.remove(); 94 + context.afterNodeRemoved?.(child); 95 + } 77 96 } 78 97 // Remove any excess child nodes from the main element. This is separate because 79 98 // the loop above might modify the length of the main element’s child nodes. 80 99 while (element.childNodes.length > ref.childNodes.length) element.lastChild?.remove(); 81 100 } 82 - function morphChildNode(child, ref, parent, idMap) { 83 - if (isElement(child) && isElement(ref)) morphChildElement(child, ref, parent, idMap); 84 - else morphNodes(child, ref, idMap); 101 + function updateProperty(element, propertyName, newValue, context) { 102 + const previousValue = element[propertyName]; 103 + if (previousValue !== newValue && (context.beforePropertyUpdated?.(propertyName, newValue, element) ?? true)) { 104 + element[propertyName] = newValue; 105 + context.afterPropertyUpdated?.(propertyName, previousValue, element); 106 + } 85 107 } 86 - function morphChildElement(child, ref, parent, idMap) { 87 - const refIdSet = idMap.get(ref); 108 + function morphChildNode(child, ref, parent, context) { 109 + if (isElement(child) && isElement(ref)) morphChildElement(child, ref, parent, context); 110 + else morphNode(child, ref, context); 111 + } 112 + function morphChildElement(child, ref, parent, context) { 113 + const refIdSet = context.idMap.get(ref); 88 114 // Generate the array in advance of the loop 89 115 const refSetArray = refIdSet ? [...refIdSet] : []; 90 116 let currentNode = child; ··· 94 120 if (isElement(currentNode)) { 95 121 if (currentNode.id === ref.id) { 96 122 parent.insertBefore(currentNode, child); 97 - return morphNodes(currentNode, ref, idMap); 123 + return morphNode(currentNode, ref, context); 98 124 } else { 99 125 if (currentNode.id !== "") { 100 - const currentIdSet = idMap.get(currentNode); 126 + const currentIdSet = context.idMap.get(currentNode); 101 127 if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) { 102 128 parent.insertBefore(currentNode, child); 103 - return morphNodes(currentNode, ref, idMap); 129 + return morphNode(currentNode, ref, context); 104 130 } else if (!nextMatchByTagName && currentNode.tagName === ref.tagName) { 105 131 nextMatchByTagName = currentNode; 106 132 } ··· 111 137 } 112 138 if (nextMatchByTagName) { 113 139 if (nextMatchByTagName !== child) parent.insertBefore(nextMatchByTagName, child); 114 - morphNodes(nextMatchByTagName, ref, idMap); 115 - } else child.replaceWith(ref.cloneNode(true)); 140 + morphNode(nextMatchByTagName, ref, context); 141 + } else replaceNode(child, ref.cloneNode(true), context); 142 + } 143 + function replaceNode(node, newNode, context) { 144 + if ((context.beforeNodeRemoved?.(node) ?? true) && (context.beforeNodeAdded?.(newNode, node.parentNode) ?? true)) { 145 + node.replaceWith(newNode); 146 + context.afterNodeAdded?.(newNode); 147 + context.afterNodeRemoved?.(node); 148 + } 149 + } 150 + function appendChild(node, newNode, context) { 151 + if (context.beforeNodeAdded?.(newNode, node) ?? true) { 152 + node.appendChild(newNode); 153 + context.afterNodeAdded?.(newNode); 154 + } 116 155 } 117 156 function isText(node) { 118 157 return node.nodeType === 3;
+1 -1
package.json
··· 5 5 "license": "MIT", 6 6 "type": "module", 7 7 "description": "A safe, tiny (less than 1KB minified & gzipped), optimal DOM morphing library written in TypeScript.", 8 - "main": "dist/morphlex.js", 8 + "main": "dist/morphlex.min.js", 9 9 "types": "dist/morphlex.d.ts", 10 10 "funding": { 11 11 "type": "github",
+135 -70
src/morphlex.ts
··· 1 1 type IdSet = Set<string>; 2 - type IdMap = WeakMap<ReadOnlyNode<Node>, IdSet>; 2 + type IdMap = WeakMap<ReadonlyNode<Node>, IdSet>; 3 + type ObjectKey = string | number | symbol; 3 4 4 5 // Maps to a type that can only read properties 5 - type StrongReadOnly<T> = { 6 - readonly [K in keyof T as T[K] extends Function ? never : K]: T[K]; 7 - }; 6 + type StrongReadonly<T> = { readonly [K in keyof T as T[K] extends Function ? never : K]: T[K] }; 8 7 9 8 // Maps a Node to a type limited to read-only properties and methods for that Node 10 - type ReadOnlyNode<T extends Node> = 9 + type ReadonlyNode<T extends Node> = 11 10 | T 12 - | (StrongReadOnly<T> & { 11 + | (StrongReadonly<T> & { 13 12 readonly cloneNode: (deep: true) => Node; 14 - readonly childNodes: ReadOnlyNodeList<ChildNode>; 15 - readonly querySelectorAll: (query: string) => ReadOnlyNodeList<Element>; 16 - readonly parentElement: ReadOnlyNode<Element> | null; 13 + readonly childNodes: ReadonlyNodeList<ChildNode>; 14 + readonly querySelectorAll: (query: string) => ReadonlyNodeList<Element>; 15 + readonly parentElement: ReadonlyNode<Element> | null; 17 16 readonly hasAttribute: (name: string) => boolean; 18 17 readonly hasAttributes: () => boolean; 19 18 readonly hasChildNodes: () => boolean; 20 - readonly children: ReadOnlyNodeList<Element>; 19 + readonly children: ReadonlyNodeList<Element>; 21 20 }); 22 21 23 22 // Maps a node to a read-only node list of nodes of that type 24 - type ReadOnlyNodeList<T extends Node> = 23 + type ReadonlyNodeList<T extends Node> = 25 24 | NodeListOf<T> 26 25 | { 27 - [Symbol.iterator](): IterableIterator<ReadOnlyNode<T>>; 28 - readonly [index: number]: ReadOnlyNode<T>; 26 + [Symbol.iterator](): IterableIterator<ReadonlyNode<T>>; 27 + readonly [index: number]: ReadonlyNode<T>; 29 28 readonly length: NodeListOf<T>["length"]; 30 29 }; 31 30 32 - export function morph(node: ChildNode, reference: ChildNode): void { 33 - const readonlyReference = reference as ReadOnlyNode<ChildNode>; 31 + interface Options { 32 + ignoreActiveValue?: boolean; 33 + preserveModifiedValues?: boolean; 34 + 35 + beforeNodeMorphed?: (node: Node, referenceNode: Node) => boolean; 36 + afterNodeMorphed?: (node: Node) => void; 37 + 38 + beforeNodeAdded?: (newNode: Node, parentNode: ParentNode | null) => boolean; 39 + afterNodeAdded?: (newNode: Node) => void; 40 + 41 + beforeNodeRemoved?: (oldNode: Node) => boolean; 42 + afterNodeRemoved?: (oldNode: Node) => void; 43 + 44 + beforeAttributeUpdated?: (attributeName: string, newValue: string, element: Element) => boolean; 45 + afterAttributeUpdated?: (attributeName: string, previousValue: string | null, element: Element) => void; 46 + 47 + beforePropertyUpdated?: (propertyName: ObjectKey, newValue: unknown, node: Node) => boolean; 48 + afterPropertyUpdated?: (propertyName: ObjectKey, previousValue: unknown, node: Node) => void; 49 + } 50 + 51 + type Context = Options & { idMap: IdMap }; 52 + 53 + export function morph(node: ChildNode, reference: ChildNode, options: Options = {}): void { 54 + const readonlyReference = reference as ReadonlyNode<ChildNode>; 34 55 const idMap: IdMap = new WeakMap(); 35 56 36 57 if (isParentNode(node) && isParentNode(readonlyReference)) { ··· 38 59 populateIdSets(readonlyReference, idMap); 39 60 } 40 61 41 - morphNodes(node, readonlyReference, idMap); 62 + morphNode(node, readonlyReference, { ...options, idMap }); 42 63 } 43 64 44 65 // For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements. 45 - function populateIdSets(node: ReadOnlyNode<ParentNode>, idMap: IdMap): void { 66 + function populateIdSets(node: ReadonlyNode<ParentNode>, idMap: IdMap): void { 46 67 const elementsWithIds = node.querySelectorAll("[id]"); 47 68 48 69 for (const elementWithId of elementsWithIds) { ··· 51 72 // Ignore empty IDs 52 73 if (id === "") continue; 53 74 54 - let current: ReadOnlyNode<Element> | null = elementWithId; 75 + let current: ReadonlyNode<Element> | null = elementWithId; 55 76 56 77 while (current) { 57 78 const idSet: IdSet | undefined = idMap.get(current); ··· 63 84 } 64 85 65 86 // This is where we actually morph the nodes. The `morph` function exists to set up the `idMap`. 66 - function morphNodes(node: ChildNode, ref: ReadOnlyNode<ChildNode>, idMap: IdMap): void { 87 + function morphNode(node: ChildNode, ref: ReadonlyNode<ChildNode>, context: Context): void { 88 + const writableRef = ref as ChildNode; 89 + if (!(context.beforeNodeMorphed?.(node, writableRef) ?? true)) return; 90 + 67 91 if (isElement(node) && isElement(ref) && node.tagName === ref.tagName) { 68 - if (node.hasAttributes() || ref.hasAttributes()) morphAttributes(node, ref); 92 + if (node.hasAttributes() || ref.hasAttributes()) morphAttributes(node, ref, context); 69 93 if (isHead(node) && isHead(ref)) { 70 - const refChildNodes: Map<string, ReadOnlyNode<Element>> = new Map(); 94 + const refChildNodes: Map<string, ReadonlyNode<Element>> = new Map(); 71 95 for (const child of ref.children) refChildNodes.set(child.outerHTML, child); 72 96 for (const child of node.children) { 73 97 const key = child.outerHTML; 74 98 const refChild = refChildNodes.get(key); 75 - refChild ? refChildNodes.delete(key) : child.remove(); 99 + refChild ? refChildNodes.delete(key) : child.remove(); // TODO add callback 76 100 } 77 - for (const refChild of refChildNodes.values()) node.appendChild(refChild.cloneNode(true)); 78 - } else if (node.hasChildNodes() || ref.hasChildNodes()) morphChildNodes(node, ref, idMap); 101 + for (const refChild of refChildNodes.values()) appendChild(node, refChild.cloneNode(true), context); 102 + } else if (node.hasChildNodes() || ref.hasChildNodes()) morphChildNodes(node, ref, context); 79 103 } else { 80 104 if (isText(node) && isText(ref)) { 81 - if (node.textContent !== ref.textContent) node.textContent = ref.textContent; 105 + updateProperty(node, "textContent", ref.textContent, context); 82 106 } else if (isComment(node) && isComment(ref)) { 83 - if (node.nodeValue !== ref.nodeValue) node.nodeValue = ref.nodeValue; 84 - } else node.replaceWith(ref.cloneNode(true)); 107 + updateProperty(node, "nodeValue", ref.nodeValue, context); 108 + } else replaceNode(node, ref.cloneNode(true), context); 85 109 } 110 + 111 + context.afterNodeMorphed?.(node); 86 112 } 87 113 88 - function morphAttributes(elm: Element, ref: ReadOnlyNode<Element>): void { 114 + function morphAttributes(element: Element, ref: ReadonlyNode<Element>, context: Context): void { 89 115 // Remove any excess attributes from the element that aren’t present in the reference. 90 - for (const { name } of elm.attributes) ref.hasAttribute(name) || elm.removeAttribute(name); 116 + for (const { name } of element.attributes) ref.hasAttribute(name) || element.removeAttribute(name); 91 117 92 118 // Copy attributes from the reference to the element, if they don’t already match. 93 - for (const { name, value } of ref.attributes) elm.getAttribute(name) === value || elm.setAttribute(name, value); 119 + for (const { name, value } of ref.attributes) { 120 + const previousValue = element.getAttribute(name); 121 + if (previousValue !== value && (context.beforeAttributeUpdated?.(name, value, element) ?? true)) { 122 + element.setAttribute(name, value); 123 + context.afterAttributeUpdated?.(name, previousValue, element); 124 + } 125 + } 94 126 95 127 // For certain types of elements, we need to do some extra work to ensure 96 128 // the element’s state matches the reference elements’ state. 97 - if (isInput(elm) && isInput(ref)) { 98 - if (elm.checked !== ref.checked) elm.checked = ref.checked; 99 - if (elm.disabled !== ref.disabled) elm.disabled = ref.disabled; 100 - if (elm.indeterminate !== ref.indeterminate) elm.indeterminate = ref.indeterminate; 101 - if (elm.type !== "file" && elm.value !== ref.value) elm.value = ref.value; 102 - } else if (isOption(elm) && isOption(ref) && elm.selected !== ref.selected) elm.selected = ref.selected; 103 - else if (isTextArea(elm) && isTextArea(ref)) { 104 - if (elm.value !== ref.value) elm.value = ref.value; 129 + if (isInput(element) && isInput(ref)) { 130 + updateProperty(element, "checked", ref.checked, context); 131 + updateProperty(element, "disabled", ref.disabled, context); 132 + updateProperty(element, "indeterminate", ref.indeterminate, context); 133 + if ( 134 + element.type !== "file" && 135 + !(context.ignoreActiveValue && document.activeElement === element) && 136 + !(context.preserveModifiedValues && element.value !== element.defaultValue) 137 + ) 138 + updateProperty(element, "value", ref.value, context); 139 + } else if (isOption(element) && isOption(ref)) updateProperty(element, "selected", ref.selected, context); 140 + else if (isTextArea(element) && isTextArea(ref)) { 141 + updateProperty(element, "value", ref.value, context); 105 142 106 - const text = elm.firstChild; 107 - if (text && isText(text) && text.textContent !== ref.value) text.textContent = ref.value; 143 + // TODO: Do we need this? If so, how do we integrate with the callback? 144 + const text = element.firstChild; 145 + if (text && isText(text)) updateProperty(text, "textContent", ref.value, context); 108 146 } 109 147 } 110 148 111 149 // Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match. 112 - function morphChildNodes(element: Element, ref: ReadOnlyNode<Element>, idMap: IdMap): void { 150 + function morphChildNodes(element: Element, ref: ReadonlyNode<Element>, context: Context): void { 113 151 const childNodes = [...element.childNodes]; 114 152 const refChildNodes = [...ref.childNodes]; 115 153 ··· 117 155 const child = childNodes.at(i); 118 156 const refChild = refChildNodes.at(i); 119 157 120 - if (child && refChild) morphChildNode(child, refChild, element, idMap); 121 - else if (refChild) element.appendChild(refChild.cloneNode(true)); 122 - else if (child) child.remove(); 158 + if (child && refChild) morphChildNode(child, refChild, element, context); 159 + else if (refChild) { 160 + appendChild(element, refChild.cloneNode(true), context); 161 + } else if (child && (context.beforeNodeRemoved?.(child) ?? true)) { 162 + child.remove(); 163 + context.afterNodeRemoved?.(child); 164 + } 123 165 } 124 166 125 167 // Remove any excess child nodes from the main element. This is separate because ··· 127 169 while (element.childNodes.length > ref.childNodes.length) element.lastChild?.remove(); 128 170 } 129 171 130 - function morphChildNode(child: ChildNode, ref: ReadOnlyNode<ChildNode>, parent: Element, idMap: IdMap): void { 131 - if (isElement(child) && isElement(ref)) morphChildElement(child, ref, parent, idMap); 132 - else morphNodes(child, ref, idMap); 172 + function updateProperty<N extends Node, P extends keyof N>(element: N, propertyName: P, newValue: N[P], context: Context): void { 173 + const previousValue = element[propertyName]; 174 + if (previousValue !== newValue && (context.beforePropertyUpdated?.(propertyName, newValue, element) ?? true)) { 175 + element[propertyName] = newValue; 176 + context.afterPropertyUpdated?.(propertyName, previousValue, element); 177 + } 178 + } 179 + 180 + function morphChildNode(child: ChildNode, ref: ReadonlyNode<ChildNode>, parent: Element, context: Context): void { 181 + if (isElement(child) && isElement(ref)) morphChildElement(child, ref, parent, context); 182 + else morphNode(child, ref, context); 133 183 } 134 184 135 - function morphChildElement(child: Element, ref: ReadOnlyNode<Element>, parent: Element, idMap: IdMap): void { 136 - const refIdSet = idMap.get(ref); 185 + function morphChildElement(child: Element, ref: ReadonlyNode<Element>, parent: Element, context: Context): void { 186 + const refIdSet = context.idMap.get(ref); 137 187 138 188 // Generate the array in advance of the loop 139 189 const refSetArray = refIdSet ? [...refIdSet] : []; ··· 146 196 if (isElement(currentNode)) { 147 197 if (currentNode.id === ref.id) { 148 198 parent.insertBefore(currentNode, child); 149 - return morphNodes(currentNode, ref, idMap); 199 + return morphNode(currentNode, ref, context); 150 200 } else { 151 201 if (currentNode.id !== "") { 152 - const currentIdSet = idMap.get(currentNode); 202 + const currentIdSet = context.idMap.get(currentNode); 153 203 154 204 if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) { 155 205 parent.insertBefore(currentNode, child); 156 - return morphNodes(currentNode, ref, idMap); 206 + return morphNode(currentNode, ref, context); 157 207 } else if (!nextMatchByTagName && currentNode.tagName === ref.tagName) { 158 208 nextMatchByTagName = currentNode; 159 209 } ··· 166 216 167 217 if (nextMatchByTagName) { 168 218 if (nextMatchByTagName !== child) parent.insertBefore(nextMatchByTagName, child); 169 - morphNodes(nextMatchByTagName, ref, idMap); 170 - } else child.replaceWith(ref.cloneNode(true)); 219 + morphNode(nextMatchByTagName, ref, context); 220 + } else replaceNode(child, ref.cloneNode(true), context); 221 + } 222 + 223 + function replaceNode(node: ChildNode, newNode: Node, context: Context): void { 224 + if ((context.beforeNodeRemoved?.(node) ?? true) && (context.beforeNodeAdded?.(newNode, node.parentNode) ?? true)) { 225 + node.replaceWith(newNode); 226 + context.afterNodeAdded?.(newNode); 227 + context.afterNodeRemoved?.(node); 228 + } 229 + } 230 + 231 + function appendChild(node: ParentNode, newNode: Node, context: Context): void { 232 + if (context.beforeNodeAdded?.(newNode, node) ?? true) { 233 + node.appendChild(newNode); 234 + context.afterNodeAdded?.(newNode); 235 + } 171 236 } 172 237 173 238 // We cannot use `instanceof` when nodes might be from different documents, ··· 175 240 // the necessary checks at runtime. 176 241 177 242 function isText(node: Node): node is Text; 178 - function isText(node: ReadOnlyNode<Node>): node is ReadOnlyNode<Text>; 179 - function isText(node: Node | ReadOnlyNode<Node>): boolean { 243 + function isText(node: ReadonlyNode<Node>): node is ReadonlyNode<Text>; 244 + function isText(node: Node | ReadonlyNode<Node>): boolean { 180 245 return node.nodeType === 3; 181 246 } 182 247 183 248 function isComment(node: Node): node is Comment; 184 - function isComment(node: ReadOnlyNode<Node>): node is ReadOnlyNode<Comment>; 185 - function isComment(node: Node | ReadOnlyNode<Node>): boolean { 249 + function isComment(node: ReadonlyNode<Node>): node is ReadonlyNode<Comment>; 250 + function isComment(node: Node | ReadonlyNode<Node>): boolean { 186 251 return node.nodeType === 8; 187 252 } 188 253 189 254 function isElement(node: Node): node is Element; 190 - function isElement(node: ReadOnlyNode<Node>): node is ReadOnlyNode<Element>; 191 - function isElement(node: Node | ReadOnlyNode<Node>): boolean { 255 + function isElement(node: ReadonlyNode<Node>): node is ReadonlyNode<Element>; 256 + function isElement(node: Node | ReadonlyNode<Node>): boolean { 192 257 return node.nodeType === 1; 193 258 } 194 259 195 260 function isInput(element: Element): element is HTMLInputElement; 196 - function isInput(element: ReadOnlyNode<Element>): element is ReadOnlyNode<HTMLInputElement>; 197 - function isInput(element: Element | ReadOnlyNode<Element>): boolean { 261 + function isInput(element: ReadonlyNode<Element>): element is ReadonlyNode<HTMLInputElement>; 262 + function isInput(element: Element | ReadonlyNode<Element>): boolean { 198 263 return element.localName === "input"; 199 264 } 200 265 201 266 function isOption(element: Element): element is HTMLOptionElement; 202 - function isOption(element: ReadOnlyNode<Element>): element is ReadOnlyNode<HTMLOptionElement>; 203 - function isOption(element: Element | ReadOnlyNode<Element>): boolean { 267 + function isOption(element: ReadonlyNode<Element>): element is ReadonlyNode<HTMLOptionElement>; 268 + function isOption(element: Element | ReadonlyNode<Element>): boolean { 204 269 return element.localName === "option"; 205 270 } 206 271 207 272 function isTextArea(element: Element): element is HTMLTextAreaElement; 208 - function isTextArea(element: ReadOnlyNode<Element>): element is ReadOnlyNode<HTMLTextAreaElement>; 209 - function isTextArea(element: Element | ReadOnlyNode<Element>): boolean { 273 + function isTextArea(element: ReadonlyNode<Element>): element is ReadonlyNode<HTMLTextAreaElement>; 274 + function isTextArea(element: Element | ReadonlyNode<Element>): boolean { 210 275 return element.localName === "textarea"; 211 276 } 212 277 213 278 function isHead(element: Element): element is HTMLHeadElement; 214 - function isHead(element: ReadOnlyNode<Element>): element is ReadOnlyNode<HTMLHeadElement>; 215 - function isHead(element: Element | ReadOnlyNode<Element>): boolean { 279 + function isHead(element: ReadonlyNode<Element>): element is ReadonlyNode<HTMLHeadElement>; 280 + function isHead(element: Element | ReadonlyNode<Element>): boolean { 216 281 return element.localName === "head"; 217 282 } 218 283 219 284 function isParentNode(node: Node): node is ParentNode; 220 - function isParentNode(node: ReadOnlyNode<Node>): node is ReadOnlyNode<ParentNode>; 221 - function isParentNode(node: Node | ReadOnlyNode<Node>): boolean { 285 + function isParentNode(node: ReadonlyNode<Node>): node is ReadonlyNode<ParentNode>; 286 + function isParentNode(node: Node | ReadonlyNode<Node>): boolean { 222 287 return node.nodeType === 1 || node.nodeType === 9 || node.nodeType === 11; 223 288 }