From 37d6c30f3de55d4ea01827af9c1e6cbafee90b11 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 8 Jun 2019 01:30:49 -0700 Subject: [PATCH 01/21] Add failing test for nested c.base --- test/browser/components.test.js | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/test/browser/components.test.js b/test/browser/components.test.js index 18ec0719c3..5f2b9cdecc 100644 --- a/test/browser/components.test.js +++ b/test/browser/components.test.js @@ -1573,4 +1573,62 @@ describe('Components', () => { expect(mounted).to.equal(',1,0,3,2,5,4'); expect(unmounted).to.equal(',0,1,2,3'); }); + + it('should keep c.base up to date if a nested child component changes DOM nodes', () => { + let parentDom = {}; + let parent1 = {}; + let parent2 = {}; + let child = {}; + + class Child extends Component { + constructor(props, context) { + super(props, context); + this.state = { tagName: 'p' }; + } + render() { + return h(this.state.tagName, {}, 'helo'); + } + } + + class Parent1 extends Component { + render() { + return this.props.children; + } + } + + function Parent2(props) { + return props.children; + } + + function ParentWithDom(props) { + return
{props.children}
; + } + + render(( + + + + + + + + ), scratch); + + let domChild = scratch.firstChild.firstChild; + expect(scratch.innerHTML).to.equal('

helo

'); + expect(child.current.base).to.equalNode(domChild); + expect(parent2.current.base).to.equalNode(domChild); + expect(parent1.current.base).to.equalNode(domChild); + expect(parentDom.current.base).to.equalNode(scratch.firstChild); + + child.current.setState({ tagName: 'span' }); + rerender(); + + domChild = scratch.firstChild.firstChild; + expect(scratch.innerHTML).to.equal('
helo
'); + expect(child.current.base).to.equalNode(domChild); + expect(parent2.current.base).to.equalNode(domChild); + expect(parent1.current.base).to.equalNode(domChild); + expect(parentDom.current.base).to.equalNode(scratch.firstChild); + }); }); From 9df48dc8abd389605cccf8130a6ff1b762b5f2b8 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 8 Jun 2019 01:45:05 -0700 Subject: [PATCH 02/21] Fix out of date `c.base` pointers (+28 B) --- src/component.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/component.js b/src/component.js index e6ab7a0aa6..67df58bbf1 100644 --- a/src/component.js +++ b/src/component.js @@ -69,8 +69,16 @@ Component.prototype.forceUpdate = function(callback) { const force = callback!==false; let mounts = []; - diff(parentDom, vnode, assign({}, vnode), this._context, parentDom.ownerSVGElement!==undefined, null, mounts, force, dom == null ? getDomSibling(vnode) : dom); + let newDom = diff(parentDom, vnode, assign({}, vnode), this._context, parentDom.ownerSVGElement!==undefined, null, mounts, force, dom == null ? getDomSibling(vnode) : dom); commitRoot(mounts, vnode); + + if (newDom != dom) { + // Update parent component's _dom and c.base pointers + // TODO: What to do about _lastDomChild? + while ((vnode = vnode._parent) && vnode._component && vnode._dom == dom) { + vnode._dom = vnode._component.base = newDom; + } + } } if (callback) callback(); }; From e0d05a7803a6568a160be2e11021ac6f6b2793bd Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 8 Jun 2019 01:55:21 -0700 Subject: [PATCH 03/21] Update c.base test to verify sibling doesn't change --- test/browser/components.test.js | 59 +++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/test/browser/components.test.js b/test/browser/components.test.js index 5f2b9cdecc..50e3282e24 100644 --- a/test/browser/components.test.js +++ b/test/browser/components.test.js @@ -1575,28 +1575,32 @@ describe('Components', () => { }); it('should keep c.base up to date if a nested child component changes DOM nodes', () => { - let parentDom = {}; - let parent1 = {}; - let parent2 = {}; - let child = {}; + let parentDom1 = {}; + let parentDom2 = {}; + let parent1; + let parent2; + let child; class Child extends Component { constructor(props, context) { super(props, context); + child = this; this.state = { tagName: 'p' }; } render() { - return h(this.state.tagName, {}, 'helo'); + return h(this.state.tagName, {}, 'Hello'); } } class Parent1 extends Component { render() { + parent1 = this; return this.props.children; } } function Parent2(props) { + parent2 = this; return props.children; } @@ -1605,30 +1609,37 @@ describe('Components', () => { } render(( - - - - - - - + + + + + + + + + + World! + + ), scratch); let domChild = scratch.firstChild.firstChild; - expect(scratch.innerHTML).to.equal('

helo

'); - expect(child.current.base).to.equalNode(domChild); - expect(parent2.current.base).to.equalNode(domChild); - expect(parent1.current.base).to.equalNode(domChild); - expect(parentDom.current.base).to.equalNode(scratch.firstChild); - - child.current.setState({ tagName: 'span' }); + expect(scratch.innerHTML).to.equal('

Hello

World!
'); + expect(child.base).to.equalNode(domChild); + expect(parent2.base).to.equalNode(domChild); + expect(parent1.base).to.equalNode(domChild); + expect(parentDom1.current.base).to.equalNode(scratch.firstChild); + expect(parentDom2.current.base).to.equalNode(scratch.lastChild); + + child.setState({ tagName: 'span' }); rerender(); domChild = scratch.firstChild.firstChild; - expect(scratch.innerHTML).to.equal('
helo
'); - expect(child.current.base).to.equalNode(domChild); - expect(parent2.current.base).to.equalNode(domChild); - expect(parent1.current.base).to.equalNode(domChild); - expect(parentDom.current.base).to.equalNode(scratch.firstChild); + expect(scratch.innerHTML).to.equal('
Hello
World!
'); + expect(child.base).to.equalNode(domChild); + expect(parent2.base).to.equalNode(domChild); + expect(parent1.base).to.equalNode(domChild); + expect(parentDom1.current.base).to.equalNode(scratch.firstChild); + expect(parentDom2.current.base).to.equalNode(scratch.lastChild); }); }); From b0d3045a688d2ca47bacb1005ed7c8048bd57bbf Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 8 Jun 2019 02:30:17 -0700 Subject: [PATCH 04/21] Only update c.base if component is first child of parent (+12 B) --- src/component.js | 14 ++- test/browser/components.test.js | 186 ++++++++++++++++++++++++++++---- 2 files changed, 173 insertions(+), 27 deletions(-) diff --git a/src/component.js b/src/component.js index 67df58bbf1..62ee9cd62b 100644 --- a/src/component.js +++ b/src/component.js @@ -61,7 +61,7 @@ Component.prototype.setState = function(update, callback) { * re-renderd */ Component.prototype.forceUpdate = function(callback) { - let vnode = this._vnode, dom = this._vnode._dom, parentDom = this._parentDom; + let vnode = this._vnode, oldDom = this._vnode._dom, parentDom = this._parentDom; if (parentDom) { // Set render mode so that we can differantiate where the render request // is coming from. We need this because forceUpdate should never call @@ -69,13 +69,19 @@ Component.prototype.forceUpdate = function(callback) { const force = callback!==false; let mounts = []; - let newDom = diff(parentDom, vnode, assign({}, vnode), this._context, parentDom.ownerSVGElement!==undefined, null, mounts, force, dom == null ? getDomSibling(vnode) : dom); + let newDom = diff(parentDom, vnode, assign({}, vnode), this._context, parentDom.ownerSVGElement!==undefined, null, mounts, force, oldDom == null ? getDomSibling(vnode) : oldDom); commitRoot(mounts, vnode); - if (newDom != dom) { + if (newDom != oldDom) { // Update parent component's _dom and c.base pointers // TODO: What to do about _lastDomChild? - while ((vnode = vnode._parent) && vnode._component && vnode._dom == dom) { + while ( + vnode._parent + && vnode._parent._children.indexOf(vnode) == 0 + && (vnode = vnode._parent) + && vnode._component + && vnode._dom == oldDom + ) { vnode._dom = vnode._component.base = newDom; } } diff --git a/test/browser/components.test.js b/test/browser/components.test.js index 50e3282e24..3fa4d679d9 100644 --- a/test/browser/components.test.js +++ b/test/browser/components.test.js @@ -1575,8 +1575,7 @@ describe('Components', () => { }); it('should keep c.base up to date if a nested child component changes DOM nodes', () => { - let parentDom1 = {}; - let parentDom2 = {}; + let parentDom1; let parent1; let parent2; let child; @@ -1605,41 +1604,182 @@ describe('Components', () => { } function ParentWithDom(props) { + parentDom1 = this; return
{props.children}
; } + render(( + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal('

Hello

'); + expect(child.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent2.base).to.equalNode(child.base); + expect(parent1.base).to.equalNode(child.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); + + child.setState({ tagName: 'span' }); + rerender(); + + expect(scratch.innerHTML).to.equal('
Hello
'); + expect(child.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent2.base).to.equalNode(child.base); + expect(parent1.base).to.equalNode(child.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); + }); + + it('should not update sibling c.base if child component changes DOM nodes', () => { + let parentDom1; + let parent1; + let parent2; + let child; + let s1 = {}, s2 = {}, s3 = {}, s4 = {}; + + class Child extends Component { + constructor(props, context) { + super(props, context); + child = this; + this.state = { tagName: 'p' }; + } + render() { + return h(this.state.tagName, {}, 'Hello'); + } + } + + class Parent1 extends Component { + render() { + parent1 = this; + return this.props.children; + } + } + + function Parent2(props) { + parent2 = this; + return props.children; + } + + function ParentWithDom(props) { + parentDom1 = this; + return
{props.children}
; + } + + function Sibling(props) { + return

; + } + render(( - - - - + + + + + + + - - World! - + ), scratch); - let domChild = scratch.firstChild.firstChild; - expect(scratch.innerHTML).to.equal('

Hello

World!
'); - expect(child.base).to.equalNode(domChild); - expect(parent2.base).to.equalNode(domChild); - expect(parent1.base).to.equalNode(domChild); - expect(parentDom1.current.base).to.equalNode(scratch.firstChild); - expect(parentDom2.current.base).to.equalNode(scratch.lastChild); + expect(scratch.innerHTML).to.equal('

Hello

'); + expect(child.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent2.base).to.equalNode(child.base); + expect(parent1.base).to.equalNode(child.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); + expect(s1.current.base).to.equalNode(scratch.firstChild.childNodes[1]); + expect(s2.current.base).to.equalNode(scratch.firstChild.childNodes[2]); + expect(s3.current.base).to.equalNode(scratch.firstChild.childNodes[3]); + expect(s4.current.base).to.equalNode(scratch.lastChild); + + child.setState({ tagName: 'span' }); + rerender(); + + expect(scratch.innerHTML).to.equal('
Hello

'); + expect(child.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent2.base).to.equalNode(child.base); + expect(parent1.base).to.equalNode(child.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); + expect(s1.current.base).to.equalNode(scratch.firstChild.childNodes[1]); + expect(s2.current.base).to.equalNode(scratch.firstChild.childNodes[2]); + expect(s3.current.base).to.equalNode(scratch.firstChild.childNodes[3]); + expect(s4.current.base).to.equalNode(scratch.lastChild); + }); + + // TODO: + it('should not update parent c.base if child component changes DOM nodes and is not first child component', () => { + let parentDom1; + let parent1; + let parent2; + let child; + let sibling; + + class Child extends Component { + constructor(props, context) { + super(props, context); + child = this; + this.state = { tagName: 'p' }; + } + render() { + return h(this.state.tagName, {}, 'Hello'); + } + } + + class Parent1 extends Component { + render() { + parent1 = this; + return this.props.children; + } + } + + function Parent2(props) { + parent2 = this; + return props.children; + } + + function ParentWithDom(props) { + parentDom1 = this; + return
{props.children}
; + } + + function Sibling(props) { + sibling = this; + return

; + } + + render(( + + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal('

Hello

'); + expect(child.base).to.equalNode(scratch.firstChild.lastChild); + expect(parent2.base).to.equalNode(child.base); + expect(sibling.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent1.base).to.equalNode(sibling.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); child.setState({ tagName: 'span' }); rerender(); - domChild = scratch.firstChild.firstChild; - expect(scratch.innerHTML).to.equal('
Hello
World!
'); - expect(child.base).to.equalNode(domChild); - expect(parent2.base).to.equalNode(domChild); - expect(parent1.base).to.equalNode(domChild); - expect(parentDom1.current.base).to.equalNode(scratch.firstChild); - expect(parentDom2.current.base).to.equalNode(scratch.lastChild); + expect(scratch.innerHTML).to.equal('

Hello
'); + expect(child.base).to.equalNode(scratch.firstChild.lastChild); + expect(parent2.base).to.equalNode(child.base); + expect(sibling.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent1.base).to.equalNode(sibling.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); }); }); From 357ca4cee8efe3115dd3175971884876b032068e Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 8 Jun 2019 03:17:31 -0700 Subject: [PATCH 05/21] Add note about out of date _lastDomChild --- src/component.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/component.js b/src/component.js index 62ee9cd62b..6474dacb13 100644 --- a/src/component.js +++ b/src/component.js @@ -74,7 +74,10 @@ Component.prototype.forceUpdate = function(callback) { if (newDom != oldDom) { // Update parent component's _dom and c.base pointers - // TODO: What to do about _lastDomChild? + // Note: parent component's _lastDomChild may become out of date, but that is okay, + // because _lastDomChild isn't used to restart diffs. It is only important while a + // diff for that component is happening and the diff will reset it to the correct value + // if/when that component is updated while ( vnode._parent && vnode._parent._children.indexOf(vnode) == 0 From 1e21fa322b31f373d5362bedba36bd43dcadcc25 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 8 Jun 2019 03:39:08 -0700 Subject: [PATCH 06/21] Eagerly cleanup _lastDomChild (+1 B) In some situations an updating component could cause it's parent's _lastDomChild to become stale and point to a no longer mounted DOM node. This change eagerly cleans up _lastDomChild to prevent stale pointers since we don't need this pointer after its value is read in diffChildren. --- src/component.js | 4 ---- src/diff/children.js | 9 ++++++++- src/diff/index.js | 1 - 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/component.js b/src/component.js index 6474dacb13..8971d2204e 100644 --- a/src/component.js +++ b/src/component.js @@ -74,10 +74,6 @@ Component.prototype.forceUpdate = function(callback) { if (newDom != oldDom) { // Update parent component's _dom and c.base pointers - // Note: parent component's _lastDomChild may become out of date, but that is okay, - // because _lastDomChild isn't used to restart diffs. It is only important while a - // diff for that component is happening and the diff will reset it to the correct value - // if/when that component is updated while ( vnode._parent && vnode._parent._children.indexOf(vnode) == 0 diff --git a/src/diff/children.js b/src/diff/children.js index 247dd5957b..426876248e 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -100,6 +100,11 @@ export function diffChildren(parentDom, newParentVNode, oldParentVNode, context, // have a non-null _lastDomChild. Continue the diff from the end of // this Fragment's DOM tree. newDom = childVNode._lastDomChild; + + // Eagerly cleanup _lastDomChild. We don't need to persist the value because + // it is only used by `diffChildren` to determine where to resume the diff after + // diffing Components and Fragments. + childVNode._lastDomChild = null; } else if (excessDomChildren==oldVNode || newDom!=oldDom || newDom.parentNode==null) { // NOTE: excessDomChildren==oldVNode above: @@ -124,7 +129,9 @@ export function diffChildren(parentDom, newParentVNode, oldParentVNode, context, if (typeof newParentVNode.type == 'function') { // At this point, if childVNode._lastDomChild existed, then - // newDom = childVNode._lastDomChild per line 101 + // newDom = childVNode._lastDomChild per line 101. Else it is + // the same as childVNode._dom, meaning this component returned + // only a single DOM node newParentVNode._lastDomChild = newDom; } } diff --git a/src/diff/index.js b/src/diff/index.js index f7d5c0a068..762e7869af 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -89,7 +89,6 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi c._dirty = false; c._vnode = newVNode; newVNode._dom = oldVNode._dom; - newVNode._lastDomChild = oldVNode._lastDomChild; newVNode._children = oldVNode._children; break outer; } From 3f3de03ab6e787a01e388dbc4fcfa84f8aa954d9 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 8 Jun 2019 18:35:14 -0700 Subject: [PATCH 07/21] Add test for child that is not first child --- test/browser/components.test.js | 60 +++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/test/browser/components.test.js b/test/browser/components.test.js index 3fa4d679d9..04c0361d7f 100644 --- a/test/browser/components.test.js +++ b/test/browser/components.test.js @@ -1713,8 +1713,56 @@ describe('Components', () => { expect(s4.current.base).to.equalNode(scratch.lastChild); }); - // TODO: - it('should not update parent c.base if child component changes DOM nodes and is not first child component', () => { + it('should not update parent c.base if child component changes DOM nodes and it is not first child component', () => { + let parent1; + let child; + let sibling; + + class Child extends Component { + constructor(props, context) { + super(props, context); + child = this; + this.state = { tagName: 'p' }; + } + render() { + return h(this.state.tagName, {}, 'Hello'); + } + } + + class Parent1 extends Component { + render() { + parent1 = this; + return this.props.children; + } + } + + function Sibling(props) { + sibling = this; + return

; + } + + render(( + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal('

Hello

'); + expect(child.base).to.equalNode(scratch.lastChild); + expect(sibling.base).to.equalNode(scratch.firstChild); + expect(parent1.base).to.equalNode(sibling.base); + + child.setState({ tagName: 'span' }); + rerender(); + + expect(scratch.innerHTML).to.equal('

Hello'); + expect(child.base).to.equalNode(scratch.lastChild); + expect(sibling.base).to.equalNode(scratch.firstChild); + expect(parent1.base).to.equalNode(sibling.base); + }); + + it('should not update parent c.base if child component changes DOM nodes and a parent is not first child component', () => { let parentDom1; let parent1; let parent2; @@ -1755,11 +1803,11 @@ describe('Components', () => { } render(( - - + + - - + + From f8f5e27c6e4e4023f9b1e158cfaea2dbf85c3e3c Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 9 Jun 2019 18:59:17 -0700 Subject: [PATCH 08/21] Update Fragment tests todos --- test/browser/fragments.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/browser/fragments.test.js b/test/browser/fragments.test.js index b87997879e..333118973d 100644 --- a/test/browser/fragments.test.js +++ b/test/browser/fragments.test.js @@ -1295,7 +1295,7 @@ describe('Fragment', () => { expectDomLogToBe([ '
    012345.insertBefore(
  1. 4,
  2. 0)', '
      401235.insertBefore(
    1. 5,
    2. 0)', - // TODO: see issue #193 - Hmmm why does this extra append happen? + // TODO: Hmmm why does this extra append happen? '
        453012.appendChild(
      1. 3)' ]); @@ -2575,7 +2575,6 @@ describe('Fragment', () => { div('B') ].join(''), 'updateB'); expectDomLogToBe([ - // TODO: Extra appends happen here in actual. Why? '
        .appendChild(#text)', '
        A3A4.appendChild(
        B)' ]); From b5c70e2de05abb32aedc59717e1a7a61cfc0d15c Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 10 Jun 2019 12:36:39 -0700 Subject: [PATCH 09/21] Add test for failing c.base update with null first children --- test/browser/components.test.js | 58 +++++++++++++++++++++++++++++++++ test/polyfills.js | 1 + 2 files changed, 59 insertions(+) diff --git a/test/browser/components.test.js b/test/browser/components.test.js index 04c0361d7f..3bdcac7fed 100644 --- a/test/browser/components.test.js +++ b/test/browser/components.test.js @@ -1762,6 +1762,64 @@ describe('Components', () => { expect(parent1.base).to.equalNode(sibling.base); }); + it('should update parent c.base if child component changes DOM nodes and it is first non-null child component', () => { + let parent1; + let child; + let sibling; + let nullInst; + + class Child extends Component { + constructor(props, context) { + super(props, context); + child = this; + this.state = { tagName: 'p' }; + } + render() { + return h(this.state.tagName, {}, 'Hello'); + } + } + + class Parent1 extends Component { + render() { + parent1 = this; + return this.props.children; + } + } + + function Sibling(props) { + sibling = this; + return

        ; + } + + function Null() { + nullInst = this; + return null; + } + + render(( + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal('

        Hello

        '); + expect(nullInst.base).to.equal(undefined); + expect(child.base).to.equalNode(scratch.firstChild); + expect(sibling.base).to.equalNode(scratch.lastChild); + expect(parent1.base).to.equalNode(child.base); + + child.setState({ tagName: 'span' }); + rerender(); + + expect(scratch.innerHTML).to.equal('Hello

        '); + expect(nullInst.base).to.equal(undefined); + expect(child.base).to.equalNode(scratch.firstChild); + expect(sibling.base).to.equalNode(scratch.lastChild); + expect(parent1.base).to.equalNode(child.base); + }); + it('should not update parent c.base if child component changes DOM nodes and a parent is not first child component', () => { let parentDom1; let parent1; diff --git a/test/polyfills.js b/test/polyfills.js index a61318c6df..b58c943d56 100644 --- a/test/polyfills.js +++ b/test/polyfills.js @@ -28,6 +28,7 @@ chai.use((chai, util) => { Assertion.addMethod('equalNode', function (expectedNode, message) { const obj = this._obj; + message = message || 'equalNode'; if (expectedNode == null) { new Assertion(obj).to.equal(expectedNode); From 6f97524f9b7c0c54e23e654b47e95b5807de4209 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 10 Jun 2019 12:37:15 -0700 Subject: [PATCH 10/21] Simplify condition and fix children position check (-7 B) --- src/component.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/component.js b/src/component.js index 8971d2204e..30861702ab 100644 --- a/src/component.js +++ b/src/component.js @@ -76,10 +76,9 @@ Component.prototype.forceUpdate = function(callback) { // Update parent component's _dom and c.base pointers while ( vnode._parent - && vnode._parent._children.indexOf(vnode) == 0 + && vnode._parent._dom === oldDom && (vnode = vnode._parent) && vnode._component - && vnode._dom == oldDom ) { vnode._dom = vnode._component.base = newDom; } From fe4f87680ece1a1c5a05c4fb3be5c2c13de6ea16 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Mon, 10 Jun 2019 12:46:13 -0700 Subject: [PATCH 11/21] Fix c.base condition to update root Fragment (+3 B) --- src/component.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/component.js b/src/component.js index 30861702ab..cbef9444f5 100644 --- a/src/component.js +++ b/src/component.js @@ -78,9 +78,11 @@ Component.prototype.forceUpdate = function(callback) { vnode._parent && vnode._parent._dom === oldDom && (vnode = vnode._parent) - && vnode._component ) { - vnode._dom = vnode._component.base = newDom; + vnode._dom = newDom; + if (vnode._component) { + vnode._component.base = newDom; + } } } } From e928a90e8816bc44a4da3bf3e31ce847df964097 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Tue, 11 Jun 2019 14:41:48 -0700 Subject: [PATCH 12/21] Add more failing tests for c.base --- test/browser/components.test.js | 520 +++++++++++++++++++++++++++++++- test/polyfills.js | 4 +- 2 files changed, 520 insertions(+), 4 deletions(-) diff --git a/test/browser/components.test.js b/test/browser/components.test.js index 3bdcac7fed..3ae82ffbc7 100644 --- a/test/browser/components.test.js +++ b/test/browser/components.test.js @@ -1,6 +1,7 @@ import { createElement as h, render, Component, Fragment } from '../../src/index'; import { setupRerender } from 'preact/test-utils'; import { setupScratch, teardown, getMixedArray, mixedArrayHTML, serializeHtml } from '../_util/helpers'; +import { div, span } from '../_util/dom'; /** @jsx h */ @@ -1805,7 +1806,7 @@ describe('Components', () => { ), scratch); expect(scratch.innerHTML).to.equal('

        Hello

        '); - expect(nullInst.base).to.equal(undefined); + expect(nullInst.base).to.equalNode(null); expect(child.base).to.equalNode(scratch.firstChild); expect(sibling.base).to.equalNode(scratch.lastChild); expect(parent1.base).to.equalNode(child.base); @@ -1814,7 +1815,7 @@ describe('Components', () => { rerender(); expect(scratch.innerHTML).to.equal('Hello

        '); - expect(nullInst.base).to.equal(undefined); + expect(nullInst.base).to.equalNode(null); expect(child.base).to.equalNode(scratch.firstChild); expect(sibling.base).to.equalNode(scratch.lastChild); expect(parent1.base).to.equalNode(child.base); @@ -1888,4 +1889,519 @@ describe('Components', () => { expect(parent1.base).to.equalNode(sibling.base); expect(parentDom1.base).to.equalNode(scratch.firstChild); }); + + it('should update parent c.base if first child becomes null', () => { + /* eslint-disable lines-around-comment */ + + /** @type {import('../../src').Component} */ + let maybe; + /** @type {import('../../src').Component} */ + let child; + /** @type {import('../../src').Component} */ + let parent; + + /** @type {() => void} */ + let toggleMaybeNull; + /** @type {() => void} */ + let swapChildTag1; + + class MaybeNull extends Component { + constructor(props) { + super(props); + maybe = this; + this.state = { active: true }; + toggleMaybeNull = () => this.setState(prev => ({ + active: !prev.active + })); + } + render() { + return this.state.active ?
        maybe
        : null; + } + } + + class Child extends Component { + constructor(props) { + super(props); + child = this; + this.state = { tagName: 'div' }; + swapChildTag1 = () => this.setState(prev => ({ + tagName: prev.tagName == 'div' ? 'span' : 'div' + })); + + } + render() { + return h(this.state.tagName, null, 'child1'); + } + } + + function Parent(props) { + parent = this; + return props.children; + } + + render(( + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal([ + div('maybe'), + div('child1') + ].join('')); + expect(maybe.base).to.equalNode(scratch.firstChild, 'initial - maybe.base'); + expect(child.base).to.equalNode(scratch.lastChild, 'initial - child.base'); + expect(parent.base).to.equalNode(maybe.base, 'initial - parent.base'); + + toggleMaybeNull(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + div('child1') + ].join('')); + expect(maybe.base).to.equalNode(null, 'toggleMaybe1 - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'toggleMaybe1 - child.base'); + expect(parent.base).to.equalNode(child.base, 'toggleMaybe1 - parent.base'); + + swapChildTag1(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + span('child1') + ].join('')); + expect(maybe.base).to.equalNode(null, 'swapChild1 - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'swapChild1 - child.base'); + expect(parent.base).to.equalNode(child.base, 'swapChild1 - parent.base'); + }); + + it('should update parent c.base if first child becomes non-null', () => { + /* eslint-disable lines-around-comment */ + + /** @type {import('../../src').Component} */ + let maybe; + /** @type {import('../../src').Component} */ + let child; + /** @type {import('../../src').Component} */ + let parent; + + /** @type {() => void} */ + let toggleMaybeNull; + /** @type {() => void} */ + let swapChildTag1; + + class MaybeNull extends Component { + constructor(props) { + super(props); + maybe = this; + this.state = { active: false }; + toggleMaybeNull = () => this.setState(prev => ({ + active: !prev.active + })); + } + render() { + return this.state.active ?
        maybe
        : null; + } + } + + class Child extends Component { + constructor(props) { + super(props); + child = this; + this.state = { tagName: 'div' }; + swapChildTag1 = () => this.setState(prev => ({ + tagName: prev.tagName == 'div' ? 'span' : 'div' + })); + + } + render() { + return h(this.state.tagName, null, 'child1'); + } + } + + function Parent(props) { + parent = this; + return props.children; + } + + render(( + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal([ + div('child1') + ].join('')); + expect(maybe.base).to.equalNode(null, 'toggleMaybe1 - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'toggleMaybe1 - child.base'); + expect(parent.base).to.equalNode(child.base, 'toggleMaybe1 - parent.base'); + + swapChildTag1(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + span('child1') + ].join('')); + expect(maybe.base).to.equalNode(null, 'swapChild1 - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'swapChild1 - child.base'); + expect(parent.base).to.equalNode(child.base, 'swapChild1 - parent.base'); + + toggleMaybeNull(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + div('maybe'), + span('child1') + ].join('')); + expect(maybe.base).to.equalNode(scratch.firstChild, 'toggleMaybe2 - maybe.base'); + expect(child.base).to.equalNode(scratch.lastChild, 'toggleMaybe2 - child.base'); + expect(parent.base).to.equalNode(maybe.base, 'toggleMaybe2 - parent.base'); + }); + + it('should update parent c.base if first non-null child becomes null with multiple null siblings', () => { + /* eslint-disable lines-around-comment */ + + /** @type {import('../../src').Component} */ + let maybe; + /** @type {import('../../src').Component} */ + let child; + /** @type {import('../../src').Component} */ + let parent; + + /** @type {() => void} */ + let toggleMaybeNull; + /** @type {() => void} */ + let swapChildTag1; + + const Null = () => null; + + class MaybeNull extends Component { + constructor(props) { + super(props); + maybe = this; + this.state = { active: true }; + toggleMaybeNull = () => this.setState(prev => ({ + active: !prev.active + })); + } + render() { + return this.state.active ?
        maybe
        : null; + } + } + + class Child extends Component { + constructor(props) { + super(props); + child = this; + this.state = { tagName: 'div' }; + swapChildTag1 = () => this.setState(prev => ({ + tagName: prev.tagName == 'div' ? 'span' : 'div' + })); + + } + render() { + return h(this.state.tagName, null, 'child1'); + } + } + + function Parent(props) { + parent = this; + return props.children; + } + + render(( + + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal([ + div('maybe'), + div('child1') + ].join('')); + expect(maybe.base).to.equalNode(scratch.firstChild, 'initial - maybe.base'); + expect(child.base).to.equalNode(scratch.lastChild, 'initial - child.base'); + expect(parent.base).to.equalNode(maybe.base, 'initial - parent.base'); + + toggleMaybeNull(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + div('child1') + ].join('')); + expect(maybe.base).to.equalNode(null, 'toggleMaybe1 - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'toggleMaybe1 - child.base'); + expect(parent.base).to.equalNode(child.base, 'toggleMaybe1 - parent.base'); + + swapChildTag1(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + span('child1') + ].join('')); + expect(maybe.base).to.equalNode(null, 'swapChild1 - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'swapChild1 - child.base'); + expect(parent.base).to.equalNode(child.base, 'swapChild1 - parent.base'); + }); + + it('should update parent c.base if a null child returns DOM with multiple null siblings', () => { + /* eslint-disable lines-around-comment */ + + /** @type {import('../../src').Component} */ + let maybe; + /** @type {import('../../src').Component} */ + let child; + /** @type {import('../../src').Component} */ + let parent; + + /** @type {() => void} */ + let toggleMaybeNull; + /** @type {() => void} */ + let swapChildTag1; + + const Null = () => null; + + class MaybeNull extends Component { + constructor(props) { + super(props); + maybe = this; + this.state = { active: false }; + toggleMaybeNull = () => this.setState(prev => ({ + active: !prev.active + })); + } + render() { + return this.state.active ?
        maybe
        : null; + } + } + + class Child extends Component { + constructor(props) { + super(props); + child = this; + this.state = { tagName: 'div' }; + swapChildTag1 = () => this.setState(prev => ({ + tagName: prev.tagName == 'div' ? 'span' : 'div' + })); + + } + render() { + return h(this.state.tagName, null, 'child1'); + } + } + + function Parent(props) { + parent = this; + return props.children; + } + + render(( + + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal([ + div('child1') + ].join('')); + expect(maybe.base, 'initial - maybe.base').to.equalNode(null); + expect(child.base, 'initial - child.base').to.equalNode(scratch.firstChild); + expect(parent.base, 'initial - parent.base').to.equalNode(child.base); + + swapChildTag1(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + span('child1') + ].join('')); + expect(maybe.base, 'swapChild1 - maybe.base').to.equalNode(null); + expect(child.base, 'swapChild1 - child.base').to.equalNode(scratch.firstChild); + expect(parent.base, 'swapChild1 - parent.base').to.equalNode(child.base); + + toggleMaybeNull(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + div('maybe'), + span('child1') + ].join('')); + expect(maybe.base, 'toggleMaybe2 - maybe.base').to.equalNode(scratch.firstChild); + expect(child.base, 'toggleMaybe2 - child.base').to.equalNode(scratch.lastChild); + expect(parent.base, 'toggleMaybe2 - parent.base').to.equalNode(maybe.base); + }); + + it('should update parent c.base to null if last child becomes null', () => { + /* eslint-disable lines-around-comment */ + + /** @type {import('../../src').Component} */ + let maybe; + /** @type {import('../../src').Component} */ + let child; + /** @type {import('../../src').Component} */ + let parent; + + /** @type {() => void} */ + let toggleMaybeNull; + + const Null = () => null; + + class MaybeNull extends Component { + constructor(props) { + super(props); + maybe = this; + this.state = { active: true }; + toggleMaybeNull = () => this.setState(prev => ({ + active: !prev.active + })); + } + render() { + return this.state.active ?
        maybe
        : null; + } + } + + class Child extends Component { + constructor(props) { + super(props); + child = this; + this.state = { tagName: 'div' }; + + } + render() { + return h(this.state.tagName, null, 'child1'); + } + } + + function Parent(props) { + parent = this; + return props.children; + } + + render(( + + + + + + + + + + + + ), scratch); + + + expect(scratch.innerHTML).to.equal([ + div('maybe'), + div('child1') + ].join('')); + expect(maybe.base, 'initial - maybe.base').to.equalNode(scratch.firstChild); + expect(child.base, 'initial - child.base').to.equalNode(scratch.lastChild); + expect(parent.base, 'initial - parent.base').to.equalNode(maybe.base); + + toggleMaybeNull(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + div('child1') + ].join('')); + expect(maybe.base, 'toggleMaybe1 - maybe.base').to.equalNode(null); + expect(child.base, 'toggleMaybe1 - child.base').to.equalNode(scratch.firstChild); + expect(parent.base, 'toggleMaybe1 - parent.base').to.equalNode(maybe.base); + }); + + it('should update parent c.base if last child returns dom', () => { + /* eslint-disable lines-around-comment */ + + /** @type {import('../../src').Component} */ + let maybe; + /** @type {import('../../src').Component} */ + let child; + /** @type {import('../../src').Component} */ + let parent; + + /** @type {() => void} */ + let toggleMaybeNull; + + const Null = () => null; + + class MaybeNull extends Component { + constructor(props) { + super(props); + maybe = this; + this.state = { active: false }; + toggleMaybeNull = () => this.setState(prev => ({ + active: !prev.active + })); + } + render() { + return this.state.active ?
        maybe
        : null; + } + } + + class Child extends Component { + constructor(props) { + super(props); + child = this; + this.state = { tagName: 'div' }; + + } + render() { + return h(this.state.tagName, null, 'child1'); + } + } + + function Parent(props) { + parent = this; + return props.children; + } + + render(( + + + + + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal([ + div('child1') + ].join('')); + expect(maybe.base, 'toggleMaybe1 - maybe.base').to.equalNode(null); + expect(child.base, 'toggleMaybe1 - child.base').to.equalNode(scratch.firstChild); + expect(parent.base, 'toggleMaybe1 - parent.base').to.equalNode(maybe.base); + + toggleMaybeNull(); + rerender(); + + expect(scratch.innerHTML).to.equal([ + div('maybe'), + div('child1') + ].join('')); + expect(maybe.base, 'initial - maybe.base').to.equalNode(scratch.firstChild); + expect(child.base, 'initial - child.base').to.equalNode(scratch.lastChild); + expect(parent.base, 'initial - parent.base').to.equalNode(maybe.base); + }); }); diff --git a/test/polyfills.js b/test/polyfills.js index b58c943d56..ee0d46fc12 100644 --- a/test/polyfills.js +++ b/test/polyfills.js @@ -31,10 +31,10 @@ chai.use((chai, util) => { message = message || 'equalNode'; if (expectedNode == null) { - new Assertion(obj).to.equal(expectedNode); + new Assertion(obj, message).to.not.exist; } else { - new Assertion(obj).to.be.instanceof(Node); + new Assertion(obj).to.be.instanceof(Node, message); // new Assertion(obj).to.have.property('tagName', expectedNode.tagName); this.assert( obj.tagName === expectedNode.tagName, From 36326c2656036913efa25e3a282491e72a7c961e Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Tue, 11 Jun 2019 17:37:42 -0700 Subject: [PATCH 13/21] Experiment with findDomNode implementation of c.base --- src/component.js | 113 ++++++++++++++++++++++++++++++++++++++++------ src/diff/index.js | 5 +- 2 files changed, 102 insertions(+), 16 deletions(-) diff --git a/src/component.js b/src/component.js index cbef9444f5..724246ac47 100644 --- a/src/component.js +++ b/src/component.js @@ -69,22 +69,26 @@ Component.prototype.forceUpdate = function(callback) { const force = callback!==false; let mounts = []; - let newDom = diff(parentDom, vnode, assign({}, vnode), this._context, parentDom.ownerSVGElement!==undefined, null, mounts, force, oldDom == null ? getDomSibling(vnode) : oldDom); + diff(parentDom, vnode, assign({}, vnode), this._context, parentDom.ownerSVGElement!==undefined, null, mounts, force, (oldDom = findDomNode(vnode)) == null ? getDomSibling(vnode) : oldDom); commitRoot(mounts, vnode); - if (newDom != oldDom) { - // Update parent component's _dom and c.base pointers - while ( - vnode._parent - && vnode._parent._dom === oldDom - && (vnode = vnode._parent) - ) { - vnode._dom = newDom; - if (vnode._component) { - vnode._component.base = newDom; - } - } - } + // if (newDom != oldDom) { + // if (newDom == null && vnode._parent._dom == oldDom) { + // newDom = getDomSibling(vnode); + // } + + // // Update parent component's _dom and c.base pointers + // while ( + // vnode._parent + // && vnode._parent._dom === oldDom + // && (vnode = vnode._parent) + // ) { + // vnode._dom = newDom; + // if (vnode._component) { + // vnode._component.base = newDom; + // } + // } + // } } if (callback) callback(); }; @@ -132,6 +136,87 @@ export function getDomSibling(vnode, childIndex) { return typeof vnode.type === 'function' ? getDomSibling(vnode) : null; } +Object.defineProperty(Component.prototype, 'base', { + get() { + return findDomNode(this._vnode); + } +}); + +/** + * @param {import('./internal').VNode | null} vnode + */ +function findDomNode(vnode) { + // // 3489 B + // if (vnode == null) { + // return null; + // } + // else if (typeof vnode.type !== 'function') { + // return vnode._dom; + // } + // let dom; + // for (let i = 0; i < vnode._children.length; i++) { + // dom = findDomNode(vnode._children[i]); + // if (dom) { + // return dom; + // } + // } + + // // 3489 B + // if (vnode == null) { + // return null; + // } + // else if (typeof vnode.type !== 'function') { + // return vnode._dom; + // } + // let dom; + // for (let i = 0; !dom && i < vnode._children.length; i++) { + // dom = findDomNode(vnode._children[i]); + // } + // return dom; + + // 3489 B + if (vnode == null) { + return null; + } + if (vnode._component == null) { + return vnode._dom; + } + let dom; + for (let i = 0; !dom && i < vnode._children.length; i++) { + dom = findDomNode(vnode._children[i]); + } + return dom; + + // // 3489 B + // if (vnode == null) { + // return null; + // } + // else if (typeof vnode.type !== 'function') { + // return vnode._dom; + // } + // let dom; + // for (let i = 0; !dom && i < vnode._children.length; dom = findDomNode(vnode._children[i++])) {} + // return dom; + + // // 3486 B + // if (vnode != null) { + // return typeof vnode.type !== 'function' ? vnode._dom : vnode._children.reduce((dom, child) => { + // return dom ? dom : findDomNode(child); + // }, null); + // } + + // // 3494 B + // if (vnode != null) { + // let dom = typeof vnode.type !== 'function' && vnode._dom; + // if (!dom) { + // for (let i = 0; !dom && i < vnode._children.length; i++) { + // dom = findDomNode(vnode._children[i]); + // } + // } + // return dom; + // } +} + /** * The render queue * @type {Array} diff --git a/src/diff/index.js b/src/diff/index.js index 762e7869af..7e377eae70 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -130,7 +130,7 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi diffChildren(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, oldDom); // Only change the fields on the component once they represent the new state of the DOM - c.base = newVNode._dom; + // c.base = newVNode._dom; c._vnode = newVNode; c._parentDom = parentDom; @@ -296,7 +296,8 @@ export function unmount(vnode, parentVNode, skipRemove) { } } - r.base = r._parentDom = null; + // r.base = r._parentDom = null; + r._parentDom = null; } if (r = vnode._children) { From 8568bd046e2d82b66d6b0e94a170a70e785cb63f Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Tue, 11 Jun 2019 17:37:52 -0700 Subject: [PATCH 14/21] Revert "Experiment with findDomNode implementation of c.base" This reverts commit 36326c2656036913efa25e3a282491e72a7c961e. --- src/component.js | 113 ++++++---------------------------------------- src/diff/index.js | 5 +- 2 files changed, 16 insertions(+), 102 deletions(-) diff --git a/src/component.js b/src/component.js index 724246ac47..cbef9444f5 100644 --- a/src/component.js +++ b/src/component.js @@ -69,26 +69,22 @@ Component.prototype.forceUpdate = function(callback) { const force = callback!==false; let mounts = []; - diff(parentDom, vnode, assign({}, vnode), this._context, parentDom.ownerSVGElement!==undefined, null, mounts, force, (oldDom = findDomNode(vnode)) == null ? getDomSibling(vnode) : oldDom); + let newDom = diff(parentDom, vnode, assign({}, vnode), this._context, parentDom.ownerSVGElement!==undefined, null, mounts, force, oldDom == null ? getDomSibling(vnode) : oldDom); commitRoot(mounts, vnode); - // if (newDom != oldDom) { - // if (newDom == null && vnode._parent._dom == oldDom) { - // newDom = getDomSibling(vnode); - // } - - // // Update parent component's _dom and c.base pointers - // while ( - // vnode._parent - // && vnode._parent._dom === oldDom - // && (vnode = vnode._parent) - // ) { - // vnode._dom = newDom; - // if (vnode._component) { - // vnode._component.base = newDom; - // } - // } - // } + if (newDom != oldDom) { + // Update parent component's _dom and c.base pointers + while ( + vnode._parent + && vnode._parent._dom === oldDom + && (vnode = vnode._parent) + ) { + vnode._dom = newDom; + if (vnode._component) { + vnode._component.base = newDom; + } + } + } } if (callback) callback(); }; @@ -136,87 +132,6 @@ export function getDomSibling(vnode, childIndex) { return typeof vnode.type === 'function' ? getDomSibling(vnode) : null; } -Object.defineProperty(Component.prototype, 'base', { - get() { - return findDomNode(this._vnode); - } -}); - -/** - * @param {import('./internal').VNode | null} vnode - */ -function findDomNode(vnode) { - // // 3489 B - // if (vnode == null) { - // return null; - // } - // else if (typeof vnode.type !== 'function') { - // return vnode._dom; - // } - // let dom; - // for (let i = 0; i < vnode._children.length; i++) { - // dom = findDomNode(vnode._children[i]); - // if (dom) { - // return dom; - // } - // } - - // // 3489 B - // if (vnode == null) { - // return null; - // } - // else if (typeof vnode.type !== 'function') { - // return vnode._dom; - // } - // let dom; - // for (let i = 0; !dom && i < vnode._children.length; i++) { - // dom = findDomNode(vnode._children[i]); - // } - // return dom; - - // 3489 B - if (vnode == null) { - return null; - } - if (vnode._component == null) { - return vnode._dom; - } - let dom; - for (let i = 0; !dom && i < vnode._children.length; i++) { - dom = findDomNode(vnode._children[i]); - } - return dom; - - // // 3489 B - // if (vnode == null) { - // return null; - // } - // else if (typeof vnode.type !== 'function') { - // return vnode._dom; - // } - // let dom; - // for (let i = 0; !dom && i < vnode._children.length; dom = findDomNode(vnode._children[i++])) {} - // return dom; - - // // 3486 B - // if (vnode != null) { - // return typeof vnode.type !== 'function' ? vnode._dom : vnode._children.reduce((dom, child) => { - // return dom ? dom : findDomNode(child); - // }, null); - // } - - // // 3494 B - // if (vnode != null) { - // let dom = typeof vnode.type !== 'function' && vnode._dom; - // if (!dom) { - // for (let i = 0; !dom && i < vnode._children.length; i++) { - // dom = findDomNode(vnode._children[i]); - // } - // } - // return dom; - // } -} - /** * The render queue * @type {Array} diff --git a/src/diff/index.js b/src/diff/index.js index 7e377eae70..762e7869af 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -130,7 +130,7 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi diffChildren(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, oldDom); // Only change the fields on the component once they represent the new state of the DOM - // c.base = newVNode._dom; + c.base = newVNode._dom; c._vnode = newVNode; c._parentDom = parentDom; @@ -296,8 +296,7 @@ export function unmount(vnode, parentVNode, skipRemove) { } } - // r.base = r._parentDom = null; - r._parentDom = null; + r.base = r._parentDom = null; } if (r = vnode._children) { From 2eaf2084e84d431361b405550c02bec9c2d2c64f Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 12 Jun 2019 12:33:02 -0700 Subject: [PATCH 15/21] Reorganize c.base tests into common describe and improve last couple tests --- test/_util/dom.js | 6 + test/browser/components.test.js | 1113 +++++++++++-------------------- 2 files changed, 390 insertions(+), 729 deletions(-) diff --git a/test/_util/dom.js b/test/_util/dom.js index ad01b8aaab..1d68abe872 100644 --- a/test/_util/dom.js +++ b/test/_util/dom.js @@ -10,6 +10,12 @@ export const span = contents => `${contents}`; */ export const div = contents => `
        ${contents}
        `; +/** + * A helper to generate innerHTML validation strings containing p + * @param {string | number} contents The contents of the p, as a string + */ +export const p = contents => `

        ${contents}

        `; + /** * A helper to generate innerHTML validation strings containing sections * @param {string | number} contents The contents of the section, as a string diff --git a/test/browser/components.test.js b/test/browser/components.test.js index 3ae82ffbc7..3a401577f3 100644 --- a/test/browser/components.test.js +++ b/test/browser/components.test.js @@ -1,7 +1,7 @@ import { createElement as h, render, Component, Fragment } from '../../src/index'; import { setupRerender } from 'preact/test-utils'; import { setupScratch, teardown, getMixedArray, mixedArrayHTML, serializeHtml } from '../_util/helpers'; -import { div, span } from '../_util/dom'; +import { div, span, p } from '../_util/dom'; /** @jsx h */ @@ -1575,84 +1575,33 @@ describe('Components', () => { expect(unmounted).to.equal(',0,1,2,3'); }); - it('should keep c.base up to date if a nested child component changes DOM nodes', () => { + describe('c.base', () => { + /* eslint-disable lines-around-comment */ + /** @type {import('../../src').Component} */ let parentDom1; + /** @type {import('../../src').Component} */ let parent1; + /** @type {import('../../src').Component} */ let parent2; + /** @type {import('../../src').Component} */ + let maybe; + /** @type {import('../../src').Component} */ let child; + /** @type {import('../../src').Component} */ + let sibling; + /** @type {import('../../src').Component} */ + let nullInst; - class Child extends Component { - constructor(props, context) { - super(props, context); - child = this; - this.state = { tagName: 'p' }; - } - render() { - return h(this.state.tagName, {}, 'Hello'); - } - } - - class Parent1 extends Component { - render() { - parent1 = this; - return this.props.children; - } - } - - function Parent2(props) { - parent2 = this; - return props.children; - } + /** @type {() => void} */ + let toggleMaybeNull; + /** @type {() => void} */ + let swapChildTag; function ParentWithDom(props) { parentDom1 = this; return
        {props.children}
        ; } - render(( - - - - - - - - ), scratch); - - expect(scratch.innerHTML).to.equal('

        Hello

        '); - expect(child.base).to.equalNode(scratch.firstChild.firstChild); - expect(parent2.base).to.equalNode(child.base); - expect(parent1.base).to.equalNode(child.base); - expect(parentDom1.base).to.equalNode(scratch.firstChild); - - child.setState({ tagName: 'span' }); - rerender(); - - expect(scratch.innerHTML).to.equal('
        Hello
        '); - expect(child.base).to.equalNode(scratch.firstChild.firstChild); - expect(parent2.base).to.equalNode(child.base); - expect(parent1.base).to.equalNode(child.base); - expect(parentDom1.base).to.equalNode(scratch.firstChild); - }); - - it('should not update sibling c.base if child component changes DOM nodes', () => { - let parentDom1; - let parent1; - let parent2; - let child; - let s1 = {}, s2 = {}, s3 = {}, s4 = {}; - - class Child extends Component { - constructor(props, context) { - super(props, context); - child = this; - this.state = { tagName: 'p' }; - } - render() { - return h(this.state.tagName, {}, 'Hello'); - } - } - class Parent1 extends Component { render() { parent1 = this; @@ -1665,251 +1614,11 @@ describe('Components', () => { return props.children; } - function ParentWithDom(props) { - parentDom1 = this; - return
        {props.children}
        ; - } - - function Sibling(props) { - return

        ; - } - - render(( - - - - - - - - - - - - - - ), scratch); - - expect(scratch.innerHTML).to.equal('

        Hello

        '); - expect(child.base).to.equalNode(scratch.firstChild.firstChild); - expect(parent2.base).to.equalNode(child.base); - expect(parent1.base).to.equalNode(child.base); - expect(parentDom1.base).to.equalNode(scratch.firstChild); - expect(s1.current.base).to.equalNode(scratch.firstChild.childNodes[1]); - expect(s2.current.base).to.equalNode(scratch.firstChild.childNodes[2]); - expect(s3.current.base).to.equalNode(scratch.firstChild.childNodes[3]); - expect(s4.current.base).to.equalNode(scratch.lastChild); - - child.setState({ tagName: 'span' }); - rerender(); - - expect(scratch.innerHTML).to.equal('
        Hello

        '); - expect(child.base).to.equalNode(scratch.firstChild.firstChild); - expect(parent2.base).to.equalNode(child.base); - expect(parent1.base).to.equalNode(child.base); - expect(parentDom1.base).to.equalNode(scratch.firstChild); - expect(s1.current.base).to.equalNode(scratch.firstChild.childNodes[1]); - expect(s2.current.base).to.equalNode(scratch.firstChild.childNodes[2]); - expect(s3.current.base).to.equalNode(scratch.firstChild.childNodes[3]); - expect(s4.current.base).to.equalNode(scratch.lastChild); - }); - - it('should not update parent c.base if child component changes DOM nodes and it is not first child component', () => { - let parent1; - let child; - let sibling; - - class Child extends Component { - constructor(props, context) { - super(props, context); - child = this; - this.state = { tagName: 'p' }; - } - render() { - return h(this.state.tagName, {}, 'Hello'); - } - } - - class Parent1 extends Component { - render() { - parent1 = this; - return this.props.children; - } - } - - function Sibling(props) { - sibling = this; - return

        ; - } - - render(( - - - - - ), scratch); - - expect(scratch.innerHTML).to.equal('

        Hello

        '); - expect(child.base).to.equalNode(scratch.lastChild); - expect(sibling.base).to.equalNode(scratch.firstChild); - expect(parent1.base).to.equalNode(sibling.base); - - child.setState({ tagName: 'span' }); - rerender(); - - expect(scratch.innerHTML).to.equal('

        Hello'); - expect(child.base).to.equalNode(scratch.lastChild); - expect(sibling.base).to.equalNode(scratch.firstChild); - expect(parent1.base).to.equalNode(sibling.base); - }); - - it('should update parent c.base if child component changes DOM nodes and it is first non-null child component', () => { - let parent1; - let child; - let sibling; - let nullInst; - - class Child extends Component { - constructor(props, context) { - super(props, context); - child = this; - this.state = { tagName: 'p' }; - } - render() { - return h(this.state.tagName, {}, 'Hello'); - } - } - - class Parent1 extends Component { - render() { - parent1 = this; - return this.props.children; - } - } - - function Sibling(props) { - sibling = this; - return

        ; - } - - function Null() { - nullInst = this; - return null; - } - - render(( - - - - - - ), scratch); - - expect(scratch.innerHTML).to.equal('

        Hello

        '); - expect(nullInst.base).to.equalNode(null); - expect(child.base).to.equalNode(scratch.firstChild); - expect(sibling.base).to.equalNode(scratch.lastChild); - expect(parent1.base).to.equalNode(child.base); - - child.setState({ tagName: 'span' }); - rerender(); - - expect(scratch.innerHTML).to.equal('Hello

        '); - expect(nullInst.base).to.equalNode(null); - expect(child.base).to.equalNode(scratch.firstChild); - expect(sibling.base).to.equalNode(scratch.lastChild); - expect(parent1.base).to.equalNode(child.base); - }); - - it('should not update parent c.base if child component changes DOM nodes and a parent is not first child component', () => { - let parentDom1; - let parent1; - let parent2; - let child; - let sibling; - - class Child extends Component { - constructor(props, context) { - super(props, context); - child = this; - this.state = { tagName: 'p' }; - } - render() { - return h(this.state.tagName, {}, 'Hello'); - } - } - - class Parent1 extends Component { - render() { - parent1 = this; - return this.props.children; - } - } - - function Parent2(props) { - parent2 = this; - return props.children; - } - - function ParentWithDom(props) { - parentDom1 = this; - return
        {props.children}
        ; - } - - function Sibling(props) { - sibling = this; - return

        ; - } - - render(( - - - - - - - - - ), scratch); - - expect(scratch.innerHTML).to.equal('

        Hello

        '); - expect(child.base).to.equalNode(scratch.firstChild.lastChild); - expect(parent2.base).to.equalNode(child.base); - expect(sibling.base).to.equalNode(scratch.firstChild.firstChild); - expect(parent1.base).to.equalNode(sibling.base); - expect(parentDom1.base).to.equalNode(scratch.firstChild); - - child.setState({ tagName: 'span' }); - rerender(); - - expect(scratch.innerHTML).to.equal('

        Hello
        '); - expect(child.base).to.equalNode(scratch.firstChild.lastChild); - expect(parent2.base).to.equalNode(child.base); - expect(sibling.base).to.equalNode(scratch.firstChild.firstChild); - expect(parent1.base).to.equalNode(sibling.base); - expect(parentDom1.base).to.equalNode(scratch.firstChild); - }); - - it('should update parent c.base if first child becomes null', () => { - /* eslint-disable lines-around-comment */ - - /** @type {import('../../src').Component} */ - let maybe; - /** @type {import('../../src').Component} */ - let child; - /** @type {import('../../src').Component} */ - let parent; - - /** @type {() => void} */ - let toggleMaybeNull; - /** @type {() => void} */ - let swapChildTag1; - class MaybeNull extends Component { constructor(props) { super(props); maybe = this; - this.state = { active: true }; + this.state = { active: props.active || false }; toggleMaybeNull = () => this.setState(prev => ({ active: !prev.active })); @@ -1923,485 +1632,431 @@ describe('Components', () => { constructor(props) { super(props); child = this; - this.state = { tagName: 'div' }; - swapChildTag1 = () => this.setState(prev => ({ - tagName: prev.tagName == 'div' ? 'span' : 'div' + this.state = { tagName: 'p' }; + swapChildTag = () => this.setState(prev => ({ + tagName: prev.tagName == 'p' ? 'span' : 'p' })); } render() { - return h(this.state.tagName, null, 'child1'); - } - } - - function Parent(props) { - parent = this; - return props.children; - } - - render(( - - - - - - - ), scratch); - - expect(scratch.innerHTML).to.equal([ - div('maybe'), - div('child1') - ].join('')); - expect(maybe.base).to.equalNode(scratch.firstChild, 'initial - maybe.base'); - expect(child.base).to.equalNode(scratch.lastChild, 'initial - child.base'); - expect(parent.base).to.equalNode(maybe.base, 'initial - parent.base'); - - toggleMaybeNull(); - rerender(); - - expect(scratch.innerHTML).to.equal([ - div('child1') - ].join('')); - expect(maybe.base).to.equalNode(null, 'toggleMaybe1 - maybe.base'); - expect(child.base).to.equalNode(scratch.firstChild, 'toggleMaybe1 - child.base'); - expect(parent.base).to.equalNode(child.base, 'toggleMaybe1 - parent.base'); - - swapChildTag1(); - rerender(); - - expect(scratch.innerHTML).to.equal([ - span('child1') - ].join('')); - expect(maybe.base).to.equalNode(null, 'swapChild1 - maybe.base'); - expect(child.base).to.equalNode(scratch.firstChild, 'swapChild1 - child.base'); - expect(parent.base).to.equalNode(child.base, 'swapChild1 - parent.base'); - }); - - it('should update parent c.base if first child becomes non-null', () => { - /* eslint-disable lines-around-comment */ - - /** @type {import('../../src').Component} */ - let maybe; - /** @type {import('../../src').Component} */ - let child; - /** @type {import('../../src').Component} */ - let parent; - - /** @type {() => void} */ - let toggleMaybeNull; - /** @type {() => void} */ - let swapChildTag1; - - class MaybeNull extends Component { - constructor(props) { - super(props); - maybe = this; - this.state = { active: false }; - toggleMaybeNull = () => this.setState(prev => ({ - active: !prev.active - })); - } - render() { - return this.state.active ?
        maybe
        : null; + return h(this.state.tagName, null, 'child'); } } - class Child extends Component { - constructor(props) { - super(props); - child = this; - this.state = { tagName: 'div' }; - swapChildTag1 = () => this.setState(prev => ({ - tagName: prev.tagName == 'div' ? 'span' : 'div' - })); - } - render() { - return h(this.state.tagName, null, 'child1'); - } + function Sibling(props) { + sibling = this; + return

        ; } - function Parent(props) { - parent = this; - return props.children; + function Null() { + nullInst = this; + return null; } - render(( - - - - - - - ), scratch); - - expect(scratch.innerHTML).to.equal([ - div('child1') - ].join('')); - expect(maybe.base).to.equalNode(null, 'toggleMaybe1 - maybe.base'); - expect(child.base).to.equalNode(scratch.firstChild, 'toggleMaybe1 - child.base'); - expect(parent.base).to.equalNode(child.base, 'toggleMaybe1 - parent.base'); - - swapChildTag1(); - rerender(); + afterEach(() => { + parentDom1 = null; + parent1 = null; + parent2 = null; + child = null; + sibling = null; + }); - expect(scratch.innerHTML).to.equal([ - span('child1') - ].join('')); - expect(maybe.base).to.equalNode(null, 'swapChild1 - maybe.base'); - expect(child.base).to.equalNode(scratch.firstChild, 'swapChild1 - child.base'); - expect(parent.base).to.equalNode(child.base, 'swapChild1 - parent.base'); + it('should keep c.base up to date if a nested child component changes DOM nodes', () => { + render(( + + + + + + + + ), scratch); - toggleMaybeNull(); - rerender(); + expect(scratch.innerHTML).to.equal('

        child

        '); + expect(child.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent2.base).to.equalNode(child.base); + expect(parent1.base).to.equalNode(child.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); - expect(scratch.innerHTML).to.equal([ - div('maybe'), - span('child1') - ].join('')); - expect(maybe.base).to.equalNode(scratch.firstChild, 'toggleMaybe2 - maybe.base'); - expect(child.base).to.equalNode(scratch.lastChild, 'toggleMaybe2 - child.base'); - expect(parent.base).to.equalNode(maybe.base, 'toggleMaybe2 - parent.base'); - }); + swapChildTag(); + rerender(); - it('should update parent c.base if first non-null child becomes null with multiple null siblings', () => { - /* eslint-disable lines-around-comment */ + expect(scratch.innerHTML).to.equal('
        child
        '); + expect(child.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent2.base).to.equalNode(child.base); + expect(parent1.base).to.equalNode(child.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); + }); - /** @type {import('../../src').Component} */ - let maybe; - /** @type {import('../../src').Component} */ - let child; - /** @type {import('../../src').Component} */ - let parent; + it('should not update sibling c.base if child component changes DOM nodes', () => { + let s1 = {}, s2 = {}, s3 = {}, s4 = {}; - /** @type {() => void} */ - let toggleMaybeNull; - /** @type {() => void} */ - let swapChildTag1; + render(( + + + + + + + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal('

        child

        '); + expect(child.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent2.base).to.equalNode(child.base); + expect(parent1.base).to.equalNode(child.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); + expect(s1.current.base).to.equalNode(scratch.firstChild.childNodes[1]); + expect(s2.current.base).to.equalNode(scratch.firstChild.childNodes[2]); + expect(s3.current.base).to.equalNode(scratch.firstChild.childNodes[3]); + expect(s4.current.base).to.equalNode(scratch.lastChild); + + swapChildTag(); + rerender(); - const Null = () => null; + expect(scratch.innerHTML).to.equal('
        child

        '); + expect(child.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent2.base).to.equalNode(child.base); + expect(parent1.base).to.equalNode(child.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); + expect(s1.current.base).to.equalNode(scratch.firstChild.childNodes[1]); + expect(s2.current.base).to.equalNode(scratch.firstChild.childNodes[2]); + expect(s3.current.base).to.equalNode(scratch.firstChild.childNodes[3]); + expect(s4.current.base).to.equalNode(scratch.lastChild); + }); - class MaybeNull extends Component { - constructor(props) { - super(props); - maybe = this; - this.state = { active: true }; - toggleMaybeNull = () => this.setState(prev => ({ - active: !prev.active - })); - } - render() { - return this.state.active ?
        maybe
        : null; - } - } + it('should not update parent c.base if child component changes DOM nodes and it is not first child component', () => { + render(( + + + + + ), scratch); - class Child extends Component { - constructor(props) { - super(props); - child = this; - this.state = { tagName: 'div' }; - swapChildTag1 = () => this.setState(prev => ({ - tagName: prev.tagName == 'div' ? 'span' : 'div' - })); + expect(scratch.innerHTML).to.equal('

        child

        '); + expect(child.base).to.equalNode(scratch.lastChild); + expect(sibling.base).to.equalNode(scratch.firstChild); + expect(parent1.base).to.equalNode(sibling.base); - } - render() { - return h(this.state.tagName, null, 'child1'); - } - } + swapChildTag(); + rerender(); - function Parent(props) { - parent = this; - return props.children; - } + expect(scratch.innerHTML).to.equal('

        child'); + expect(child.base).to.equalNode(scratch.lastChild); + expect(sibling.base).to.equalNode(scratch.firstChild); + expect(parent1.base).to.equalNode(sibling.base); + }); - render(( - - - - - + it('should update parent c.base if child component changes DOM nodes and it is first non-null child component', () => { + render(( + + - - - ), scratch); - - expect(scratch.innerHTML).to.equal([ - div('maybe'), - div('child1') - ].join('')); - expect(maybe.base).to.equalNode(scratch.firstChild, 'initial - maybe.base'); - expect(child.base).to.equalNode(scratch.lastChild, 'initial - child.base'); - expect(parent.base).to.equalNode(maybe.base, 'initial - parent.base'); - - toggleMaybeNull(); - rerender(); + + + ), scratch); - expect(scratch.innerHTML).to.equal([ - div('child1') - ].join('')); - expect(maybe.base).to.equalNode(null, 'toggleMaybe1 - maybe.base'); - expect(child.base).to.equalNode(scratch.firstChild, 'toggleMaybe1 - child.base'); - expect(parent.base).to.equalNode(child.base, 'toggleMaybe1 - parent.base'); + expect(scratch.innerHTML).to.equal('

        child

        '); + expect(nullInst.base).to.equalNode(null); + expect(child.base).to.equalNode(scratch.firstChild); + expect(sibling.base).to.equalNode(scratch.lastChild); + expect(parent1.base).to.equalNode(child.base); - swapChildTag1(); - rerender(); + swapChildTag(); + rerender(); - expect(scratch.innerHTML).to.equal([ - span('child1') - ].join('')); - expect(maybe.base).to.equalNode(null, 'swapChild1 - maybe.base'); - expect(child.base).to.equalNode(scratch.firstChild, 'swapChild1 - child.base'); - expect(parent.base).to.equalNode(child.base, 'swapChild1 - parent.base'); - }); + expect(scratch.innerHTML).to.equal('child

        '); + expect(nullInst.base).to.equalNode(null); + expect(child.base).to.equalNode(scratch.firstChild); + expect(sibling.base).to.equalNode(scratch.lastChild); + expect(parent1.base).to.equalNode(child.base); + }); - it('should update parent c.base if a null child returns DOM with multiple null siblings', () => { - /* eslint-disable lines-around-comment */ + it('should not update parent c.base if child component changes DOM nodes and a parent is not first child component', () => { + render(( + + + + + + + + + ), scratch); - /** @type {import('../../src').Component} */ - let maybe; - /** @type {import('../../src').Component} */ - let child; - /** @type {import('../../src').Component} */ - let parent; + expect(scratch.innerHTML).to.equal('

        child

        '); + expect(child.base).to.equalNode(scratch.firstChild.lastChild); + expect(parent2.base).to.equalNode(child.base); + expect(sibling.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent1.base).to.equalNode(sibling.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); - /** @type {() => void} */ - let toggleMaybeNull; - /** @type {() => void} */ - let swapChildTag1; + swapChildTag(); + rerender(); - const Null = () => null; + expect(scratch.innerHTML).to.equal('

        child
        '); + expect(child.base).to.equalNode(scratch.firstChild.lastChild); + expect(parent2.base).to.equalNode(child.base); + expect(sibling.base).to.equalNode(scratch.firstChild.firstChild); + expect(parent1.base).to.equalNode(sibling.base); + expect(parentDom1.base).to.equalNode(scratch.firstChild); + }); - class MaybeNull extends Component { - constructor(props) { - super(props); - maybe = this; - this.state = { active: false }; - toggleMaybeNull = () => this.setState(prev => ({ - active: !prev.active - })); - } - render() { - return this.state.active ?
        maybe
        : null; - } - } + it('should update parent c.base if first child becomes null', () => { + render(( + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal([ + div('maybe'), + p('child') + ].join('')); + expect(maybe.base).to.equalNode(scratch.firstChild, 'initial - maybe.base'); + expect(child.base).to.equalNode(scratch.lastChild, 'initial - child.base'); + expect(parent2.base).to.equalNode(child.base, 'initial - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'initial - parent1.base'); + + toggleMaybeNull(); + rerender(); - class Child extends Component { - constructor(props) { - super(props); - child = this; - this.state = { tagName: 'div' }; - swapChildTag1 = () => this.setState(prev => ({ - tagName: prev.tagName == 'div' ? 'span' : 'div' - })); + expect(scratch.innerHTML).to.equal([ + p('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'toggleMaybe - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'toggleMaybe - child.base'); + expect(parent2.base).to.equalNode(child.base, 'toggleMaybe - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'toggleMaybe - parent1.base'); - } - render() { - return h(this.state.tagName, null, 'child1'); - } - } + swapChildTag(); + rerender(); - function Parent(props) { - parent = this; - return props.children; - } + expect(scratch.innerHTML).to.equal([ + span('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'swapChildTag - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'swapChildTag - child.base'); + expect(parent2.base).to.equalNode(child.base, 'swapChildTag - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'swapChildTag - parent1.base'); + }); - render(( - - - - + it('should update parent c.base if first child becomes non-null', () => { + render(( + - - - - ), scratch); - - expect(scratch.innerHTML).to.equal([ - div('child1') - ].join('')); - expect(maybe.base, 'initial - maybe.base').to.equalNode(null); - expect(child.base, 'initial - child.base').to.equalNode(scratch.firstChild); - expect(parent.base, 'initial - parent.base').to.equalNode(child.base); - - swapChildTag1(); - rerender(); - - expect(scratch.innerHTML).to.equal([ - span('child1') - ].join('')); - expect(maybe.base, 'swapChild1 - maybe.base').to.equalNode(null); - expect(child.base, 'swapChild1 - child.base').to.equalNode(scratch.firstChild); - expect(parent.base, 'swapChild1 - parent.base').to.equalNode(child.base); - - toggleMaybeNull(); - rerender(); + + + + + ), scratch); - expect(scratch.innerHTML).to.equal([ - div('maybe'), - span('child1') - ].join('')); - expect(maybe.base, 'toggleMaybe2 - maybe.base').to.equalNode(scratch.firstChild); - expect(child.base, 'toggleMaybe2 - child.base').to.equalNode(scratch.lastChild); - expect(parent.base, 'toggleMaybe2 - parent.base').to.equalNode(maybe.base); - }); + expect(scratch.innerHTML).to.equal([ + p('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'initial - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'initial - child.base'); + expect(parent2.base).to.equalNode(child.base, 'initial - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'initial - parent1.base'); - it('should update parent c.base to null if last child becomes null', () => { - /* eslint-disable lines-around-comment */ + swapChildTag(); + rerender(); - /** @type {import('../../src').Component} */ - let maybe; - /** @type {import('../../src').Component} */ - let child; - /** @type {import('../../src').Component} */ - let parent; + expect(scratch.innerHTML).to.equal([ + span('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'swapChildTag - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'swapChildTag - child.base'); + expect(parent2.base).to.equalNode(child.base, 'swapChildTag - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'swapChildTag - parent1.base'); - /** @type {() => void} */ - let toggleMaybeNull; + toggleMaybeNull(); + rerender(); - const Null = () => null; + expect(scratch.innerHTML).to.equal([ + div('maybe'), + span('child') + ].join('')); + expect(maybe.base).to.equalNode(scratch.firstChild, 'toggleMaybe - maybe.base'); + expect(child.base).to.equalNode(scratch.lastChild, 'toggleMaybe - child.base'); + expect(parent2.base).to.equalNode(child.base, 'toggleMaybe - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'toggleMaybe - parent1.base'); + }); - class MaybeNull extends Component { - constructor(props) { - super(props); - maybe = this; - this.state = { active: true }; - toggleMaybeNull = () => this.setState(prev => ({ - active: !prev.active - })); - } - render() { - return this.state.active ?
        maybe
        : null; - } - } + it('should update parent c.base if first non-null child becomes null with multiple null siblings', () => { + render(( + + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal([ + div('maybe'), + p('child') + ].join('')); + expect(maybe.base).to.equalNode(scratch.firstChild, 'initial - maybe.base'); + expect(child.base).to.equalNode(scratch.lastChild, 'initial - child.base'); + expect(parent2.base).to.equalNode(maybe.base, 'initial - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'initial - parent1.base'); + + toggleMaybeNull(); + rerender(); - class Child extends Component { - constructor(props) { - super(props); - child = this; - this.state = { tagName: 'div' }; + expect(scratch.innerHTML).to.equal([ + p('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'toggleMaybe - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'toggleMaybe - child.base'); + expect(parent2.base).to.equalNode(child.base, 'toggleMaybe - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'toggleMaybe - parent1.base'); - } - render() { - return h(this.state.tagName, null, 'child1'); - } - } + swapChildTag(); + rerender(); - function Parent(props) { - parent = this; - return props.children; - } + expect(scratch.innerHTML).to.equal([ + span('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'swapChildTag - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'swapChildTag - child.base'); + expect(parent2.base).to.equalNode(child.base, 'swapChildTag - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'swapChildTag - parent1.base'); + }); - render(( - - + it('should update parent c.base if a null child returns DOM with multiple null siblings', () => { + render(( + - + - - - - - - ), scratch); - - - expect(scratch.innerHTML).to.equal([ - div('maybe'), - div('child1') - ].join('')); - expect(maybe.base, 'initial - maybe.base').to.equalNode(scratch.firstChild); - expect(child.base, 'initial - child.base').to.equalNode(scratch.lastChild); - expect(parent.base, 'initial - parent.base').to.equalNode(maybe.base); - - toggleMaybeNull(); - rerender(); - - expect(scratch.innerHTML).to.equal([ - div('child1') - ].join('')); - expect(maybe.base, 'toggleMaybe1 - maybe.base').to.equalNode(null); - expect(child.base, 'toggleMaybe1 - child.base').to.equalNode(scratch.firstChild); - expect(parent.base, 'toggleMaybe1 - parent.base').to.equalNode(maybe.base); - }); - - it('should update parent c.base if last child returns dom', () => { - /* eslint-disable lines-around-comment */ + + + + ), scratch); - /** @type {import('../../src').Component} */ - let maybe; - /** @type {import('../../src').Component} */ - let child; - /** @type {import('../../src').Component} */ - let parent; + expect(scratch.innerHTML).to.equal([ + p('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'initial - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'initial - child.base'); + expect(parent2.base).to.equalNode(child.base, 'initial - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'initial - parent1.base'); - /** @type {() => void} */ - let toggleMaybeNull; + swapChildTag(); + rerender(); - const Null = () => null; + expect(scratch.innerHTML).to.equal([ + span('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'swapChildTag - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'swapChildTag - child.base'); + expect(parent2.base).to.equalNode(child.base, 'swapChildTag - parent2.base'); + expect(parent1.base).to.equalNode(child.base, 'swapChildTag - parent1.base'); - class MaybeNull extends Component { - constructor(props) { - super(props); - maybe = this; - this.state = { active: false }; - toggleMaybeNull = () => this.setState(prev => ({ - active: !prev.active - })); - } - render() { - return this.state.active ?
        maybe
        : null; - } - } + toggleMaybeNull(); + rerender(); - class Child extends Component { - constructor(props) { - super(props); - child = this; - this.state = { tagName: 'div' }; + expect(scratch.innerHTML).to.equal([ + div('maybe'), + span('child') + ].join('')); + expect(maybe.base).to.equalNode(scratch.firstChild, 'toggleMaybe - maybe.base'); + expect(child.base).to.equalNode(scratch.lastChild, 'toggleMaybe - child.base'); + expect(parent2.base).to.equalNode(maybe.base, 'toggleMaybe - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'toggleMaybe - parent1.base'); + }); - } - render() { - return h(this.state.tagName, null, 'child1'); - } - } + it('should update parent c.base to null if last child becomes null', () => { + let fragRef = {}; + render(( + + + + + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal([ + div('maybe'), + p('child') + ].join('')); + expect(maybe.base).to.equalNode(scratch.firstChild, 'initial - maybe.base'); + expect(child.base).to.equalNode(scratch.lastChild, 'initial - child.base'); + expect(parent2.base).to.equalNode(maybe.base, 'initial - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'initial - parent1.base'); + expect(fragRef.current.base).to.equalNode(maybe.base, 'initial - fragRef.current.base'); + + toggleMaybeNull(); + rerender(); - function Parent(props) { - parent = this; - return props.children; - } + expect(scratch.innerHTML).to.equal([ + p('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'toggleMaybe - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'toggleMaybe - child.base'); + expect(parent2.base).to.equalNode(maybe.base, 'toggleMaybe - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'toggleMaybe - parent1.base'); + expect(fragRef.current.base).to.equalNode(child.base, 'toggleMaybe - fragRef.current.base'); + }); - render(( - - - - - - - - - - - - ), scratch); - - expect(scratch.innerHTML).to.equal([ - div('child1') - ].join('')); - expect(maybe.base, 'toggleMaybe1 - maybe.base').to.equalNode(null); - expect(child.base, 'toggleMaybe1 - child.base').to.equalNode(scratch.firstChild); - expect(parent.base, 'toggleMaybe1 - parent.base').to.equalNode(maybe.base); - - toggleMaybeNull(); - rerender(); + it('should update parent c.base if last child returns dom', () => { + let fragRef = {}; + render(( + + + + + + + + + + + + ), scratch); + + expect(scratch.innerHTML).to.equal([ + p('child') + ].join('')); + expect(maybe.base).to.equalNode(null, 'initial - maybe.base'); + expect(child.base).to.equalNode(scratch.firstChild, 'initial - child.base'); + expect(parent2.base).to.equalNode(maybe.base, 'initial - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'initial - parent1.base'); + expect(fragRef.current.base).to.equalNode(child.base, 'initial - fragRef.current.base'); + + toggleMaybeNull(); + rerender(); - expect(scratch.innerHTML).to.equal([ - div('maybe'), - div('child1') - ].join('')); - expect(maybe.base, 'initial - maybe.base').to.equalNode(scratch.firstChild); - expect(child.base, 'initial - child.base').to.equalNode(scratch.lastChild); - expect(parent.base, 'initial - parent.base').to.equalNode(maybe.base); + expect(scratch.innerHTML).to.equal([ + div('maybe'), + p('child') + ].join('')); + expect(maybe.base).to.equalNode(scratch.firstChild, 'toggleMaybe - maybe.base'); + expect(child.base).to.equalNode(scratch.lastChild, 'toggleMaybe - child.base'); + expect(parent2.base).to.equalNode(maybe.base, 'initial - parent2.base'); + expect(parent1.base).to.equalNode(maybe.base, 'toggleMaybe - parent1.base'); + expect(fragRef.current.base).to.equalNode(maybe.base, 'toggleMaybe - fragRef.current.base'); + }); }); }); From 7af00d8e8b3415b3de43301c461e14280c8107de Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 12 Jun 2019 15:52:05 -0700 Subject: [PATCH 16/21] Add another c.base failing test --- test/browser/components.test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/browser/components.test.js b/test/browser/components.test.js index 3a401577f3..396fd9e6b4 100644 --- a/test/browser/components.test.js +++ b/test/browser/components.test.js @@ -2058,5 +2058,21 @@ describe('Components', () => { expect(parent1.base).to.equalNode(maybe.base, 'toggleMaybe - parent1.base'); expect(fragRef.current.base).to.equalNode(maybe.base, 'toggleMaybe - fragRef.current.base'); }); + + it('should not update parent if it is a DOM node', () => { + let divVNode =
        ; + render(divVNode, scratch); + + expect(scratch.innerHTML).to.equal('

        child

        '); + expect(divVNode._dom).to.equalNode(scratch.firstChild, 'initial - divVNode._dom'); + expect(child.base).to.equalNode(scratch.firstChild.firstChild, 'initial - child.base'); + + swapChildTag(); + rerender(); + + expect(scratch.innerHTML).to.equal('
        child
        '); + expect(divVNode._dom).to.equalNode(scratch.firstChild, 'swapChildTag - divVNode._dom'); + expect(child.base).to.equalNode(scratch.firstChild.firstChild, 'swapChildTag - child.base'); + }); }); }); From 6385ee047650ccaf5e7612159da857aa7081abb4 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 12 Jun 2019 15:55:17 -0700 Subject: [PATCH 17/21] Recursively update DOM pointers handling null first children (+36 B) --- src/component.js | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/component.js b/src/component.js index cbef9444f5..c42251befa 100644 --- a/src/component.js +++ b/src/component.js @@ -72,18 +72,8 @@ Component.prototype.forceUpdate = function(callback) { let newDom = diff(parentDom, vnode, assign({}, vnode), this._context, parentDom.ownerSVGElement!==undefined, null, mounts, force, oldDom == null ? getDomSibling(vnode) : oldDom); commitRoot(mounts, vnode); - if (newDom != oldDom) { - // Update parent component's _dom and c.base pointers - while ( - vnode._parent - && vnode._parent._dom === oldDom - && (vnode = vnode._parent) - ) { - vnode._dom = newDom; - if (vnode._component) { - vnode._component.base = newDom; - } - } + if (newDom != oldDom && typeof vnode._parent.type === 'function') { + updateDomPointers(vnode._parent); } } if (callback) callback(); @@ -132,6 +122,24 @@ export function getDomSibling(vnode, childIndex) { return typeof vnode.type === 'function' ? getDomSibling(vnode) : null; } +function updateDomPointers(vnode) { + let dom = null; + for (let i = 0; dom == null && i < vnode._children.length; i++) { + if (vnode._children[i] != null) { + dom = vnode._children[i]._dom; + } + } + + vnode._dom = dom; + if (vnode._component) { + vnode._component.base = dom; + } + + if (vnode._parent != null && typeof vnode._parent.type == 'function') { + return updateDomPointers(vnode._parent); + } +} + /** * The render queue * @type {Array} From c8ceb537bc070650f3f79a5637e4ac2b60ead2ed Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 12 Jun 2019 15:58:49 -0700 Subject: [PATCH 18/21] Fix suspense catching and removal (+0 B) --- compat/src/index.js | 4 ++-- compat/src/suspense.js | 21 ++++++++++++++------- src/diff/index.js | 2 +- src/internal.d.ts | 4 ++-- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/compat/src/index.js b/compat/src/index.js index be714e1105..865620be41 100644 --- a/compat/src/index.js +++ b/compat/src/index.js @@ -20,8 +20,8 @@ options.event = e => { }; let oldCatchRender = options._catchRender; -options._catchRender = (error, component) => ( - oldCatchRender && oldCatchRender(error, component) || catchRender(error, component) +options._catchRender = (error, newVNode, oldVNode) => ( + oldCatchRender && oldCatchRender(error, newVNode, oldVNode) || catchRender(error, newVNode, oldVNode) ); /** diff --git a/compat/src/suspense.js b/compat/src/suspense.js index 81e330fc8f..b4040e3abf 100644 --- a/compat/src/suspense.js +++ b/compat/src/suspense.js @@ -3,17 +3,24 @@ import { removeNode } from '../../src/util'; /** * @param {any} error - * @param {import('./internal').VNode} vnode + * @param {import('./internal').VNode} newVNode + * @param {import('./internal').VNode} oldVNode */ -export function catchRender(error, vnode) { +export function catchRender(error, newVNode, oldVNode) { // thrown Promises are meant to suspend... if (error.then) { /** @type {import('./internal').Component} */ let component; + let vnode = newVNode; for (; vnode; vnode = vnode._parent) { if ((component = vnode._component) && component._childDidSuspend) { + if (oldVNode) { + newVNode._dom = oldVNode._dom; + newVNode._children = oldVNode._children; + } + component._childDidSuspend(error); return true; } @@ -27,18 +34,18 @@ function removeDom(children) { for (let i = 0; i < children.length; i++) { let child = children[i]; if (child != null) { - if (child._children) { - removeDom(child._children); - } - if (child._dom) { + if (typeof child.type !== 'function' && child._dom) { removeNode(child._dom); } + else if (child._children) { + removeDom(child._children); + } } } } // having custom inheritance instead of a class here saves a lot of bytes -export function Suspense(props) { +export function Suspense() { // we do not call super here to golf some bytes... this._suspensions = []; } diff --git a/src/diff/index.js b/src/diff/index.js index 762e7869af..31659a8627 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -115,7 +115,7 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi toChildArray(isTopLevelFragment ? tmp.props.children : tmp, newVNode._children=[], coerceToVNode, true); } catch (e) { - if ((tmp = options._catchRender) && tmp(e, newVNode)) return; + if ((tmp = options._catchRender) && tmp(e, newVNode, oldVNode)) break outer; throw e; } diff --git a/src/internal.d.ts b/src/internal.d.ts index a1fba0a6e5..b9209e8fd9 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -12,7 +12,7 @@ export interface Options extends preact.Options { /** Attach a hook that is invoked before a hook's state is queried. */ _hook?(component: Component): void; /** Attach a hook that is invoked after an error is caught in a component but before calling lifecycle hooks */ - catchError?(error: any, vnode: VNode): void; + _catchError?(error: any, vnode: VNode): void; /** * Attach a hook that is invoked after an error is caught while executing render. * @@ -23,7 +23,7 @@ export interface Options extends preact.Options { * @param vnode The VNode whose component's render method threw an error * @return Return a boolean indicating whether the error was handled by the hook or not */ - catchRender?(error: any, vnode: VNode): boolean; + _catchRender?(error: any, newVNode: VNode, oldVNode: VNode): boolean; } export interface FunctionalComponent

        extends preact.FunctionComponent

        { From 1649d768de3bfe1a6042c0438af3c6d86f107c9d Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 12 Jun 2019 18:37:04 -0700 Subject: [PATCH 19/21] Improve toEqualNode null assertion --- test/polyfills.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/polyfills.js b/test/polyfills.js index ee0d46fc12..f1e990d638 100644 --- a/test/polyfills.js +++ b/test/polyfills.js @@ -31,15 +31,20 @@ chai.use((chai, util) => { message = message || 'equalNode'; if (expectedNode == null) { - new Assertion(obj, message).to.not.exist; + this.assert( + obj == null, + `${message}: expected node to "== null" but got #{act} instead.`, + `${message}: expected node to not "!= null".`, + expectedNode, + obj + ); } else { new Assertion(obj).to.be.instanceof(Node, message); - // new Assertion(obj).to.have.property('tagName', expectedNode.tagName); this.assert( obj.tagName === expectedNode.tagName, `${message}: expected node to have tagName #{exp} but got #{act} instead.`, - `${message}: expected node to not have tagName #{act} instead.`, + `${message}: expected node to not have tagName #{act}.`, expectedNode.tagName, obj.tagName ); From 2d3a8a54cf4ca3a98083082b6d7288307c8b2de2 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 12 Jun 2019 18:37:57 -0700 Subject: [PATCH 20/21] Golf updateParentDomPointers (-20 B) --- src/component.js | 71 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/src/component.js b/src/component.js index c42251befa..2a20468a5e 100644 --- a/src/component.js +++ b/src/component.js @@ -72,8 +72,20 @@ Component.prototype.forceUpdate = function(callback) { let newDom = diff(parentDom, vnode, assign({}, vnode), this._context, parentDom.ownerSVGElement!==undefined, null, mounts, force, oldDom == null ? getDomSibling(vnode) : oldDom); commitRoot(mounts, vnode); - if (newDom != oldDom && typeof vnode._parent.type === 'function') { - updateDomPointers(vnode._parent); + if (newDom != oldDom) { + updateParentDomPointers(vnode); + + // // 3496 B + // while ((vnode = vnode._parent) != null && vnode._component != null) { + // vnode._dom = vnode._component.base = null; + // for (let i = 0; i < vnode._children.length; i++) { + // let child = vnode._children[i]; + // if (child != null && child._dom != null) { + // vnode._dom = vnode._component.base = child._dom; + // break; + // } + // } + // } } } if (callback) callback(); @@ -122,21 +134,50 @@ export function getDomSibling(vnode, childIndex) { return typeof vnode.type === 'function' ? getDomSibling(vnode) : null; } -function updateDomPointers(vnode) { - let dom = null; - for (let i = 0; dom == null && i < vnode._children.length; i++) { - if (vnode._children[i] != null) { - dom = vnode._children[i]._dom; +/** + * @param {import('./internal').VNode} vnode + */ +function updateParentDomPointers(vnode) { + if ((vnode = vnode._parent) != null && vnode._component != null) { + // // 3495 B + // let dom; + // for (let i = 0; dom == null && i < vnode._children.length; i++) { + // let child = vnode._children[i]; + // if (child != null) { + // dom = child._dom; + // } + // } + // vnode._dom = dom; + // vnode._component.base = dom; + + // 3486 B + vnode._dom = vnode._component.base = null; + for (let i = 0; i < vnode._children.length; i++) { + let child = vnode._children[i]; + if (child != null && child._dom != null) { + vnode._dom = vnode._component.base = child._dom; + break; + } } - } - - vnode._dom = dom; - if (vnode._component) { - vnode._component.base = dom; - } - if (vnode._parent != null && typeof vnode._parent.type == 'function') { - return updateDomPointers(vnode._parent); + // 3496 B + // let i = 0; + // let child = vnode._children[i]; + // while (i < vnode._children.length && (child == null || child._dom == null)) { + // child = vnode._children[++i]; + // } + // vnode._dom = vnode._component.base = child && child._dom; + + // 3496 B + // let i, child; + // for ( + // child = vnode._children[(i = 0)]; + // i < vnode._children.length && (child == null || null == child._dom); + // child = vnode._children[++i] + // ); + // vnode._dom = vnode._component.base = child && child._dom; + + return updateParentDomPointers(vnode); } } From 2d08b3b8a56170997151b772c10fe89405cb5854 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 14 Jun 2019 16:31:35 -0700 Subject: [PATCH 21/21] Remove failed golf attempts --- src/component.js | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/src/component.js b/src/component.js index 2a20468a5e..a3bba422bd 100644 --- a/src/component.js +++ b/src/component.js @@ -74,18 +74,6 @@ Component.prototype.forceUpdate = function(callback) { if (newDom != oldDom) { updateParentDomPointers(vnode); - - // // 3496 B - // while ((vnode = vnode._parent) != null && vnode._component != null) { - // vnode._dom = vnode._component.base = null; - // for (let i = 0; i < vnode._children.length; i++) { - // let child = vnode._children[i]; - // if (child != null && child._dom != null) { - // vnode._dom = vnode._component.base = child._dom; - // break; - // } - // } - // } } } if (callback) callback(); @@ -139,18 +127,6 @@ export function getDomSibling(vnode, childIndex) { */ function updateParentDomPointers(vnode) { if ((vnode = vnode._parent) != null && vnode._component != null) { - // // 3495 B - // let dom; - // for (let i = 0; dom == null && i < vnode._children.length; i++) { - // let child = vnode._children[i]; - // if (child != null) { - // dom = child._dom; - // } - // } - // vnode._dom = dom; - // vnode._component.base = dom; - - // 3486 B vnode._dom = vnode._component.base = null; for (let i = 0; i < vnode._children.length; i++) { let child = vnode._children[i]; @@ -160,23 +136,6 @@ function updateParentDomPointers(vnode) { } } - // 3496 B - // let i = 0; - // let child = vnode._children[i]; - // while (i < vnode._children.length && (child == null || child._dom == null)) { - // child = vnode._children[++i]; - // } - // vnode._dom = vnode._component.base = child && child._dom; - - // 3496 B - // let i, child; - // for ( - // child = vnode._children[(i = 0)]; - // i < vnode._children.length && (child == null || null == child._dom); - // child = vnode._children[++i] - // ); - // vnode._dom = vnode._component.base = child && child._dom; - return updateParentDomPointers(vnode); } }