diff --git a/packages/compiler-ssr/__tests__/ssrPortal.spec.ts b/packages/compiler-ssr/__tests__/ssrPortal.spec.ts index 5490649d5..7f608f914 100644 --- a/packages/compiler-ssr/__tests__/ssrPortal.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrPortal.spec.ts @@ -7,7 +7,7 @@ describe('ssr compile: portal', () => { "const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\") return function ssrRender(_ctx, _push, _parent) { - _ssrRenderPortal((_push) => { + _ssrRenderPortal(_push, (_push) => { _push(\`
\`) }, _ctx.target, _parent) }" diff --git a/packages/compiler-ssr/src/transforms/ssrTransformPortal.ts b/packages/compiler-ssr/src/transforms/ssrTransformPortal.ts index c380e672a..8c7fa063b 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformPortal.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformPortal.ts @@ -52,6 +52,7 @@ export function ssrProcessPortal( contentRenderFn.body = processChildrenAsStatement(node.children, context) context.pushStatement( createCallExpression(context.helper(SSR_RENDER_PORTAL), [ + `_push`, contentRenderFn, target, `_parent` diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index 84a95b41e..1b9440398 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -12,6 +12,7 @@ import { } from '@vue/runtime-dom' import { renderToString } from '@vue/server-renderer' import { mockWarn } from '@vue/shared' +import { SSRContext } from 'packages/server-renderer/src/renderToString' function mountWithHydration(html: string, render: () => any) { const container = document.createElement('div') @@ -157,7 +158,7 @@ describe('SSR hydration', () => { const fn = jest.fn() const portalContainer = document.createElement('div') portalContainer.id = 'portal' - portalContainer.innerHTML = `foo` + portalContainer.innerHTML = `foo` document.body.appendChild(portalContainer) const { vnode, container } = mountWithHydration('', () => @@ -182,7 +183,69 @@ describe('SSR hydration', () => { msg.value = 'bar' await nextTick() expect(portalContainer.innerHTML).toBe( - `bar` + `bar` + ) + }) + + test('Portal (multiple + integration)', async () => { + const msg = ref('foo') + const fn1 = jest.fn() + const fn2 = jest.fn() + + const Comp = () => [ + h(Portal, { target: '#portal2' }, [ + h('span', msg.value), + h('span', { class: msg.value, onClick: fn1 }) + ]), + h(Portal, { target: '#portal2' }, [ + h('span', msg.value + '2'), + h('span', { class: msg.value + '2', onClick: fn2 }) + ]) + ] + + const portalContainer = document.createElement('div') + portalContainer.id = 'portal2' + const ctx: SSRContext = {} + const mainHtml = await renderToString(h(Comp), ctx) + expect(mainHtml).toMatchInlineSnapshot( + `""` + ) + + const portalHtml = ctx.portals!['#portal2'] + expect(portalHtml).toMatchInlineSnapshot( + `"foofoo2"` + ) + + portalContainer.innerHTML = portalHtml + document.body.appendChild(portalContainer) + + const { vnode, container } = mountWithHydration(mainHtml, Comp) + expect(vnode.el).toBe(container.firstChild) + const portalVnode1 = (vnode.children as VNode[])[0] + const portalVnode2 = (vnode.children as VNode[])[1] + expect(portalVnode1.el).toBe(container.childNodes[1]) + expect(portalVnode2.el).toBe(container.childNodes[2]) + + expect((portalVnode1 as any).children[0].el).toBe( + portalContainer.childNodes[0] + ) + expect(portalVnode1.anchor).toBe(portalContainer.childNodes[2]) + expect((portalVnode2 as any).children[0].el).toBe( + portalContainer.childNodes[3] + ) + expect(portalVnode2.anchor).toBe(portalContainer.childNodes[5]) + + // // event handler + triggerEvent('click', portalContainer.querySelector('.foo')!) + expect(fn1).toHaveBeenCalled() + + triggerEvent('click', portalContainer.querySelector('.foo2')!) + expect(fn2).toHaveBeenCalled() + + msg.value = 'bar' + await nextTick() + expect(portalContainer.innerHTML).toMatchInlineSnapshot( + `"barbar2"` ) }) diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 37933eca5..3cbe98758 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -366,6 +366,11 @@ export function createHydrationFunctions( } } + interface PortalTargetElement extends Element { + // last portal target + _lpa?: Node | null + } + const hydratePortal = ( vnode: VNode, parentComponent: ComponentInternalInstance | null, @@ -377,14 +382,17 @@ export function createHydrationFunctions( ? document.querySelector(targetSelector) : targetSelector) if (target && vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) { - hydrateChildren( - target.firstChild, + vnode.anchor = hydrateChildren( + // if multiple portals rendered to the same target element, we need to + // pick up from where the last portal finished instead of the first node + (target as PortalTargetElement)._lpa || target.firstChild, vnode, target, parentComponent, parentSuspense, optimized ) + ;(target as PortalTargetElement)._lpa = nextSibling(vnode.anchor as Node) } else if (__DEV__) { warn( `Attempting to hydrate portal but target ${targetSelector} does not ` + diff --git a/packages/server-renderer/__tests__/ssrPortal.spec.ts b/packages/server-renderer/__tests__/ssrPortal.spec.ts index c26d60cc9..45314c2b4 100644 --- a/packages/server-renderer/__tests__/ssrPortal.spec.ts +++ b/packages/server-renderer/__tests__/ssrPortal.spec.ts @@ -4,16 +4,15 @@ import { ssrRenderPortal } from '../src/helpers/ssrRenderPortal' describe('ssrRenderPortal', () => { test('portal rendering (compiled)', async () => { - const ctx = { - portals: {} - } as SSRContext - await renderToString( + const ctx: SSRContext = {} + const html = await renderToString( createApp({ data() { return { msg: 'hello' } }, ssrRender(_ctx, _push, _parent) { ssrRenderPortal( + _push, _push => { _push(`