diff --git a/render/hyperscript.js b/render/hyperscript.js index 8b34233e9..2f7edf6db 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -62,6 +62,9 @@ function execSelector(state, vnode) { attrs = Object.assign({type: attrs.type}, attrs) } + // This reduces the complexity of the evaluation of "is" within the render function. + vnode.is = attrs.is + vnode.attrs = attrs return vnode diff --git a/render/render.js b/render/render.js index 773a3edb9..52b7132a5 100644 --- a/render/render.js +++ b/render/render.js @@ -114,7 +114,7 @@ module.exports = function() { function createElement(parent, vnode, hooks, ns, nextSibling) { var tag = vnode.tag var attrs = vnode.attrs - var is = attrs && attrs.is + var is = vnode.is ns = getNameSpace(vnode) || ns @@ -396,7 +396,7 @@ module.exports = function() { } function updateNode(parent, old, vnode, hooks, nextSibling, ns) { var oldTag = old.tag, tag = vnode.tag - if (oldTag === tag) { + if (oldTag === tag && old.is === vnode.is) { vnode.state = old.state vnode.events = old.events if (shouldNotUpdate(vnode, old)) return @@ -643,7 +643,7 @@ module.exports = function() { } } function setAttr(vnode, key, old, value, ns) { - if (key === "key" || key === "is" || value == null || isLifecycleMethod(key) || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object") return + if (key === "key" || value == null || isLifecycleMethod(key) || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object") return if (key[0] === "o" && key[1] === "n") return updateEvent(vnode, key, value) if (key.slice(0, 6) === "xlink:") vnode.dom.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(6), value) else if (key === "style") updateStyle(vnode.dom, old, value) @@ -676,7 +676,7 @@ module.exports = function() { } } function removeAttr(vnode, key, old, ns) { - if (key === "key" || key === "is" || old == null || isLifecycleMethod(key)) return + if (key === "key" || old == null || isLifecycleMethod(key)) return if (key[0] === "o" && key[1] === "n") updateEvent(vnode, key, undefined) else if (key === "style") updateStyle(vnode.dom, old, null) else if ( @@ -710,22 +710,24 @@ module.exports = function() { if ("selectedIndex" in attrs) setAttr(vnode, "selectedIndex", null, attrs.selectedIndex, undefined) } function updateAttrs(vnode, old, attrs, ns) { - if (old && old === attrs) { - console.warn("Don't reuse attrs object, use new object for every redraw, this will throw in next major") - } - if (attrs != null) { - for (var key in attrs) { - setAttr(vnode, key, old && old[key], attrs[key], ns) - } - } + // Some attributes may NOT be case-sensitive (e.g. data-***), + // so removal should be done first to prevent accidental removal for newly setting values. var val if (old != null) { + if (old === attrs) { + console.warn("Don't reuse attrs object, use new object for every redraw, this will throw in next major") + } for (var key in old) { if (((val = old[key]) != null) && (attrs == null || attrs[key] == null)) { removeAttr(vnode, key, val, ns) } } } + if (attrs != null) { + for (var key in attrs) { + setAttr(vnode, key, old && old[key], attrs[key], ns) + } + } } function isFormAttribute(vnode, attr) { return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === activeElement(vnode.dom) || vnode.tag === "option" && vnode.dom.parentNode === activeElement(vnode.dom) @@ -737,7 +739,7 @@ module.exports = function() { // Filter out namespaced keys return ns === undefined && ( // If it's a custom element, just keep it. - vnode.tag.indexOf("-") > -1 || vnode.attrs != null && vnode.attrs.is || + vnode.tag.indexOf("-") > -1 || vnode.is || // If it's a normal element, let's try to avoid a few browser bugs. key !== "href" && key !== "list" && key !== "form" && key !== "width" && key !== "height"// && key !== "type" // Defer the property check until *after* we check everything. @@ -756,7 +758,7 @@ module.exports = function() { element.style = style } else if (old == null || typeof old !== "object") { // `old` is missing or a string, `style` is an object. - element.style.cssText = "" + element.style = "" // Add new style properties for (var key in style) { var value = style[key] @@ -767,6 +769,15 @@ module.exports = function() { } } else { // Both old & new are (different) objects. + // Remove style properties that no longer exist + // Style properties may have two cases(dash-case and camelCase), + // so removal should be done first to prevent accidental removal for newly setting values. + for (var key in old) { + if (old[key] != null && style[key] == null) { + if (key.includes("-")) element.style.removeProperty(key) + else element.style[key] = "" + } + } // Update style properties that have changed for (var key in style) { var value = style[key] @@ -775,13 +786,6 @@ module.exports = function() { else element.style[key] = value } } - // Remove style properties that no longer exist - for (var key in old) { - if (old[key] != null && style[key] == null) { - if (key.includes("-")) element.style.removeProperty(key) - else element.style[key] = "" - } - } } } diff --git a/render/tests/manual/case-handling.html b/render/tests/manual/case-handling.html new file mode 100644 index 000000000..372f8bc60 --- /dev/null +++ b/render/tests/manual/case-handling.html @@ -0,0 +1,40 @@ + + + + + + +

This is a test for special case-handling of attribute and style properties. (#2988).

+

Open your browser's Developer Console and follow these steps:

+
    +
  1. Check the background color of the "foo" below.
  2. + +
  3. Check the logs displayed in the console.
  4. + +
+ +
+ + + + diff --git a/render/tests/test-attributes.js b/render/tests/test-attributes.js index 1daed5e5f..3d469c9d8 100644 --- a/render/tests/test-attributes.js +++ b/render/tests/test-attributes.js @@ -80,8 +80,8 @@ o.spec("attributes", function() { o(spies[0].callCount).equals(0) o(spies[2].callCount).equals(0) o(spies[3].calls).deepEquals([{this: spies[3].elem, args: ["custom", "x"]}]) - o(spies[4].calls).deepEquals([{this: spies[4].elem, args: ["custom", "x"]}]) - o(spies[5].calls).deepEquals([{this: spies[5].elem, args: ["custom", "x"]}]) + o(spies[4].calls).deepEquals([{this: spies[4].elem, args: ["is", "something-special"]}, {this: spies[4].elem, args: ["custom", "x"]}]) + o(spies[5].calls).deepEquals([{this: spies[5].elem, args: ["is", "something-special"]}, {this: spies[5].elem, args: ["custom", "x"]}]) }) o("when vnode is customElement with property, custom setAttribute not called", function(){ @@ -124,8 +124,8 @@ o.spec("attributes", function() { o(spies[1].callCount).equals(0) o(spies[2].callCount).equals(0) o(spies[3].callCount).equals(0) - o(spies[4].callCount).equals(0) - o(spies[5].callCount).equals(0) + o(spies[4].callCount).equals(1) // setAttribute("is", "something-special") is called + o(spies[5].callCount).equals(1) // setAttribute("is", "something-special") is called o(getters[0].callCount).equals(0) o(getters[1].callCount).equals(0) o(getters[2].callCount).equals(0) diff --git a/render/tests/test-updateElement.js b/render/tests/test-updateElement.js index 771b4a405..2cc689321 100644 --- a/render/tests/test-updateElement.js +++ b/render/tests/test-updateElement.js @@ -388,4 +388,178 @@ o.spec("updateElement", function() { o(root.childNodes.length).equals(3) o(x).notEquals(y) // this used to be a recycling pool test }) + o.spec("element node with `is` attribute", function() { + o("recreate element node with `is` attribute (set `is`)", function() { + var vnode = m("a") + var updated = m("a", {is: "bar"}) + + render(root, vnode) + render(root, updated) + + o(vnode.dom).notEquals(root.firstChild) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("A") + o(updated.dom.getAttribute("is")).equals("bar") + }) + o("recreate element node without `is` attribute (remove `is`)", function() { + var vnode = m("a", {is: "foo"}) + var updated = m("a") + + render(root, vnode) + render(root, updated) + + o(vnode.dom).notEquals(root.firstChild) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("A") + o(updated.dom.getAttribute("is")).equals(null) + }) + o("recreate element node with `is` attribute (same tag, different `is`)", function() { + var vnode = m("a", {is: "foo"}) + var updated = m("a", {is: "bar"}) + + render(root, vnode) + render(root, updated) + + o(vnode.dom).notEquals(root.firstChild) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("A") + o(updated.dom.getAttribute("is")).equals("bar") + }) + o("recreate element node with `is` attribute (different tag, same `is`)", function() { + var vnode = m("a", {is: "foo"}) + var updated = m("b", {is: "foo"}) + + render(root, vnode) + render(root, updated) + + o(vnode.dom).notEquals(root.firstChild) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("B") + o(updated.dom.getAttribute("is")).equals("foo") + }) + o("recreate element node with `is` attribute (different tag, different `is`)", function() { + var vnode = m("a", {is: "foo"}) + var updated = m("b", {is: "bar"}) + + render(root, vnode) + render(root, updated) + + o(vnode.dom).notEquals(root.firstChild) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("B") + o(updated.dom.getAttribute("is")).equals("bar") + }) + o("keep element node with `is` attribute (same tag, same `is`)", function() { + var vnode = m("a", {is: "foo"}) + var updated = m("a", {is: "foo"}, "x") + + render(root, vnode) + render(root, updated) + + o(vnode.dom).equals(root.firstChild) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("A") + o(updated.dom.getAttribute("is")).equals("foo") + o(updated.dom.firstChild.nodeValue).equals("x") + }) + o("recreate element node with `is` attribute (set `is`, CSS selector)", function() { + var vnode = m("a") + var updated = m("a[is=bar]") + + render(root, vnode) + render(root, updated) + + o(vnode.dom).notEquals(root.firstChild) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("A") + o(updated.dom.getAttribute("is")).equals("bar") + }) + o("recreate element node without `is` attribute (remove `is`, CSS selector)", function() { + var vnode = m("a[is=foo]") + var updated = m("a") + + render(root, vnode) + render(root, updated) + + o(vnode.dom).notEquals(root.firstChild) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("A") + o(updated.dom.getAttribute("is")).equals(null) + }) + o("recreate element node with `is` attribute (same tag, different `is`, CSS selector)", function() { + var vnode = m("a[is=foo]") + var updated = m("a[is=bar]") + + render(root, vnode) + render(root, updated) + + o(vnode.dom).notEquals(root.firstChild) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("A") + o(updated.dom.getAttribute("is")).equals("bar") + }) + o("recreate element node with `is` attribute (different tag, same `is`, CSS selector)", function() { + var vnode = m("a[is=foo]") + var updated = m("b[is=foo]") + + render(root, vnode) + render(root, updated) + + o(vnode.dom).notEquals(root.firstChild) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("B") + o(updated.dom.getAttribute("is")).equals("foo") + }) + o("recreate element node with `is` attribute (different tag, different `is`, CSS selector)", function() { + var vnode = m("a[is=foo]") + var updated = m("b[is=bar]") + + render(root, vnode) + render(root, updated) + + o(vnode.dom).notEquals(root.firstChild) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("B") + o(updated.dom.getAttribute("is")).equals("bar") + }) + o("keep element node with `is` attribute (same tag, same `is`, CSS selector)", function() { + var vnode = m("a[is=foo]") + var updated = m("a[is=foo]", "x") + + render(root, vnode) + render(root, updated) + + o(vnode.dom).equals(root.firstChild) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("A") + o(updated.dom.getAttribute("is")).equals("foo") + o(updated.dom.firstChild.nodeValue).equals("x") + }) + o("keep element node with `is` attribute (same tag, same `is`, from attrs to CSS selector)", function() { + var vnode = m("a", {is: "foo"}) + var updated = m("a[is=foo]", "x") + + render(root, vnode) + render(root, updated) + + o(vnode.dom).equals(root.firstChild) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("A") + o(updated.dom.getAttribute("is")).equals("foo") + o(updated.dom.firstChild.nodeValue).equals("x") + }) + o("keep element node with `is` attribute (same tag, same `is`, from CSS selector to attrs)", function() { + var vnode = m("a[is=foo]") + var updated = m("a", {is: "foo"}, "x") + + render(root, vnode) + render(root, updated) + + o(vnode.dom).equals(root.firstChild) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("A") + o(updated.dom.getAttribute("is")).equals("foo") + o(updated.dom.firstChild.nodeValue).equals("x") + }) + }) }) diff --git a/render/vnode.js b/render/vnode.js index ec19b174f..215980f21 100644 --- a/render/vnode.js +++ b/render/vnode.js @@ -1,7 +1,7 @@ "use strict" function Vnode(tag, key, attrs, children, text, dom) { - return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: undefined, events: undefined, instance: undefined} + return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, is: undefined, domSize: undefined, state: undefined, events: undefined, instance: undefined} } Vnode.normalize = function(node) { if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined)