Precise DOM morphing
morphing typescript dom

Move options to `this`

+103 -63
+2 -1
dist/morphlex.d.ts
··· 1 - export interface Options { 1 + interface Options { 2 2 ignoreActiveValue?: boolean; 3 3 preserveModifiedValues?: boolean; 4 4 beforeNodeMorphed?: (node: Node, referenceNode: Node) => boolean; ··· 13 13 afterPropertyUpdated?: (node: Node, propertyName: PropertyKey, previousValue: unknown) => void; 14 14 } 15 15 export declare function morph(node: ChildNode, reference: ChildNode | string, options?: Options): void; 16 + export {};
+49 -28
dist/morphlex.js
··· 16 16 } 17 17 } 18 18 class Morph { 19 - #options; 20 19 #idMap; 21 20 #sensivityMap; 21 + #ignoreActiveValue; 22 + #preserveModifiedValues; 23 + #beforeNodeMorphed; 24 + #afterNodeMorphed; 25 + #beforeNodeAdded; 26 + #afterNodeAdded; 27 + #beforeNodeRemoved; 28 + #afterNodeRemoved; 29 + #beforeAttributeUpdated; 30 + #afterAttributeUpdated; 31 + #beforePropertyUpdated; 32 + #afterPropertyUpdated; 22 33 constructor(options = {}) { 23 - this.#options = options; 24 34 this.#idMap = new WeakMap(); 25 35 this.#sensivityMap = new WeakMap(); 26 - Object.freeze(this.#options); 36 + this.#ignoreActiveValue = options.ignoreActiveValue || false; 37 + this.#preserveModifiedValues = options.preserveModifiedValues || false; 38 + this.#beforeNodeMorphed = options.beforeNodeMorphed; 39 + this.#afterNodeMorphed = options.afterNodeMorphed; 40 + this.#beforeNodeAdded = options.beforeNodeAdded; 41 + this.#afterNodeAdded = options.afterNodeAdded; 42 + this.#beforeNodeRemoved = options.beforeNodeRemoved; 43 + this.#afterNodeRemoved = options.afterNodeRemoved; 44 + this.#beforeAttributeUpdated = options.beforeAttributeUpdated; 45 + this.#afterAttributeUpdated = options.afterAttributeUpdated; 46 + this.#beforePropertyUpdated = options.beforePropertyUpdated; 47 + this.#afterPropertyUpdated = options.afterPropertyUpdated; 27 48 Object.freeze(this); 28 49 } 29 50 morph(node, reference) { ··· 86 107 } 87 108 } 88 109 #morphMatchingElementNode(node, reference) { 89 - if (!(this.#options.beforeNodeMorphed?.(node, reference) ?? true)) return; 110 + if (!(this.#beforeNodeMorphed?.(node, reference) ?? true)) return; 90 111 if (node.hasAttributes() || reference.hasAttributes()) this.#morphAttributes(node, reference); 91 112 if (isHead(node)) { 92 113 this.#morphHead(node, reference); 93 114 } else if (node.hasChildNodes() || reference.hasChildNodes()) this.#morphChildNodes(node, reference); 94 - this.#options.afterNodeMorphed?.(node, reference); 115 + this.#afterNodeMorphed?.(node, reference); 95 116 } 96 117 #morphOtherNode(node, reference) { 97 - if (!(this.#options.beforeNodeMorphed?.(node, reference) ?? true)) return; 118 + if (!(this.#beforeNodeMorphed?.(node, reference) ?? true)) return; 98 119 if (node.nodeType === reference.nodeType && node.nodeValue !== null && reference.nodeValue !== null) { 99 120 // Handle text nodes, comments, and CDATA sections. 100 121 this.#updateProperty(node, "nodeValue", reference.nodeValue); 101 122 } else this.#replaceNode(node, reference.cloneNode(true)); 102 - this.#options.afterNodeMorphed?.(node, reference); 123 + this.#afterNodeMorphed?.(node, reference); 103 124 } 104 125 #morphHead(node, reference) { 105 126 const refChildNodesMap = new Map(); ··· 118 139 #morphAttributes(element, reference) { 119 140 // Remove any excess attributes from the element that aren’t present in the reference. 120 141 for (const { name, value } of element.attributes) { 121 - if (!reference.hasAttribute(name) && (this.#options.beforeAttributeUpdated?.(element, name, null) ?? true)) { 142 + if (!reference.hasAttribute(name) && (this.#beforeAttributeUpdated?.(element, name, null) ?? true)) { 122 143 element.removeAttribute(name); 123 - this.#options.afterAttributeUpdated?.(element, name, value); 144 + this.#afterAttributeUpdated?.(element, name, value); 124 145 } 125 146 } 126 147 // Copy attributes from the reference to the element, if they don’t already match. 127 148 for (const { name, value } of reference.attributes) { 128 149 const previousValue = element.getAttribute(name); 129 - if (previousValue !== value && (this.#options.beforeAttributeUpdated?.(element, name, value) ?? true)) { 150 + if (previousValue !== value && (this.#beforeAttributeUpdated?.(element, name, value) ?? true)) { 130 151 element.setAttribute(name, value); 131 - this.#options.afterAttributeUpdated?.(element, name, previousValue); 152 + this.#afterAttributeUpdated?.(element, name, previousValue); 132 153 } 133 154 } 134 155 // For certain types of elements, we need to do some extra work to ensure ··· 139 160 this.#updateProperty(element, "indeterminate", reference.indeterminate); 140 161 if ( 141 162 element.type !== "file" && 142 - !(this.#options.ignoreActiveValue && document.activeElement === element) && 143 - !(this.#options.preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue) 163 + !(this.#ignoreActiveValue && document.activeElement === element) && 164 + !(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue) 144 165 ) { 145 166 this.#updateProperty(element, "value", reference.value); 146 167 } ··· 149 170 } else if ( 150 171 isTextArea(element) && 151 172 isTextArea(reference) && 152 - !(this.#options.ignoreActiveValue && document.activeElement === element) && 153 - !(this.#options.preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue) 173 + !(this.#ignoreActiveValue && document.activeElement === element) && 174 + !(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue) 154 175 ) { 155 176 this.#updateProperty(element, "value", reference.value); 156 177 const text = element.firstElementChild; ··· 183 204 } 184 205 } 185 206 #morphChildElement(child, reference, parent) { 186 - if (!(this.#options.beforeNodeMorphed?.(child, reference) ?? true)) return; 207 + if (!(this.#beforeNodeMorphed?.(child, reference) ?? true)) return; 187 208 const refIdSet = this.#idMap.get(reference); 188 209 // Generate the array in advance of the loop 189 210 const refSetArray = refIdSet ? [...refIdSet] : []; ··· 216 237 this.#morphNode(nextMatchByTagName, reference); 217 238 } else { 218 239 const newNode = reference.cloneNode(true); 219 - if (this.#options.beforeNodeAdded?.(newNode) ?? true) { 240 + if (this.#beforeNodeAdded?.(newNode) ?? true) { 220 241 this.#insertBefore(parent, newNode, child); 221 - this.#options.afterNodeAdded?.(newNode); 242 + this.#afterNodeAdded?.(newNode); 222 243 } 223 244 } 224 - this.#options.afterNodeMorphed?.(child, reference); 245 + this.#afterNodeMorphed?.(child, reference); 225 246 } 226 247 #updateProperty(node, propertyName, newValue) { 227 248 const previousValue = node[propertyName]; 228 - if (previousValue !== newValue && (this.#options.beforePropertyUpdated?.(node, propertyName, newValue) ?? true)) { 249 + if (previousValue !== newValue && (this.#beforePropertyUpdated?.(node, propertyName, newValue) ?? true)) { 229 250 node[propertyName] = newValue; 230 - this.#options.afterPropertyUpdated?.(node, propertyName, previousValue); 251 + this.#afterPropertyUpdated?.(node, propertyName, previousValue); 231 252 } 232 253 } 233 254 #replaceNode(node, newNode) { 234 - if ((this.#options.beforeNodeRemoved?.(node) ?? true) && (this.#options.beforeNodeAdded?.(newNode) ?? true)) { 255 + if ((this.#beforeNodeRemoved?.(node) ?? true) && (this.#beforeNodeAdded?.(newNode) ?? true)) { 235 256 node.replaceWith(newNode); 236 - this.#options.afterNodeAdded?.(newNode); 237 - this.#options.afterNodeRemoved?.(node); 257 + this.#afterNodeAdded?.(newNode); 258 + this.#afterNodeRemoved?.(node); 238 259 } 239 260 } 240 261 #insertBefore(parent, node, insertionPoint) { ··· 256 277 parent.insertBefore(node, insertionPoint); 257 278 } 258 279 #appendChild(node, newNode) { 259 - if (this.#options.beforeNodeAdded?.(newNode) ?? true) { 280 + if (this.#beforeNodeAdded?.(newNode) ?? true) { 260 281 node.appendChild(newNode); 261 - this.#options.afterNodeAdded?.(newNode); 282 + this.#afterNodeAdded?.(newNode); 262 283 } 263 284 } 264 285 #removeNode(node) { 265 - if (this.#options.beforeNodeRemoved?.(node) ?? true) { 286 + if (this.#beforeNodeRemoved?.(node) ?? true) { 266 287 node.remove(); 267 - this.#options.afterNodeRemoved?.(node); 288 + this.#afterNodeRemoved?.(node); 268 289 } 269 290 } 270 291 }
+52 -34
src/morphlex.ts
··· 28 28 readonly length: NodeListOf<T>["length"]; 29 29 }; 30 30 31 - export interface Options { 31 + interface Options { 32 32 ignoreActiveValue?: boolean; 33 33 preserveModifiedValues?: boolean; 34 - 35 34 beforeNodeMorphed?: (node: Node, referenceNode: Node) => boolean; 36 35 afterNodeMorphed?: (node: Node, referenceNode: Node) => void; 37 - 38 36 beforeNodeAdded?: (node: Node) => boolean; 39 37 afterNodeAdded?: (node: Node) => void; 40 - 41 38 beforeNodeRemoved?: (node: Node) => boolean; 42 39 afterNodeRemoved?: (node: Node) => void; 43 - 44 40 beforeAttributeUpdated?: (element: Element, attributeName: string, newValue: string | null) => boolean; 45 41 afterAttributeUpdated?: (element: Element, attributeName: string, previousValue: string | null) => void; 46 - 47 42 beforePropertyUpdated?: (node: Node, propertyName: PropertyKey, newValue: unknown) => boolean; 48 43 afterPropertyUpdated?: (node: Node, propertyName: PropertyKey, previousValue: unknown) => void; 49 44 } ··· 68 63 } 69 64 70 65 class Morph { 71 - readonly #options: Options; 72 66 readonly #idMap: IdMap; 73 67 readonly #sensivityMap: SensivityMap; 74 68 69 + readonly #ignoreActiveValue: boolean; 70 + readonly #preserveModifiedValues: boolean; 71 + readonly #beforeNodeMorphed?: (node: Node, referenceNode: Node) => boolean; 72 + readonly #afterNodeMorphed?: (node: Node, referenceNode: Node) => void; 73 + readonly #beforeNodeAdded?: (node: Node) => boolean; 74 + readonly #afterNodeAdded?: (node: Node) => void; 75 + readonly #beforeNodeRemoved?: (node: Node) => boolean; 76 + readonly #afterNodeRemoved?: (node: Node) => void; 77 + readonly #beforeAttributeUpdated?: (element: Element, attributeName: string, newValue: string | null) => boolean; 78 + readonly #afterAttributeUpdated?: (element: Element, attributeName: string, previousValue: string | null) => void; 79 + readonly #beforePropertyUpdated?: (node: Node, propertyName: PropertyKey, newValue: unknown) => boolean; 80 + readonly #afterPropertyUpdated?: (node: Node, propertyName: PropertyKey, previousValue: unknown) => void; 81 + 75 82 constructor(options: Options = {}) { 76 - this.#options = options; 77 83 this.#idMap = new WeakMap(); 78 84 this.#sensivityMap = new WeakMap(); 79 85 80 - Object.freeze(this.#options); 86 + this.#ignoreActiveValue = options.ignoreActiveValue || false; 87 + this.#preserveModifiedValues = options.preserveModifiedValues || false; 88 + this.#beforeNodeMorphed = options.beforeNodeMorphed; 89 + this.#afterNodeMorphed = options.afterNodeMorphed; 90 + this.#beforeNodeAdded = options.beforeNodeAdded; 91 + this.#afterNodeAdded = options.afterNodeAdded; 92 + this.#beforeNodeRemoved = options.beforeNodeRemoved; 93 + this.#afterNodeRemoved = options.afterNodeRemoved; 94 + this.#beforeAttributeUpdated = options.beforeAttributeUpdated; 95 + this.#afterAttributeUpdated = options.afterAttributeUpdated; 96 + this.#beforePropertyUpdated = options.beforePropertyUpdated; 97 + this.#afterPropertyUpdated = options.afterPropertyUpdated; 98 + 81 99 Object.freeze(this); 82 100 } 83 101 ··· 155 173 } 156 174 157 175 #morphMatchingElementNode(node: Element, reference: ReadonlyNode<Element>): void { 158 - if (!(this.#options.beforeNodeMorphed?.(node, reference as ChildNode) ?? true)) return; 176 + if (!(this.#beforeNodeMorphed?.(node, reference as ChildNode) ?? true)) return; 159 177 160 178 if (node.hasAttributes() || reference.hasAttributes()) this.#morphAttributes(node, reference); 161 179 ··· 163 181 this.#morphHead(node, reference as ReadonlyNode<HTMLHeadElement>); 164 182 } else if (node.hasChildNodes() || reference.hasChildNodes()) this.#morphChildNodes(node, reference); 165 183 166 - this.#options.afterNodeMorphed?.(node, reference as ChildNode); 184 + this.#afterNodeMorphed?.(node, reference as ChildNode); 167 185 } 168 186 169 187 #morphOtherNode(node: ChildNode, reference: ReadonlyNode<ChildNode>): void { 170 - if (!(this.#options.beforeNodeMorphed?.(node, reference as ChildNode) ?? true)) return; 188 + if (!(this.#beforeNodeMorphed?.(node, reference as ChildNode) ?? true)) return; 171 189 172 190 if (node.nodeType === reference.nodeType && node.nodeValue !== null && reference.nodeValue !== null) { 173 191 // Handle text nodes, comments, and CDATA sections. 174 192 this.#updateProperty(node, "nodeValue", reference.nodeValue); 175 193 } else this.#replaceNode(node, reference.cloneNode(true)); 176 194 177 - this.#options.afterNodeMorphed?.(node, reference as ChildNode); 195 + this.#afterNodeMorphed?.(node, reference as ChildNode); 178 196 } 179 197 180 198 #morphHead(node: HTMLHeadElement, reference: ReadonlyNode<HTMLHeadElement>): void { ··· 199 217 #morphAttributes(element: Element, reference: ReadonlyNode<Element>): void { 200 218 // Remove any excess attributes from the element that aren’t present in the reference. 201 219 for (const { name, value } of element.attributes) { 202 - if (!reference.hasAttribute(name) && (this.#options.beforeAttributeUpdated?.(element, name, null) ?? true)) { 220 + if (!reference.hasAttribute(name) && (this.#beforeAttributeUpdated?.(element, name, null) ?? true)) { 203 221 element.removeAttribute(name); 204 - this.#options.afterAttributeUpdated?.(element, name, value); 222 + this.#afterAttributeUpdated?.(element, name, value); 205 223 } 206 224 } 207 225 208 226 // Copy attributes from the reference to the element, if they don’t already match. 209 227 for (const { name, value } of reference.attributes) { 210 228 const previousValue = element.getAttribute(name); 211 - if (previousValue !== value && (this.#options.beforeAttributeUpdated?.(element, name, value) ?? true)) { 229 + if (previousValue !== value && (this.#beforeAttributeUpdated?.(element, name, value) ?? true)) { 212 230 element.setAttribute(name, value); 213 - this.#options.afterAttributeUpdated?.(element, name, previousValue); 231 + this.#afterAttributeUpdated?.(element, name, previousValue); 214 232 } 215 233 } 216 234 ··· 222 240 this.#updateProperty(element, "indeterminate", reference.indeterminate); 223 241 if ( 224 242 element.type !== "file" && 225 - !(this.#options.ignoreActiveValue && document.activeElement === element) && 226 - !(this.#options.preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue) 243 + !(this.#ignoreActiveValue && document.activeElement === element) && 244 + !(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue) 227 245 ) { 228 246 this.#updateProperty(element, "value", reference.value); 229 247 } ··· 232 250 } else if ( 233 251 isTextArea(element) && 234 252 isTextArea(reference) && 235 - !(this.#options.ignoreActiveValue && document.activeElement === element) && 236 - !(this.#options.preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue) 253 + !(this.#ignoreActiveValue && document.activeElement === element) && 254 + !(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue) 237 255 ) { 238 256 this.#updateProperty(element, "value", reference.value); 239 257 ··· 272 290 } 273 291 274 292 #morphChildElement(child: Element, reference: ReadonlyNode<Element>, parent: Element): void { 275 - if (!(this.#options.beforeNodeMorphed?.(child, reference as ChildNode) ?? true)) return; 293 + if (!(this.#beforeNodeMorphed?.(child, reference as ChildNode) ?? true)) return; 276 294 277 295 const refIdSet = this.#idMap.get(reference); 278 296 ··· 314 332 this.#morphNode(nextMatchByTagName, reference); 315 333 } else { 316 334 const newNode = reference.cloneNode(true); 317 - if (this.#options.beforeNodeAdded?.(newNode) ?? true) { 335 + if (this.#beforeNodeAdded?.(newNode) ?? true) { 318 336 this.#insertBefore(parent, newNode, child); 319 - this.#options.afterNodeAdded?.(newNode); 337 + this.#afterNodeAdded?.(newNode); 320 338 } 321 339 } 322 340 323 - this.#options.afterNodeMorphed?.(child, reference as ChildNode); 341 + this.#afterNodeMorphed?.(child, reference as ChildNode); 324 342 } 325 343 326 344 #updateProperty<N extends Node, P extends keyof N>(node: N, propertyName: P, newValue: N[P]): void { 327 345 const previousValue = node[propertyName]; 328 346 329 - if (previousValue !== newValue && (this.#options.beforePropertyUpdated?.(node, propertyName, newValue) ?? true)) { 347 + if (previousValue !== newValue && (this.#beforePropertyUpdated?.(node, propertyName, newValue) ?? true)) { 330 348 node[propertyName] = newValue; 331 - this.#options.afterPropertyUpdated?.(node, propertyName, previousValue); 349 + this.#afterPropertyUpdated?.(node, propertyName, previousValue); 332 350 } 333 351 } 334 352 335 353 #replaceNode(node: ChildNode, newNode: Node): void { 336 - if ((this.#options.beforeNodeRemoved?.(node) ?? true) && (this.#options.beforeNodeAdded?.(newNode) ?? true)) { 354 + if ((this.#beforeNodeRemoved?.(node) ?? true) && (this.#beforeNodeAdded?.(newNode) ?? true)) { 337 355 node.replaceWith(newNode); 338 - this.#options.afterNodeAdded?.(newNode); 339 - this.#options.afterNodeRemoved?.(node); 356 + this.#afterNodeAdded?.(newNode); 357 + this.#afterNodeRemoved?.(node); 340 358 } 341 359 } 342 360 ··· 366 384 } 367 385 368 386 #appendChild(node: ParentNode, newNode: Node): void { 369 - if (this.#options.beforeNodeAdded?.(newNode) ?? true) { 387 + if (this.#beforeNodeAdded?.(newNode) ?? true) { 370 388 node.appendChild(newNode); 371 - this.#options.afterNodeAdded?.(newNode); 389 + this.#afterNodeAdded?.(newNode); 372 390 } 373 391 } 374 392 375 393 #removeNode(node: ChildNode): void { 376 - if (this.#options.beforeNodeRemoved?.(node) ?? true) { 394 + if (this.#beforeNodeRemoved?.(node) ?? true) { 377 395 node.remove(); 378 - this.#options.afterNodeRemoved?.(node); 396 + this.#afterNodeRemoved?.(node); 379 397 } 380 398 } 381 399 }