Precise DOM morphing
morphing typescript dom

Better handling of value attributes

+68 -31
+68 -31
src/morphlex.ts
··· 1 const ParentNodeTypes = new Set([1, 9, 11]) 2 3 type IdSet = Set<string> 4 type IdMap = WeakMap<Node, IdSet> ··· 8 9 type PairOfNodes<N extends Node> = [N, N] 10 type PairOfMatchingElements<E extends Element> = Branded<PairOfNodes<E>, "MatchingElementPair"> 11 12 interface Options { 13 beforeNodeVisited?: (fromNode: Node, toNode: Node) => boolean ··· 148 } 149 150 private morphOtherNode([from, to]: PairOfNodes<ChildNode>): void { 151 - // TODO: Improve this logic 152 - // Handle text nodes, comments, and CDATA sections. 153 if (from.nodeType === to.nodeType && from.nodeValue !== null && to.nodeValue !== null) { 154 from.nodeValue = to.nodeValue 155 } else { ··· 158 } 159 160 private visitAttributes([from, to]: PairOfMatchingElements<Element>): void { 161 - const isInput = isInputElement(from) && isInputElement(to) 162 - const isOption = isOptionElement(from) && isOptionElement(to) 163 - 164 const toAttrs = to.attributes 165 const fromAttrs = from.attributes 166 ··· 171 const value = attr.value 172 const oldValue = from.getAttribute(name) 173 174 - if (isInput) { 175 - if (name === "value" || name === "checked" || name === "indeterminate") { 176 - continue 177 - } else if (name === "morph-value" && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 178 - from.setAttribute(name, value) 179 - from.value = value 180 - this.options.afterAttributeUpdated?.(from, name, oldValue) 181 - continue 182 - } else if (name === "morph-checked" && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 183 - from.setAttribute(name, value) 184 - from.checked = value === "true" 185 - this.options.afterAttributeUpdated?.(from, name, oldValue) 186 - continue 187 - } else if (name === "morph-indeterminate" && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 188 - from.setAttribute(name, value) 189 - from.indeterminate = value === "true" 190 - this.options.afterAttributeUpdated?.(from, name, oldValue) 191 - continue 192 } 193 - } else if (isOption) { 194 - if (name === "selected") { 195 - continue 196 - } else if (name === "morph-selected") { 197 - from.setAttribute(name, value) 198 - from.selected = value === "true" 199 - this.options.afterAttributeUpdated?.(from, name, oldValue) 200 } 201 } 202 203 if (oldValue !== value && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 204 from.setAttribute(name, value) 205 206 - if (isInput && name === "disabled" && from.disabled !== to.disabled) { 207 from.disabled = to.disabled 208 } 209 ··· 421 } 422 } 423 } 424 } 425 426 function isMatchingElementPair(pair: PairOfNodes<Element>): pair is PairOfMatchingElements<Element> {
··· 1 const ParentNodeTypes = new Set([1, 9, 11]) 2 + const DisablableElements = new Set(["input", "button", "select", "textarea", "option", "optgroup", "fieldset"]) 3 + const ValuableElements = new Set(["input", "select", "textarea"]) 4 + const ValueAttributes = new Set([ 5 + "value", 6 + "selected", 7 + "checked", 8 + "indeterminate", 9 + "morph-value", 10 + "morph-selected", 11 + "morph-checked", 12 + "morph-indeterminate", 13 + ]) 14 15 type IdSet = Set<string> 16 type IdMap = WeakMap<Node, IdSet> ··· 20 21 type PairOfNodes<N extends Node> = [N, N] 22 type PairOfMatchingElements<E extends Element> = Branded<PairOfNodes<E>, "MatchingElementPair"> 23 + 24 + type DisablableElement = 25 + | HTMLInputElement 26 + | HTMLButtonElement 27 + | HTMLSelectElement 28 + | HTMLTextAreaElement 29 + | HTMLOptionElement 30 + | HTMLOptGroupElement 31 + | HTMLFieldSetElement 32 + 33 + type ValuableElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement 34 35 interface Options { 36 beforeNodeVisited?: (fromNode: Node, toNode: Node) => boolean ··· 171 } 172 173 private morphOtherNode([from, to]: PairOfNodes<ChildNode>): void { 174 if (from.nodeType === to.nodeType && from.nodeValue !== null && to.nodeValue !== null) { 175 from.nodeValue = to.nodeValue 176 } else { ··· 179 } 180 181 private visitAttributes([from, to]: PairOfMatchingElements<Element>): void { 182 const toAttrs = to.attributes 183 const fromAttrs = from.attributes 184 ··· 189 const value = attr.value 190 const oldValue = from.getAttribute(name) 191 192 + if (ValueAttributes.has(name)) { 193 + if (isValuableElement(from) && isValuableElement(to)) { 194 + if (name === "value") { 195 + continue 196 + } else if (name === "morph-value" && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 197 + from.setAttribute(name, value) 198 + from.value = value 199 + this.options.afterAttributeUpdated?.(from, name, oldValue) 200 + continue 201 + } 202 + } 203 + 204 + if (isInputElement(from) && isInputElement(to)) { 205 + if (name === "checked" || name === "indeterminate") { 206 + continue 207 + } else if (name === "morph-checked" && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 208 + from.setAttribute(name, value) 209 + from.checked = value === "true" 210 + this.options.afterAttributeUpdated?.(from, name, oldValue) 211 + continue 212 + } else if (name === "morph-indeterminate" && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 213 + from.setAttribute(name, value) 214 + from.indeterminate = value === "true" 215 + this.options.afterAttributeUpdated?.(from, name, oldValue) 216 + continue 217 + } 218 } 219 + 220 + if (isOptionElement(from) && isOptionElement(to)) { 221 + if (name === "selected") { 222 + continue 223 + } else if (name === "morph-selected") { 224 + from.setAttribute(name, value) 225 + from.selected = value === "true" 226 + this.options.afterAttributeUpdated?.(from, name, oldValue) 227 + continue 228 + } 229 } 230 } 231 232 if (oldValue !== value && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 233 from.setAttribute(name, value) 234 235 + if (name === "disabled" && isDisablableElement(from) && isDisablableElement(to) && from.disabled !== to.disabled) { 236 from.disabled = to.disabled 237 } 238 ··· 450 } 451 } 452 } 453 + } 454 + 455 + function isDisablableElement(element: Element): element is DisablableElement { 456 + return DisablableElements.has(element.localName) 457 + } 458 + 459 + function isValuableElement(element: Element): element is ValuableElement { 460 + return ValuableElements.has(element.localName) 461 } 462 463 function isMatchingElementPair(pair: PairOfNodes<Element>): pair is PairOfMatchingElements<Element> {