From ab2e603e43d8c31df071adf0038df44ec70321a9 Mon Sep 17 00:00:00 2001 From: James Forbes Date: Sun, 9 Feb 2025 10:36:24 +1100 Subject: [PATCH] Release - v2.2.14 (#3006) --- render/domFor.js | 4 +- render/hyperscript.js | 3 + render/render.js | 131 ++-- render/tests/manual/case-handling.html | 40 ++ render/tests/test-attributes.js | 8 +- render/tests/test-domFor.js | 812 +++++++++++++++++++++++++ render/tests/test-onbeforeremove.js | 17 + render/tests/test-onremove.js | 167 ++--- render/tests/test-updateElement.js | 174 ++++++ render/vnode.js | 2 +- 10 files changed, 1194 insertions(+), 164 deletions(-) create mode 100644 render/tests/manual/case-handling.html diff --git a/render/domFor.js b/render/domFor.js index 16b17a972..89a5dd966 100644 --- a/render/domFor.js +++ b/render/domFor.js @@ -2,12 +2,12 @@ var delayedRemoval = new WeakMap -function *domFor(vnode, object = {}) { +function *domFor(vnode) { // To avoid unintended mangling of the internal bundler, // parameter destructuring is not used here. var dom = vnode.dom var domSize = vnode.domSize - var generation = object.generation + var generation = delayedRemoval.get(dom) if (dom != null) do { var nextSibling = dom.nextSibling 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 674a8266a..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 @@ -426,7 +426,7 @@ module.exports = function() { } function updateHTML(parent, old, vnode, ns, nextSibling) { if (old.children !== vnode.children) { - removeDOM(parent, old, undefined) + removeDOM(parent, old) createHTML(parent, vnode, ns, nextSibling) } else { @@ -585,71 +585,38 @@ module.exports = function() { if (vnode != null) removeNode(parent, vnode) } } - function removeNode(parent, vnode) { - var mask = 0 + function tryBlockRemove(parent, vnode, source, counter) { var original = vnode.state - var stateResult, attrsResult - if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") { - var result = callHook.call(vnode.state.onbeforeremove, vnode) - if (result != null && typeof result.then === "function") { - mask = 1 - stateResult = result - } - } - if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { - var result = callHook.call(vnode.attrs.onbeforeremove, vnode) - if (result != null && typeof result.then === "function") { - // eslint-disable-next-line no-bitwise - mask |= 2 - attrsResult = result - } - } - checkState(vnode, original) - var generation - // If we can, try to fast-path it and avoid all the overhead of awaiting - if (!mask) { + var result = callHook.call(source.onbeforeremove, vnode) + if (result == null) return + + var generation = currentRender + for (var dom of domFor(vnode)) delayedRemoval.set(dom, generation) + counter.v++ + + Promise.resolve(result).finally(function () { + checkState(vnode, original) + tryResumeRemove(parent, vnode, counter) + }) + } + function tryResumeRemove(parent, vnode, counter) { + if (--counter.v === 0) { onremove(vnode) - removeDOM(parent, vnode, generation) - } else { - generation = currentRender - for (var dom of domFor(vnode)) delayedRemoval.set(dom, generation) - if (stateResult != null) { - stateResult.finally(function () { - // eslint-disable-next-line no-bitwise - if (mask & 1) { - // eslint-disable-next-line no-bitwise - mask &= 2 - if (!mask) { - checkState(vnode, original) - onremove(vnode) - removeDOM(parent, vnode, generation) - } - } - }) - } - if (attrsResult != null) { - attrsResult.finally(function () { - // eslint-disable-next-line no-bitwise - if (mask & 2) { - // eslint-disable-next-line no-bitwise - mask &= 1 - if (!mask) { - checkState(vnode, original) - onremove(vnode) - removeDOM(parent, vnode, generation) - } - } - }) - } + removeDOM(parent, vnode) } } - function removeDOM(parent, vnode, generation) { + function removeNode(parent, vnode) { + var counter = {v: 1} + if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") tryBlockRemove(parent, vnode, vnode.state, counter) + if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") tryBlockRemove(parent, vnode, vnode.attrs, counter) + tryResumeRemove(parent, vnode, counter) + } + function removeDOM(parent, vnode) { if (vnode.dom == null) return if (vnode.domSize == null) { - // don't allocate for the common case - if (delayedRemoval.get(vnode.dom) === generation) parent.removeChild(vnode.dom) + parent.removeChild(vnode.dom) } else { - for (var dom of domFor(vnode, {generation})) parent.removeChild(dom) + for (var dom of domFor(vnode)) parent.removeChild(dom) } } @@ -676,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) @@ -709,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 ( @@ -743,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) @@ -770,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. @@ -789,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] @@ -800,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] @@ -808,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-domFor.js b/render/tests/test-domFor.js index b0c3444fa..dae83bd13 100644 --- a/render/tests/test-domFor.js +++ b/render/tests/test-domFor.js @@ -1,6 +1,7 @@ "use strict" const o = require("ospec") +const callAsync = require("../../test-utils/callAsync") const components = require("../../test-utils/components") const domMock = require("../../test-utils/domMock") const vdom = require("../render") @@ -85,6 +86,790 @@ o.spec("domFor(vnode)", function() { )) }) + o("works in onbeforeremove and onremove", function (done) { + const onbeforeremove = o.spy(function onbeforeremove(vnode){ + o(root.childNodes.length).equals(1) + o(root.childNodes[0].nodeName).equals("A") + const iter = domFor(vnode) + o(iter.next()).deepEquals({done:false, value: root.childNodes[0]}) + o(iter.next().done).deepEquals(true) + o(root.childNodes.length).equals(1) + return {then(resolve){resolve()}} + }) + const onremove = o.spy(function onremove(vnode){ + o(root.childNodes.length).equals(1) + o(root.childNodes[0].nodeName).equals("A") + const iter = domFor(vnode) + o(iter.next()).deepEquals({done:false, value: root.childNodes[0]}) + o(iter.next().done).deepEquals(true) + o(root.childNodes.length).equals(1) + }) + render(root, [m("a", {onbeforeremove, onremove})]) + render(root, []) + + o(onbeforeremove.callCount).equals(1) + o(onremove.callCount).equals(0) + callAsync(function(){ + o(onremove.callCount).equals(1) + done() + }) + }) + o("works multiple vnodes with onbeforeremove (#3007, 1/6, BCA)", function (done) { + let thenCBA, thenCBB, thenCBC + const onbeforeremoveA = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBA = resolve}} + }) + const onbeforeremoveB = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBB = resolve}} + }) + const onbeforeremoveC = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBC = resolve}} + }) + // to avoid updating internal nodes only, vnodes have key attributes + const A = fragment({key: 1, onbeforeremove: onbeforeremoveA}, [m("a1"), m("a2")]) + const B = fragment({key: 2, onbeforeremove: onbeforeremoveB}, [m("b1"), m("b2")]) + const C = fragment({key: 3, onbeforeremove: onbeforeremoveC}, [m("c1"), m("c2")]) + + render(root, [A]) + o(onbeforeremoveA.callCount).equals(0) + o(onbeforeremoveB.callCount).equals(0) + o(onbeforeremoveC.callCount).equals(0) + + render(root, [B]) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(0) + o(onbeforeremoveC.callCount).equals(0) + + render(root, [C]) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(1) + o(onbeforeremoveC.callCount).equals(0) + + render(root, []) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(1) + o(onbeforeremoveC.callCount).equals(1) + + // not resolved + o(root.childNodes.length).equals(6) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("B1") + o(root.childNodes[3].nodeName).equals("B2") + o(root.childNodes[4].nodeName).equals("C1") + o(root.childNodes[5].nodeName).equals("C2") + + const iterA = domFor(A) + o(iterA.next().value.nodeName).equals("A1") + o(iterA.next().value.nodeName).equals("A2") + o(iterA.next().done).deepEquals(true) + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + callAsync(function(){ + // not resolved yet + o(root.childNodes.length).equals(6) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("B1") + o(root.childNodes[3].nodeName).equals("B2") + o(root.childNodes[4].nodeName).equals("C1") + o(root.childNodes[5].nodeName).equals("C2") + + const iterA = domFor(A) + o(iterA.next().value.nodeName).equals("A1") + o(iterA.next().value.nodeName).equals("A2") + o(iterA.next().done).deepEquals(true) + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + // resolve B + thenCBB() + callAsync(function(){ + o(root.childNodes.length).equals(4) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("C1") + o(root.childNodes[3].nodeName).equals("C2") + + const iterA = domFor(A) + o(iterA.next().value.nodeName).equals("A1") + o(iterA.next().value.nodeName).equals("A2") + o(iterA.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + // resolve C + thenCBC() + callAsync(function(){ + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + + const iterA = domFor(A) + o(iterA.next().value.nodeName).equals("A1") + o(iterA.next().value.nodeName).equals("A2") + o(iterA.next().done).deepEquals(true) + + // resolve A + thenCBA() + callAsync(function(){ + o(root.childNodes.length).equals(0) + done() + }) + }) + }) + }) + }) + o("works multiple vnodes with onbeforeremove (#3007, 2/6, CAB)", function (done) { + let thenCBA, thenCBB, thenCBC + const onbeforeremoveA = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBA = resolve}} + }) + const onbeforeremoveB = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBB = resolve}} + }) + const onbeforeremoveC = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBC = resolve}} + }) + // to avoid updating internal nodes only, vnodes have key attributes + const A = fragment({key: 1, onbeforeremove: onbeforeremoveA}, [m("a1"), m("a2")]) + const B = fragment({key: 2, onbeforeremove: onbeforeremoveB}, [m("b1"), m("b2")]) + const C = fragment({key: 3, onbeforeremove: onbeforeremoveC}, [m("c1"), m("c2")]) + + render(root, [A]) + o(onbeforeremoveA.callCount).equals(0) + o(onbeforeremoveB.callCount).equals(0) + o(onbeforeremoveC.callCount).equals(0) + + render(root, [B]) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(0) + o(onbeforeremoveC.callCount).equals(0) + + render(root, [C]) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(1) + o(onbeforeremoveC.callCount).equals(0) + + render(root, []) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(1) + o(onbeforeremoveC.callCount).equals(1) + + // not resolved + o(root.childNodes.length).equals(6) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("B1") + o(root.childNodes[3].nodeName).equals("B2") + o(root.childNodes[4].nodeName).equals("C1") + o(root.childNodes[5].nodeName).equals("C2") + + const iterA = domFor(A) + o(iterA.next().value.nodeName).equals("A1") + o(iterA.next().value.nodeName).equals("A2") + o(iterA.next().done).deepEquals(true) + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + callAsync(function(){ + // not resolved yet + o(root.childNodes.length).equals(6) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("B1") + o(root.childNodes[3].nodeName).equals("B2") + o(root.childNodes[4].nodeName).equals("C1") + o(root.childNodes[5].nodeName).equals("C2") + + const iterA = domFor(A) + o(iterA.next().value.nodeName).equals("A1") + o(iterA.next().value.nodeName).equals("A2") + o(iterA.next().done).deepEquals(true) + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + // resolve C + thenCBC() + callAsync(function(){ + o(root.childNodes.length).equals(4) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("B1") + o(root.childNodes[3].nodeName).equals("B2") + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + const iterA = domFor(A) + o(iterA.next().value.nodeName).equals("A1") + o(iterA.next().value.nodeName).equals("A2") + o(iterA.next().done).deepEquals(true) + + // resolve A + thenCBA() + callAsync(function(){ + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("B1") + o(root.childNodes[1].nodeName).equals("B2") + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + // resolve B + thenCBB() + callAsync(function(){ + o(root.childNodes.length).equals(0) + done() + }) + }) + }) + }) + }) + o("works multiple vnodes with onbeforeremove (#3007, 3/6, ABC)", function (done) { + let thenCBA, thenCBB, thenCBC + const onbeforeremoveA = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBA = resolve}} + }) + const onbeforeremoveB = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBB = resolve}} + }) + const onbeforeremoveC = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBC = resolve}} + }) + // to avoid updating internal nodes only, vnodes have key attributes + const A = fragment({key: 1, onbeforeremove: onbeforeremoveA}, [m("a1"), m("a2")]) + const B = fragment({key: 2, onbeforeremove: onbeforeremoveB}, [m("b1"), m("b2")]) + const C = fragment({key: 3, onbeforeremove: onbeforeremoveC}, [m("c1"), m("c2")]) + + render(root, [A]) + o(onbeforeremoveA.callCount).equals(0) + o(onbeforeremoveB.callCount).equals(0) + o(onbeforeremoveC.callCount).equals(0) + + render(root, [B]) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(0) + o(onbeforeremoveC.callCount).equals(0) + + render(root, [C]) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(1) + o(onbeforeremoveC.callCount).equals(0) + + render(root, []) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(1) + o(onbeforeremoveC.callCount).equals(1) + + // not resolved + o(root.childNodes.length).equals(6) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("B1") + o(root.childNodes[3].nodeName).equals("B2") + o(root.childNodes[4].nodeName).equals("C1") + o(root.childNodes[5].nodeName).equals("C2") + + const iterA = domFor(A) + o(iterA.next().value.nodeName).equals("A1") + o(iterA.next().value.nodeName).equals("A2") + o(iterA.next().done).deepEquals(true) + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + callAsync(function(){ + // not resolved yet + o(root.childNodes.length).equals(6) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("B1") + o(root.childNodes[3].nodeName).equals("B2") + o(root.childNodes[4].nodeName).equals("C1") + o(root.childNodes[5].nodeName).equals("C2") + + const iterA = domFor(A) + o(iterA.next().value.nodeName).equals("A1") + o(iterA.next().value.nodeName).equals("A2") + o(iterA.next().done).deepEquals(true) + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + // resolve A + thenCBA() + callAsync(function(){ + o(root.childNodes.length).equals(4) + o(root.childNodes[0].nodeName).equals("B1") + o(root.childNodes[1].nodeName).equals("B2") + o(root.childNodes[2].nodeName).equals("C1") + o(root.childNodes[3].nodeName).equals("C2") + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + // resolve B + thenCBB() + callAsync(function(){ + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("C1") + o(root.childNodes[1].nodeName).equals("C2") + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + // resolve C + thenCBC() + callAsync(function(){ + o(root.childNodes.length).equals(0) + done() + }) + }) + }) + }) + }) + o("works multiple vnodes with onbeforeremove (#3007, 4/6, ACB)", function (done) { + let thenCBA, thenCBB, thenCBC + const onbeforeremoveA = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBA = resolve}} + }) + const onbeforeremoveB = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBB = resolve}} + }) + const onbeforeremoveC = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBC = resolve}} + }) + // to avoid updating internal nodes only, vnodes have key attributes + const A = fragment({key: 1, onbeforeremove: onbeforeremoveA}, [m("a1"), m("a2")]) + const B = fragment({key: 2, onbeforeremove: onbeforeremoveB}, [m("b1"), m("b2")]) + const C = fragment({key: 3, onbeforeremove: onbeforeremoveC}, [m("c1"), m("c2")]) + + render(root, [A]) + o(onbeforeremoveA.callCount).equals(0) + o(onbeforeremoveB.callCount).equals(0) + o(onbeforeremoveC.callCount).equals(0) + + render(root, [B]) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(0) + o(onbeforeremoveC.callCount).equals(0) + + render(root, [C]) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(1) + o(onbeforeremoveC.callCount).equals(0) + + render(root, []) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(1) + o(onbeforeremoveC.callCount).equals(1) + + // not resolved + o(root.childNodes.length).equals(6) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("B1") + o(root.childNodes[3].nodeName).equals("B2") + o(root.childNodes[4].nodeName).equals("C1") + o(root.childNodes[5].nodeName).equals("C2") + + const iterA = domFor(A) + o(iterA.next().value.nodeName).equals("A1") + o(iterA.next().value.nodeName).equals("A2") + o(iterA.next().done).deepEquals(true) + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + callAsync(function(){ + // not resolved yet + o(root.childNodes.length).equals(6) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("B1") + o(root.childNodes[3].nodeName).equals("B2") + o(root.childNodes[4].nodeName).equals("C1") + o(root.childNodes[5].nodeName).equals("C2") + + const iterA = domFor(A) + o(iterA.next().value.nodeName).equals("A1") + o(iterA.next().value.nodeName).equals("A2") + o(iterA.next().done).deepEquals(true) + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + // resolve A + thenCBA() + callAsync(function(){ + o(root.childNodes.length).equals(4) + o(root.childNodes[0].nodeName).equals("B1") + o(root.childNodes[1].nodeName).equals("B2") + o(root.childNodes[2].nodeName).equals("C1") + o(root.childNodes[3].nodeName).equals("C2") + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + // resolve C + thenCBC() + callAsync(function(){ + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("B1") + o(root.childNodes[1].nodeName).equals("B2") + + const iterC = domFor(B) + o(iterC.next().value.nodeName).equals("B1") + o(iterC.next().value.nodeName).equals("B2") + o(iterC.next().done).deepEquals(true) + + // resolve B + thenCBB() + callAsync(function(){ + o(root.childNodes.length).equals(0) + done() + }) + }) + }) + }) + }) + o("works multiple vnodes with onbeforeremove (#3007, 5/6, BAC)", function (done) { + let thenCBA, thenCBB, thenCBC + const onbeforeremoveA = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBA = resolve}} + }) + const onbeforeremoveB = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBB = resolve}} + }) + const onbeforeremoveC = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBC = resolve}} + }) + // to avoid updating internal nodes only, vnodes have key attributes + const A = fragment({key: 1, onbeforeremove: onbeforeremoveA}, [m("a1"), m("a2")]) + const B = fragment({key: 2, onbeforeremove: onbeforeremoveB}, [m("b1"), m("b2")]) + const C = fragment({key: 3, onbeforeremove: onbeforeremoveC}, [m("c1"), m("c2")]) + + render(root, [A]) + o(onbeforeremoveA.callCount).equals(0) + o(onbeforeremoveB.callCount).equals(0) + o(onbeforeremoveC.callCount).equals(0) + + render(root, [B]) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(0) + o(onbeforeremoveC.callCount).equals(0) + + render(root, [C]) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(1) + o(onbeforeremoveC.callCount).equals(0) + + render(root, []) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(1) + o(onbeforeremoveC.callCount).equals(1) + + // not resolved + o(root.childNodes.length).equals(6) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("B1") + o(root.childNodes[3].nodeName).equals("B2") + o(root.childNodes[4].nodeName).equals("C1") + o(root.childNodes[5].nodeName).equals("C2") + + const iterA = domFor(A) + o(iterA.next().value.nodeName).equals("A1") + o(iterA.next().value.nodeName).equals("A2") + o(iterA.next().done).deepEquals(true) + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + callAsync(function(){ + // not resolved yet + o(root.childNodes.length).equals(6) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("B1") + o(root.childNodes[3].nodeName).equals("B2") + o(root.childNodes[4].nodeName).equals("C1") + o(root.childNodes[5].nodeName).equals("C2") + + const iterA = domFor(A) + o(iterA.next().value.nodeName).equals("A1") + o(iterA.next().value.nodeName).equals("A2") + o(iterA.next().done).deepEquals(true) + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + // resolve B + thenCBB() + callAsync(function(){ + o(root.childNodes.length).equals(4) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("C1") + o(root.childNodes[3].nodeName).equals("C2") + + const iterB = domFor(A) + o(iterB.next().value.nodeName).equals("A1") + o(iterB.next().value.nodeName).equals("A2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + // resolve A + thenCBA() + callAsync(function(){ + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("C1") + o(root.childNodes[1].nodeName).equals("C2") + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + // resolve C + thenCBC() + callAsync(function(){ + o(root.childNodes.length).equals(0) + done() + }) + }) + }) + }) + }) + o("works multiple vnodes with onbeforeremove (#3007, 6/6, CBA)", function (done) { + let thenCBA, thenCBB, thenCBC + const onbeforeremoveA = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBA = resolve}} + }) + const onbeforeremoveB = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBB = resolve}} + }) + const onbeforeremoveC = o.spy(function onbeforeremove(){ + return {then(resolve){thenCBC = resolve}} + }) + // to avoid updating internal nodes only, vnodes have key attributes + const A = fragment({key: 1, onbeforeremove: onbeforeremoveA}, [m("a1"), m("a2")]) + const B = fragment({key: 2, onbeforeremove: onbeforeremoveB}, [m("b1"), m("b2")]) + const C = fragment({key: 3, onbeforeremove: onbeforeremoveC}, [m("c1"), m("c2")]) + + render(root, [A]) + o(onbeforeremoveA.callCount).equals(0) + o(onbeforeremoveB.callCount).equals(0) + o(onbeforeremoveC.callCount).equals(0) + + render(root, [B]) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(0) + o(onbeforeremoveC.callCount).equals(0) + + render(root, [C]) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(1) + o(onbeforeremoveC.callCount).equals(0) + + render(root, []) + o(onbeforeremoveA.callCount).equals(1) + o(onbeforeremoveB.callCount).equals(1) + o(onbeforeremoveC.callCount).equals(1) + + // not resolved + o(root.childNodes.length).equals(6) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("B1") + o(root.childNodes[3].nodeName).equals("B2") + o(root.childNodes[4].nodeName).equals("C1") + o(root.childNodes[5].nodeName).equals("C2") + + const iterA = domFor(A) + o(iterA.next().value.nodeName).equals("A1") + o(iterA.next().value.nodeName).equals("A2") + o(iterA.next().done).deepEquals(true) + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + callAsync(function(){ + // not resolved yet + o(root.childNodes.length).equals(6) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("B1") + o(root.childNodes[3].nodeName).equals("B2") + o(root.childNodes[4].nodeName).equals("C1") + o(root.childNodes[5].nodeName).equals("C2") + + const iterA = domFor(A) + o(iterA.next().value.nodeName).equals("A1") + o(iterA.next().value.nodeName).equals("A2") + o(iterA.next().done).deepEquals(true) + + const iterB = domFor(B) + o(iterB.next().value.nodeName).equals("B1") + o(iterB.next().value.nodeName).equals("B2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(C) + o(iterC.next().value.nodeName).equals("C1") + o(iterC.next().value.nodeName).equals("C2") + o(iterC.next().done).deepEquals(true) + + // resolve C + thenCBC() + callAsync(function(){ + o(root.childNodes.length).equals(4) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + o(root.childNodes[2].nodeName).equals("B1") + o(root.childNodes[3].nodeName).equals("B2") + + const iterB = domFor(A) + o(iterB.next().value.nodeName).equals("A1") + o(iterB.next().value.nodeName).equals("A2") + o(iterB.next().done).deepEquals(true) + + const iterC = domFor(B) + o(iterC.next().value.nodeName).equals("B1") + o(iterC.next().value.nodeName).equals("B2") + o(iterC.next().done).deepEquals(true) + + // resolve B + thenCBB() + callAsync(function(){ + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("A1") + o(root.childNodes[1].nodeName).equals("A2") + + const iterC = domFor(A) + o(iterC.next().value.nodeName).equals("A1") + o(iterC.next().value.nodeName).equals("A2") + o(iterC.next().done).deepEquals(true) + + // resolve A + thenCBA() + callAsync(function(){ + o(root.childNodes.length).equals(0) + done() + }) + }) + }) + }) + }) components.forEach(function(cmp){ const {kind, create: createComponent} = cmp o.spec(kind, function(){ @@ -173,6 +958,33 @@ o.spec("domFor(vnode)", function() { o(onupdate.callCount).equals(1) o(onbeforeremove.callCount).equals(1) }) + o("works in state.onbeforeremove and attrs.onbeforeremove", function () { + const onbeforeremove = o.spy(function onbeforeremove(vnode){ + o(root.childNodes.length).equals(3) + o(root.childNodes[0].nodeName).equals("A") + o(root.childNodes[1].nodeName).equals("B") + o(root.childNodes[2].nodeName).equals("C") + const iter = domFor(vnode) + o(iter.next()).deepEquals({done:false, value: root.childNodes[0]}) + o(iter.next()).deepEquals({done:false, value: root.childNodes[1]}) + o(iter.next()).deepEquals({done:false, value: root.childNodes[2]}) + o(iter.next().done).deepEquals(true) + o(root.childNodes.length).equals(3) + return {then(){}, finally(){}} + }) + const C = createComponent({ + view({children}){return children}, + onbeforeremove + }) + render(root, m(C, {onbeforeremove}, [ + m("a"), + m("b"), + m("c") + ])) + render(root, []) + + o(onbeforeremove.callCount).equals(2) + }) }) }) }) \ No newline at end of file diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js index b5621e421..a9a55b6c7 100644 --- a/render/tests/test-onbeforeremove.js +++ b/render/tests/test-onbeforeremove.js @@ -122,6 +122,23 @@ o.spec("onbeforeremove", function() { done() }) }) + o("handles thenable objecs (#2592)", function(done) { + var remove = function() {return {then: function(resolve) {resolve()}}} + var vnodes = m("div", {key: 1, onbeforeremove: remove}, "a") + var updated = [] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(root.firstChild.firstChild.nodeValue).equals("a") + + callAsync(function() { + o(root.childNodes.length).equals(0) + + done() + }) + }) components.forEach(function(cmp){ o.spec(cmp.kind, function(){ var createComponent = cmp.create diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index bf4800ffd..23ae334a5 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -6,6 +6,7 @@ var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") var fragment = require("../../render/fragment") +var callAsync = require("../../test-utils/callAsync") o.spec("onremove", function() { var $window, root, render @@ -194,7 +195,7 @@ o.spec("onremove", function() { o(onremove.callCount).equals(1) }) // Warning: this test is complicated because it's replicating a race condition. - o("removes correct nodes in fragment when child delays removal, parent removes, then child resolves", function () { + o("removes correct nodes in fragment when child delays removal, parent removes, then child resolves", function (done) { // Custom assertion - we need to test the entire tree for consistency. const template = (tpl) => (root) => { @@ -233,12 +234,12 @@ o.spec("onremove", function() { ${actual}` } } - var finallyCB1 - var finallyCB2 + var thenCB1 + var thenCB2 var C = createComponent({ view({children}){return children}, onbeforeremove(){ - return {then(){}, finally: function (fcb) { finallyCB1 = fcb }} + return {then(resolve){thenCB1=resolve}} } }) function update(id, showParent, showChild) { @@ -253,7 +254,7 @@ ${actual}` m("a", {onremove: removeSyncChild}, "sync child"), showChild && m(C, { onbeforeremove: function () { - return {then(){}, finally: function (fcb) { finallyCB2 = fcb }} + return {then(resolve){thenCB2=resolve}} }, onremove: removeAsyncChild }, m("div", id)) @@ -268,8 +269,8 @@ ${actual}` ["A", "sync child"], ["DIV", "1"], ])) - o(finallyCB1).equals(undefined) - o(finallyCB2).equals(undefined) + o(thenCB1).equals(undefined) + o(thenCB2).equals(undefined) const hooks2 = update("2", true, false) @@ -277,102 +278,114 @@ ${actual}` ["A", "sync child"], ["DIV", "1"], ])) + o(thenCB1).equals(undefined) + o(thenCB2).equals(undefined) - o(typeof finallyCB1).equals("function") - o(typeof finallyCB2).equals("function") + // Promises (micro-tasks) are processed before the callAsync callback. + callAsync(() => { + o(typeof thenCB1).equals("function") + o(typeof thenCB2).equals("function") - var original1 = finallyCB1 - var original2 = finallyCB2 + var original1 = thenCB1 + var original2 = thenCB2 - const hooks3 = update("3", true, true) + const hooks3 = update("3", true, true) - o(root).satisfies(template([ - ["A", "sync child"], - ["DIV", "1"], - ["DIV", "3"], - ])) + o(root).satisfies(template([ + ["A", "sync child"], + ["DIV", "1"], + ["DIV", "3"], + ])) - o(hooks3.removeParent.callCount).equals(0) - o(hooks3.removeSyncChild.callCount).equals(0) - o(hooks3.removeAsyncChild.callCount).equals(0) - o(finallyCB1).equals(original1) - o(finallyCB2).equals(original2) + o(hooks3.removeParent.callCount).equals(0) + o(hooks3.removeSyncChild.callCount).equals(0) + o(hooks3.removeAsyncChild.callCount).equals(0) + o(thenCB1).equals(original1) + o(thenCB2).equals(original2) - const hooks4 = update("4", false, true) + const hooks4 = update("4", false, true) - o(root).satisfies(template([ - ["DIV", "1"], - ])) + o(root).satisfies(template([ + ["DIV", "1"], + ])) - o(hooks3.removeParent.callCount).equals(1) - o(hooks3.removeSyncChild.callCount).equals(1) - o(hooks3.removeAsyncChild.callCount).equals(1) - o(hooks3.removeParent.args[0].tag).equals("[") - o(finallyCB1).equals(original1) - o(finallyCB2).equals(original2) + o(hooks3.removeParent.callCount).equals(1) + o(hooks3.removeSyncChild.callCount).equals(1) + o(hooks3.removeAsyncChild.callCount).equals(1) + o(hooks3.removeParent.args[0].tag).equals("[") + o(thenCB1).equals(original1) + o(thenCB2).equals(original2) - const hooks5 = update("5", true, true) + const hooks5 = update("5", true, true) - o(root).satisfies(template([ - ["DIV", "1"], - ["A", "sync child"], - ["DIV", "5"], - ])) - o(finallyCB1).equals(original1) - o(finallyCB2).equals(original2) + o(root).satisfies(template([ + ["DIV", "1"], + ["A", "sync child"], + ["DIV", "5"], + ])) + o(thenCB1).equals(original1) + o(thenCB2).equals(original2) - o(hooks1.removeAsyncChild.callCount).equals(0) + o(hooks1.removeAsyncChild.callCount).equals(0) - finallyCB1() + thenCB1() - o(hooks1.removeAsyncChild.callCount).equals(0) + o(hooks1.removeAsyncChild.callCount).equals(0) + callAsync(() => { + o(hooks1.removeAsyncChild.callCount).equals(0) - finallyCB2() + thenCB2() - o(hooks1.removeAsyncChild.callCount).equals(1) + o(hooks1.removeAsyncChild.callCount).equals(0) + callAsync(() => { + o(hooks1.removeAsyncChild.callCount).equals(1) - o(root).satisfies(template([ - ["A", "sync child"], - ["DIV", "5"], - ])) - o(finallyCB1).equals(original1) - o(finallyCB2).equals(original2) + o(root).satisfies(template([ + ["A", "sync child"], + ["DIV", "5"], + ])) + o(thenCB1).equals(original1) + o(thenCB2).equals(original2) - const hooks6 = update("6", true, true) + const hooks6 = update("6", true, true) - o(root).satisfies(template([ - ["A", "sync child"], - ["DIV", "6"], - ])) - o(finallyCB1).equals(original1) - o(finallyCB2).equals(original2) + o(root).satisfies(template([ + ["A", "sync child"], + ["DIV", "6"], + ])) + o(thenCB1).equals(original1) + o(thenCB2).equals(original2) - // final tally - o(hooks1.removeParent.callCount).equals(0) - o(hooks1.removeSyncChild.callCount).equals(0) - o(hooks1.removeAsyncChild.callCount).equals(1) + // final tally + o(hooks1.removeParent.callCount).equals(0) + o(hooks1.removeSyncChild.callCount).equals(0) + o(hooks1.removeAsyncChild.callCount).equals(1) - o(hooks2.removeParent.callCount).equals(0) - o(hooks2.removeSyncChild.callCount).equals(0) - o(hooks2.removeAsyncChild.callCount).equals(0) + o(hooks2.removeParent.callCount).equals(0) + o(hooks2.removeSyncChild.callCount).equals(0) + o(hooks2.removeAsyncChild.callCount).equals(0) - o(hooks3.removeParent.callCount).equals(1) - o(hooks3.removeSyncChild.callCount).equals(1) - o(hooks3.removeAsyncChild.callCount).equals(1) + o(hooks3.removeParent.callCount).equals(1) + o(hooks3.removeSyncChild.callCount).equals(1) + o(hooks3.removeAsyncChild.callCount).equals(1) - o(hooks4.removeParent.callCount).equals(0) - o(hooks4.removeSyncChild.callCount).equals(0) - o(hooks4.removeAsyncChild.callCount).equals(0) + o(hooks4.removeParent.callCount).equals(0) + o(hooks4.removeSyncChild.callCount).equals(0) + o(hooks4.removeAsyncChild.callCount).equals(0) - o(hooks5.removeParent.callCount).equals(0) - o(hooks5.removeSyncChild.callCount).equals(0) - o(hooks5.removeAsyncChild.callCount).equals(0) + o(hooks5.removeParent.callCount).equals(0) + o(hooks5.removeSyncChild.callCount).equals(0) + o(hooks5.removeAsyncChild.callCount).equals(0) - o(hooks6.removeParent.callCount).equals(0) - o(hooks6.removeSyncChild.callCount).equals(0) - o(hooks6.removeAsyncChild.callCount).equals(0) + o(hooks6.removeParent.callCount).equals(0) + o(hooks6.removeSyncChild.callCount).equals(0) + o(hooks6.removeAsyncChild.callCount).equals(0) + done() + }) + }) + }) }) }) }) 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)