Precise DOM morphing
morphing typescript dom

Better handling of value attributes

+68 -31
+68 -31
src/morphlex.ts
··· 1 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 + ]) 2 14 3 15 type IdSet = Set<string> 4 16 type IdMap = WeakMap<Node, IdSet> ··· 8 20 9 21 type PairOfNodes<N extends Node> = [N, N] 10 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 11 34 12 35 interface Options { 13 36 beforeNodeVisited?: (fromNode: Node, toNode: Node) => boolean ··· 148 171 } 149 172 150 173 private morphOtherNode([from, to]: PairOfNodes<ChildNode>): void { 151 - // TODO: Improve this logic 152 - // Handle text nodes, comments, and CDATA sections. 153 174 if (from.nodeType === to.nodeType && from.nodeValue !== null && to.nodeValue !== null) { 154 175 from.nodeValue = to.nodeValue 155 176 } else { ··· 158 179 } 159 180 160 181 private visitAttributes([from, to]: PairOfMatchingElements<Element>): void { 161 - const isInput = isInputElement(from) && isInputElement(to) 162 - const isOption = isOptionElement(from) && isOptionElement(to) 163 - 164 182 const toAttrs = to.attributes 165 183 const fromAttrs = from.attributes 166 184 ··· 171 189 const value = attr.value 172 190 const oldValue = from.getAttribute(name) 173 191 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 + 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 + } 192 218 } 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) 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 + } 200 229 } 201 230 } 202 231 203 232 if (oldValue !== value && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 204 233 from.setAttribute(name, value) 205 234 206 - if (isInput && name === "disabled" && from.disabled !== to.disabled) { 235 + if (name === "disabled" && isDisablableElement(from) && isDisablableElement(to) && from.disabled !== to.disabled) { 207 236 from.disabled = to.disabled 208 237 } 209 238 ··· 421 450 } 422 451 } 423 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) 424 461 } 425 462 426 463 function isMatchingElementPair(pair: PairOfNodes<Element>): pair is PairOfMatchingElements<Element> {