diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index 2f57bcd9a01..12e16c59f76 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -184,13 +184,14 @@ export function findDir( export function findProp( node: ElementNode, name: string, - dynamicOnly: boolean = false + dynamicOnly: boolean = false, + allowEmpty: boolean = false ): ElementNode['props'][0] | undefined { for (let i = 0; i < node.props.length; i++) { const p = node.props[i] if (p.type === NodeTypes.ATTRIBUTE) { if (dynamicOnly) continue - if (p.name === name && p.value) { + if (p.name === name && (p.value || allowEmpty)) { return p } } else if (p.name === 'bind' && p.exp && isBindKey(p.arg, name)) { diff --git a/packages/compiler-ssr/__tests__/ssrPortal.spec.ts b/packages/compiler-ssr/__tests__/ssrPortal.spec.ts index 7f608f91442..7f0f448a9b2 100644 --- a/packages/compiler-ssr/__tests__/ssrPortal.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrPortal.spec.ts @@ -9,8 +9,33 @@ describe('ssr compile: portal', () => { return function ssrRender(_ctx, _push, _parent) { _ssrRenderPortal(_push, (_push) => { _push(\`
\`) - }, _ctx.target, _parent) + }, _ctx.target, false, _parent) }" `) }) + + test('disabled prop handling', () => { + expect(compile(`
`).code) + .toMatchInlineSnapshot(` + "const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent) { + _ssrRenderPortal(_push, (_push) => { + _push(\`
\`) + }, _ctx.target, true, _parent) + }" + `) + + expect( + compile(`
`).code + ).toMatchInlineSnapshot(` + "const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent) { + _ssrRenderPortal(_push, (_push) => { + _push(\`
\`) + }, _ctx.target, _ctx.foo, _parent) + }" + `) + }) }) diff --git a/packages/compiler-ssr/src/transforms/ssrTransformPortal.ts b/packages/compiler-ssr/src/transforms/ssrTransformPortal.ts index 8c7fa063b6c..e67a5852184 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformPortal.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformPortal.ts @@ -1,11 +1,11 @@ import { ComponentNode, findProp, - JSChildNode, NodeTypes, createSimpleExpression, createFunctionExpression, - createCallExpression + createCallExpression, + ExpressionNode } from '@vue/compiler-dom' import { SSRTransformContext, @@ -27,12 +27,14 @@ export function ssrProcessPortal( 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 + let target: ExpressionNode | undefined + if (targetProp.type === NodeTypes.ATTRIBUTE) { + target = + targetProp.value && createSimpleExpression(targetProp.value.content, true) } else { + target = targetProp.exp + } + if (!target) { context.onError( createSSRCompilerError( SSRErrorCodes.X_SSR_NO_PORTAL_TARGET, @@ -42,6 +44,13 @@ export function ssrProcessPortal( return } + const disabledProp = findProp(node, 'disabled', false, true /* allow empty */) + const disabled = disabledProp + ? disabledProp.type === NodeTypes.ATTRIBUTE + ? `true` + : disabledProp.exp || `false` + : `false` + const contentRenderFn = createFunctionExpression( [`_push`], undefined, // Body is added later @@ -55,6 +64,7 @@ export function ssrProcessPortal( `_push`, contentRenderFn, target, + disabled, `_parent` ]) ) diff --git a/packages/runtime-core/src/components/Portal.ts b/packages/runtime-core/src/components/Portal.ts index 370b43bb0e6..85ca6ef1f65 100644 --- a/packages/runtime-core/src/components/Portal.ts +++ b/packages/runtime-core/src/components/Portal.ts @@ -23,6 +23,9 @@ export const enum PortalMoveTypes { REORDER // moved in the main view } +const isDisabled = (props: VNode['props']): boolean => + props && (props.disabled || props.disabled === '') + const movePortal = ( vnode: VNode, container: RendererElement, @@ -43,7 +46,7 @@ const movePortal = ( // if this is a re-order and portal is enabled (content is in target) // do not move children. So the opposite is: only move children if this // is not a reorder, or the portal is disabled - if (!isReorder || (props && props.disabled)) { + if (!isReorder || isDisabled(props)) { // Portal has either Array children or no children. if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { for (let i = 0; i < (children as VNode[]).length; i++) { @@ -83,7 +86,7 @@ export const PortalImpl = { } = internals const targetSelector = n2.props && n2.props.target - const disabled = n2.props && n2.props.disabled + const disabled = isDisabled(n2.props) const { shapeFlag, children } = n2 if (n1 == null) { if (__DEV__ && isString(targetSelector) && !querySelector) { @@ -140,7 +143,7 @@ export const PortalImpl = { const mainAnchor = (n2.anchor = n1.anchor)! const target = (n2.target = n1.target)! const targetAnchor = (n2.targetAnchor = n1.targetAnchor)! - const wasDisabled = n1.props && n1.props.disabled + const wasDisabled = isDisabled(n1.props) const currentContainer = wasDisabled ? container : target const currentAnchor = wasDisabled ? mainAnchor : targetAnchor diff --git a/packages/server-renderer/__tests__/ssrPortal.spec.ts b/packages/server-renderer/__tests__/ssrPortal.spec.ts index 45314c2b464..0a095e07207 100644 --- a/packages/server-renderer/__tests__/ssrPortal.spec.ts +++ b/packages/server-renderer/__tests__/ssrPortal.spec.ts @@ -17,16 +17,42 @@ describe('ssrRenderPortal', () => { _push(`
content
`) }, '#target', + false, _parent ) } }), ctx ) - expect(html).toBe('') + expect(html).toBe('') expect(ctx.portals!['#target']).toBe(`
content
`) }) + test('portal rendering (compiled + disabled)', async () => { + const ctx: SSRContext = {} + const html = await renderToString( + createApp({ + data() { + return { msg: 'hello' } + }, + ssrRender(_ctx, _push, _parent) { + ssrRenderPortal( + _push, + _push => { + _push(`
content
`) + }, + '#target', + true, + _parent + ) + } + }), + ctx + ) + expect(html).toBe('
content
') + expect(ctx.portals!['#target']).toBe(``) + }) + test('portal rendering (vnode)', async () => { const ctx: SSRContext = {} const html = await renderToString( @@ -39,10 +65,27 @@ describe('ssrRenderPortal', () => { ), ctx ) - expect(html).toBe('') + expect(html).toBe('') expect(ctx.portals!['#target']).toBe('hello') }) + test('portal rendering (vnode + disabled)', async () => { + const ctx: SSRContext = {} + const html = await renderToString( + h( + Portal, + { + target: `#target`, + disabled: true + }, + h('span', 'hello') + ), + ctx + ) + expect(html).toBe('hello') + expect(ctx.portals!['#target']).toBe(``) + }) + test('multiple portals with same target', async () => { const ctx: SSRContext = {} const html = await renderToString( @@ -58,7 +101,9 @@ describe('ssrRenderPortal', () => { ]), ctx ) - expect(html).toBe('
') + expect(html).toBe( + '
' + ) expect(ctx.portals!['#target']).toBe( 'helloworld' ) diff --git a/packages/server-renderer/src/helpers/ssrRenderPortal.ts b/packages/server-renderer/src/helpers/ssrRenderPortal.ts index 3e54d999ac1..2f694cbdf77 100644 --- a/packages/server-renderer/src/helpers/ssrRenderPortal.ts +++ b/packages/server-renderer/src/helpers/ssrRenderPortal.ts @@ -1,16 +1,31 @@ import { ComponentInternalInstance, ssrContextKey } from 'vue' -import { SSRContext, createBuffer, PushFn } from '../renderToString' +import { + SSRContext, + createBuffer, + PushFn, + SSRBufferItem +} from '../renderToString' export function ssrRenderPortal( parentPush: PushFn, contentRenderFn: (push: PushFn) => void, target: string, + disabled: boolean, parentComponent: ComponentInternalInstance ) { - parentPush('') - const { getBuffer, push } = createBuffer() - contentRenderFn(push) - push(``) // portal end anchor + parentPush('') + + let portalContent: SSRBufferItem + + if (disabled) { + contentRenderFn(parentPush) + portalContent = `` + } else { + const { getBuffer, push } = createBuffer() + contentRenderFn(push) + push(``) // portal end anchor + portalContent = getBuffer() + } const context = parentComponent.appContext.provides[ ssrContextKey as any @@ -18,8 +33,10 @@ export function ssrRenderPortal( const portalBuffers = context.__portalBuffers || (context.__portalBuffers = {}) if (portalBuffers[target]) { - portalBuffers[target].push(getBuffer()) + portalBuffers[target].push(portalContent) } else { - portalBuffers[target] = [getBuffer()] + portalBuffers[target] = [portalContent] } + + parentPush('') } diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index 14666678da3..5c84ae7aec7 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/packages/server-renderer/src/renderToString.ts @@ -366,6 +366,7 @@ function renderPortalVNode( parentComponent: ComponentInternalInstance ) { const target = vnode.props && vnode.props.target + const disabled = vnode.props && vnode.props.disabled if (!target) { warn(`[@vue/server-renderer] Portal is missing target prop.`) return [] @@ -386,6 +387,7 @@ function renderPortalVNode( ) }, target, + disabled || disabled === '', parentComponent ) }