diff --git a/src/runtime/test/hydrate-no-encapsulation.spec.tsx b/src/runtime/test/hydrate-no-encapsulation.spec.tsx index 0881b555226..9733c08a796 100644 --- a/src/runtime/test/hydrate-no-encapsulation.spec.tsx +++ b/src/runtime/test/hydrate-no-encapsulation.spec.tsx @@ -13,7 +13,6 @@ describe('hydrate no encapsulation', () => { ); } } - // @ts-ignore const serverHydrated = await newSpecPage({ components: [CmpA], html: ``, @@ -38,7 +37,6 @@ describe('hydrate no encapsulation', () => { ); } } - // @ts-ignore const serverHydrated = await newSpecPage({ components: [CmpA], html: ``, @@ -54,7 +52,6 @@ describe('hydrate no encapsulation', () => { `); - // @ts-ignore const clientHydrated = await newSpecPage({ components: [CmpA], html: serverHydrated.root.outerHTML, @@ -81,7 +78,6 @@ describe('hydrate no encapsulation', () => { return Hello; } } - // @ts-ignore const serverHydrated = await newSpecPage({ components: [CmpA], html: ``, @@ -95,7 +91,6 @@ describe('hydrate no encapsulation', () => { `); - // @ts-ignore const clientHydrated = await newSpecPage({ components: [CmpA], html: serverHydrated.root.outerHTML, @@ -126,7 +121,6 @@ describe('hydrate no encapsulation', () => { ); } } - // @ts-ignore const serverHydrated = await newSpecPage({ components: [CmpA], html: ``, @@ -146,7 +140,6 @@ describe('hydrate no encapsulation', () => { `); - // @ts-ignore const clientHydrated = await newSpecPage({ components: [CmpA], html: serverHydrated.root.outerHTML, @@ -166,6 +159,67 @@ describe('hydrate no encapsulation', () => { `); }); + it('nested text slot with key', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + return ( + + light-dom + + ); + } + } + @Component({ tag: 'cmp-b' }) + class CmpB { + render() { + return ( + + + + + ); + } + } + const serverHydrated = await newSpecPage({ + components: [CmpA, CmpB], + html: ``, + hydrateServerSide: true, + }); + expect(serverHydrated.root).toEqualHtml(` + + + + + + + + light-dom + + + + `); + + const clientHydrated = await newSpecPage({ + components: [CmpA, CmpB], + html: serverHydrated.root.outerHTML, + hydrateClientSide: true, + }); + + expect(clientHydrated.root).toEqualHtml(` + + + + + + + light-dom + + + + `); + }); + it('nested, text slot, footer', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @@ -188,7 +242,6 @@ describe('hydrate no encapsulation', () => { ); } } - // @ts-ignore const serverHydrated = await newSpecPage({ components: [CmpA, CmpB], html: ``, @@ -208,7 +261,6 @@ describe('hydrate no encapsulation', () => { `); - // @ts-ignore const clientHydrated = await newSpecPage({ components: [CmpA, CmpB], html: serverHydrated.root.outerHTML, @@ -252,7 +304,6 @@ describe('hydrate no encapsulation', () => { } } - // @ts-ignore const serverHydrated = await newSpecPage({ components: [CmpA, CmpB], html: ``, @@ -272,7 +323,6 @@ describe('hydrate no encapsulation', () => { `); - // @ts-ignore const clientHydrated = await newSpecPage({ components: [CmpA, CmpB], html: serverHydrated.root.outerHTML, @@ -316,7 +366,6 @@ describe('hydrate no encapsulation', () => { ); } } - // @ts-ignore const serverHydrated = await newSpecPage({ components: [CmpA, CmpB], html: ``, @@ -337,7 +386,6 @@ describe('hydrate no encapsulation', () => { `); - // @ts-ignore const clientHydrated = await newSpecPage({ components: [CmpA, CmpB], html: serverHydrated.root.outerHTML, @@ -388,7 +436,6 @@ describe('hydrate no encapsulation', () => { ); } } - // @ts-ignore const serverHydrated = await newSpecPage({ components: [CmpA, CmpB], html: ``, @@ -421,7 +468,6 @@ describe('hydrate no encapsulation', () => { `); - // @ts-ignore const clientHydrated = await newSpecPage({ components: [CmpA, CmpB], html: serverHydrated.root.outerHTML, diff --git a/src/runtime/vdom/vdom-render.ts b/src/runtime/vdom/vdom-render.ts index 9643007de0f..2e3bec3729b 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/src/runtime/vdom/vdom-render.ts @@ -393,8 +393,15 @@ const removeVnodes = (vnodes: d.VNode[], startIdx: number, endIdx: number) => { * @param oldCh the old children of the parent node * @param newVNode the new VNode which will replace the parent * @param newCh the new children of the parent node + * @param isInitialRender whether or not this is the first render of the vdom */ -const updateChildren = (parentElm: d.RenderNode, oldCh: d.VNode[], newVNode: d.VNode, newCh: d.VNode[]) => { +const updateChildren = ( + parentElm: d.RenderNode, + oldCh: d.VNode[], + newVNode: d.VNode, + newCh: d.VNode[], + isInitialRender = false, +) => { let oldStartIdx = 0; let newStartIdx = 0; let idxInOld = 0; @@ -418,22 +425,22 @@ const updateChildren = (parentElm: d.RenderNode, oldCh: d.VNode[], newVNode: d.V newStartVnode = newCh[++newStartIdx]; } else if (newEndVnode == null) { newEndVnode = newCh[--newEndIdx]; - } else if (isSameVnode(oldStartVnode, newStartVnode)) { + } else if (isSameVnode(oldStartVnode, newStartVnode, isInitialRender)) { // if the start nodes are the same then we should patch the new VNode // onto the old one, and increment our `newStartIdx` and `oldStartIdx` // indices to reflect that. We don't need to move any DOM Nodes around // since things are matched up in order. - patch(oldStartVnode, newStartVnode); + patch(oldStartVnode, newStartVnode, isInitialRender); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; - } else if (isSameVnode(oldEndVnode, newEndVnode)) { + } else if (isSameVnode(oldEndVnode, newEndVnode, isInitialRender)) { // likewise, if the end nodes are the same we patch new onto old and // decrement our end indices, and also likewise in this case we don't // need to move any DOM Nodes. - patch(oldEndVnode, newEndVnode); + patch(oldEndVnode, newEndVnode, isInitialRender); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; - } else if (isSameVnode(oldStartVnode, newEndVnode)) { + } else if (isSameVnode(oldStartVnode, newEndVnode, isInitialRender)) { // case: "Vnode moved right" // // We've found that the last node in our window on the new children is @@ -451,7 +458,7 @@ const updateChildren = (parentElm: d.RenderNode, oldCh: d.VNode[], newVNode: d.V if (BUILD.slotRelocation && (oldStartVnode.$tag$ === 'slot' || newEndVnode.$tag$ === 'slot')) { putBackInOriginalLocation(oldStartVnode.$elm$.parentNode, false); } - patch(oldStartVnode, newEndVnode); + patch(oldStartVnode, newEndVnode, isInitialRender); // We need to move the element for `oldStartVnode` into a position which // will be appropriate for `newEndVnode`. For this we can use // `.insertBefore` and `oldEndVnode.$elm$.nextSibling`. If there is a @@ -472,7 +479,7 @@ const updateChildren = (parentElm: d.RenderNode, oldCh: d.VNode[], newVNode: d.V parentElm.insertBefore(oldStartVnode.$elm$, oldEndVnode.$elm$.nextSibling as any); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; - } else if (isSameVnode(oldEndVnode, newStartVnode)) { + } else if (isSameVnode(oldEndVnode, newStartVnode, isInitialRender)) { // case: "Vnode moved left" // // We've found that the first node in our window on the new children is @@ -491,7 +498,7 @@ const updateChildren = (parentElm: d.RenderNode, oldCh: d.VNode[], newVNode: d.V if (BUILD.slotRelocation && (oldStartVnode.$tag$ === 'slot' || newEndVnode.$tag$ === 'slot')) { putBackInOriginalLocation(oldEndVnode.$elm$.parentNode, false); } - patch(oldEndVnode, newStartVnode); + patch(oldEndVnode, newStartVnode, isInitialRender); // We've already checked above if `oldStartVnode` and `newStartVnode` are // the same node, so since we're here we know that they are not. Thus we // can move the element for `oldEndVnode` _before_ the element for @@ -528,7 +535,7 @@ const updateChildren = (parentElm: d.RenderNode, oldCh: d.VNode[], newVNode: d.V // the tag doesn't match so we'll need a new DOM element node = createElm(oldCh && oldCh[newStartIdx], newVNode, idxInOld, parentElm); } else { - patch(elmToMove, newStartVnode); + patch(elmToMove, newStartVnode, isInitialRender); // invalidate the matching old node so that we won't try to update it // again later on oldCh[idxInOld] = undefined; @@ -590,17 +597,22 @@ const updateChildren = (parentElm: d.RenderNode, oldCh: d.VNode[], newVNode: d.V * * @param leftVNode the first VNode to check * @param rightVNode the second VNode to check + * @param isInitialRender whether or not this is the first render of the vdom * @returns whether they're equal or not */ -export const isSameVnode = (leftVNode: d.VNode, rightVNode: d.VNode) => { +export const isSameVnode = (leftVNode: d.VNode, rightVNode: d.VNode, isInitialRender = false) => { // compare if two vnode to see if they're "technically" the same // need to have the same element tag, and same key to be the same if (leftVNode.$tag$ === rightVNode.$tag$) { if (BUILD.slotRelocation && leftVNode.$tag$ === 'slot') { return leftVNode.$name$ === rightVNode.$name$; } - // this will be set if components in the build have `key` attrs set on them - if (BUILD.vdomKey) { + // this will be set if JSX tags in the build have `key` attrs set on them + // we only want to check this if we're not on the first render since on + // first render `leftVNode.$key$` will always be `null`, so we can be led + // astray and, for instance, accidentally delete a DOM node that we want to + // keep around. + if (BUILD.vdomKey && !isInitialRender) { return leftVNode.$key$ === rightVNode.$key$; } return true; @@ -625,8 +637,9 @@ const parentReferenceNode = (node: d.RenderNode) => (node['s-ol'] ? node['s-ol'] * * @param oldVNode an old VNode whose DOM element and children we want to update * @param newVNode a new VNode representing an updated version of the old one + * @param isInitialRender whether or not this is the first render of the vdom */ -export const patch = (oldVNode: d.VNode, newVNode: d.VNode) => { +export const patch = (oldVNode: d.VNode, newVNode: d.VNode, isInitialRender = false) => { const elm = (newVNode.$elm$ = oldVNode.$elm$); const oldChildren = oldVNode.$children$; const newChildren = newVNode.$children$; @@ -655,7 +668,7 @@ export const patch = (oldVNode: d.VNode, newVNode: d.VNode) => { if (BUILD.updatable && oldChildren !== null && newChildren !== null) { // looks like there's child vnodes for both the old and new vnodes // so we need to call `updateChildren` to reconcile them - updateChildren(elm, oldChildren, newVNode, newChildren); + updateChildren(elm, oldChildren, newVNode, newChildren, isInitialRender); } else if (newChildren !== null) { // no old child vnodes, but there are new child vnodes to add if (BUILD.updatable && BUILD.vdomText && oldVNode.$text$ !== null) { @@ -944,6 +957,7 @@ render() { } `); } + if (BUILD.reflect && cmpMeta.$attrsToReflect$) { rootVnode.$attrs$ = rootVnode.$attrs$ || {}; cmpMeta.$attrsToReflect$.map( @@ -990,7 +1004,7 @@ render() { } // synchronous patch - patch(oldVNode, rootVnode); + patch(oldVNode, rootVnode, isInitialLoad); if (BUILD.slotRelocation) { // while we're moving nodes around existing nodes, temporarily disable