}"
`)
})
+
+ test('portal rendering', () => {
+ expect(compile(`<portal :target="target"><div/></portal>`).code)
+ .toMatchInlineSnapshot(`
+ "const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent) {
+ _ssrRenderPortal((_push) => {
+ _push(\`<div></div>\`)
+ }, _ctx.target, _parent)
+ }"
+ `)
+ })
})
})
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.`
}
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`,
[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
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,
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
}
--- /dev/null
+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(`<div>content</div>`)
+ },
+ '#target',
+ _parent
+ )
+ }
+ }),
+ ctx
+ )
+ expect(ctx.portals!['#target']).toBe(`<div>content</div>`)
+ })
+})
--- /dev/null
+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()
+}
} from './helpers/ssrRenderAttrs'
export { ssrInterpolate } from './helpers/ssrInterpolate'
export { ssrRenderList } from './helpers/ssrRenderList'
+export { ssrRenderPortal } from './helpers/ssrRenderPortal'
// v-model helpers
export {
// - 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<ResolvedSSRBuffer>
-type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
+export type SSRBuffer = SSRBufferItem[]
+export type SSRBufferItem =
+ | string
+ | ResolvedSSRBuffer
+ | Promise<ResolvedSSRBuffer>
+export type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
export type PushFn = (item: SSRBufferItem) => void
>
}
-function createBuffer() {
+export function createBuffer() {
let appendable = false
let hasAsync = false
const buffer: SSRBuffer = []