diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
index 0cca3cefee2..8efe67a5c9a 100644
--- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
@@ -309,5 +309,18 @@ describe('ssr: components', () => {
}"
`)
})
+
+ test('portal rendering', () => {
+ expect(compile(``).code)
+ .toMatchInlineSnapshot(`
+ "const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent) {
+ _ssrRenderPortal((_push) => {
+ _push(\`
\`)
+ }, _ctx.target, _parent)
+ }"
+ `)
+ })
})
})
diff --git a/packages/compiler-ssr/src/errors.ts b/packages/compiler-ssr/src/errors.ts
index 30431fb0818..fdc99160b01 100644
--- a/packages/compiler-ssr/src/errors.ts
+++ b/packages/compiler-ssr/src/errors.ts
@@ -18,10 +18,12 @@ export function createSSRCompilerError(
export const enum SSRErrorCodes {
X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM = DOMErrorCodes.__EXTEND_POINT__,
- X_SSR_UNSAFE_ATTR_NAME
+ X_SSR_UNSAFE_ATTR_NAME,
+ X_SSR_NO_PORTAL_TARGET
}
export const SSRErrorMessages: { [code: number]: string } = {
[SSRErrorCodes.X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM]: `Custom directive is missing corresponding SSR transform and will be ignored.`,
- [SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME]: `Unsafe attribute name for SSR.`
+ [SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME]: `Unsafe attribute name for SSR.`,
+ [SSRErrorCodes.X_SSR_NO_PORTAL_TARGET]: `No target prop on portal element.`
}
diff --git a/packages/compiler-ssr/src/runtimeHelpers.ts b/packages/compiler-ssr/src/runtimeHelpers.ts
index 2f394514de6..01875cf05ca 100644
--- a/packages/compiler-ssr/src/runtimeHelpers.ts
+++ b/packages/compiler-ssr/src/runtimeHelpers.ts
@@ -13,6 +13,7 @@ export const SSR_LOOSE_EQUAL = Symbol(`ssrLooseEqual`)
export const SSR_LOOSE_CONTAIN = Symbol(`ssrLooseContain`)
export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`)
export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`)
+export const SSR_RENDER_PORTAL = Symbol(`ssrRenderPortal`)
export const ssrHelpers = {
[SSR_INTERPOLATE]: `ssrInterpolate`,
@@ -27,7 +28,8 @@ export const ssrHelpers = {
[SSR_LOOSE_EQUAL]: `ssrLooseEqual`,
[SSR_LOOSE_CONTAIN]: `ssrLooseContain`,
[SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
- [SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`
+ [SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`,
+ [SSR_RENDER_PORTAL]: `ssrRenderPortal`
}
// Note: these are helpers imported from @vue/server-renderer
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
index a9c38c70cfa..87c6988bfb8 100644
--- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
+++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
@@ -31,9 +31,11 @@ import {
createTransformContext,
traverseNode,
ExpressionNode,
- TemplateNode
+ TemplateNode,
+ findProp,
+ JSChildNode
} from '@vue/compiler-dom'
-import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
+import { SSR_RENDER_COMPONENT, SSR_RENDER_PORTAL } from '../runtimeHelpers'
import {
SSRTransformContext,
processChildren,
@@ -134,7 +136,34 @@ export function ssrProcessComponent(
const component = componentTypeMap.get(node)!
if (component === PORTAL) {
- // TODO
+ const targetProp = findProp(node, 'target')
+ if (!targetProp) return
+
+ let target: JSChildNode
+ if (targetProp.type === NodeTypes.ATTRIBUTE && targetProp.value) {
+ target = createSimpleExpression(targetProp.value.content, true)
+ } else if (targetProp.type === NodeTypes.DIRECTIVE && targetProp.exp) {
+ target = targetProp.exp
+ } else {
+ return
+ }
+
+ const contentRenderFn = createFunctionExpression(
+ [`_push`],
+ undefined, // Body is added later
+ true, // newline
+ false, // isSlot
+ node.loc
+ )
+ contentRenderFn.body = processChildrenAsStatement(node.children, context)
+ context.pushStatement(
+ createCallExpression(context.helper(SSR_RENDER_PORTAL), [
+ contentRenderFn,
+ target,
+ `_parent`
+ ])
+ )
+
return
}
diff --git a/packages/server-renderer/__tests__/ssrRenderPortal.spec.ts b/packages/server-renderer/__tests__/ssrRenderPortal.spec.ts
new file mode 100644
index 00000000000..70031dd4ee0
--- /dev/null
+++ b/packages/server-renderer/__tests__/ssrRenderPortal.spec.ts
@@ -0,0 +1,29 @@
+import { createApp } from 'vue'
+import { renderToString, SSRContext } from '../src/renderToString'
+import { ssrRenderPortal } from '../src/helpers/ssrRenderPortal'
+
+describe('ssrRenderPortal', () => {
+ test('portal rendering', async () => {
+ const ctx = {
+ portals: {}
+ } as SSRContext
+ await renderToString(
+ createApp({
+ data() {
+ return { msg: 'hello' }
+ },
+ ssrRender(_ctx, _push, _parent) {
+ ssrRenderPortal(
+ _push => {
+ _push(`content
`)
+ },
+ '#target',
+ _parent
+ )
+ }
+ }),
+ ctx
+ )
+ expect(ctx.portals!['#target']).toBe(`content
`)
+ })
+})
diff --git a/packages/server-renderer/src/helpers/ssrRenderPortal.ts b/packages/server-renderer/src/helpers/ssrRenderPortal.ts
new file mode 100644
index 00000000000..12c2282334a
--- /dev/null
+++ b/packages/server-renderer/src/helpers/ssrRenderPortal.ts
@@ -0,0 +1,20 @@
+import { ComponentInternalInstance, ssrContextKey } from 'vue'
+import { SSRContext, createBuffer, PushFn } from '../renderToString'
+
+export function ssrRenderPortal(
+ contentRenderFn: (push: PushFn) => void,
+ target: string,
+ parentComponent: ComponentInternalInstance
+) {
+ const { getBuffer, push } = createBuffer()
+
+ contentRenderFn(push)
+
+ const context = parentComponent.appContext.provides[
+ ssrContextKey as any
+ ] as SSRContext
+ const portalBuffers =
+ context.__portalBuffers || (context.__portalBuffers = {})
+
+ portalBuffers[target] = getBuffer()
+}
diff --git a/packages/server-renderer/src/index.ts b/packages/server-renderer/src/index.ts
index 59b2b45f8fb..315b3ae18c7 100644
--- a/packages/server-renderer/src/index.ts
+++ b/packages/server-renderer/src/index.ts
@@ -13,6 +13,7 @@ export {
} from './helpers/ssrRenderAttrs'
export { ssrInterpolate } from './helpers/ssrInterpolate'
export { ssrRenderList } from './helpers/ssrRenderList'
+export { ssrRenderPortal } from './helpers/ssrRenderPortal'
// v-model helpers
export {
diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts
index 0aac04cd14a..14c041c6561 100644
--- a/packages/server-renderer/src/renderToString.ts
+++ b/packages/server-renderer/src/renderToString.ts
@@ -45,9 +45,12 @@ const {
// - A resolved buffer (recursive arrays of strings that can be unrolled
// synchronously)
// - An async buffer (a Promise that resolves to a resolved buffer)
-type SSRBuffer = SSRBufferItem[]
-type SSRBufferItem = string | ResolvedSSRBuffer | Promise
-type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
+export type SSRBuffer = SSRBufferItem[]
+export type SSRBufferItem =
+ | string
+ | ResolvedSSRBuffer
+ | Promise
+export type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
export type PushFn = (item: SSRBufferItem) => void
@@ -62,7 +65,7 @@ export type SSRContext = {
>
}
-function createBuffer() {
+export function createBuffer() {
let appendable = false
let hasAsync = false
const buffer: SSRBuffer = []