feat(portal): SSR support for portal disabled prop

This commit is contained in:
Evan You 2020-03-27 23:45:50 -04:00
parent 8ce3da0104
commit 9ed9bf3687
7 changed files with 126 additions and 23 deletions

View File

@ -184,13 +184,14 @@ export function findDir(
export function findProp( export function findProp(
node: ElementNode, node: ElementNode,
name: string, name: string,
dynamicOnly: boolean = false dynamicOnly: boolean = false,
allowEmpty: boolean = false
): ElementNode['props'][0] | undefined { ): ElementNode['props'][0] | undefined {
for (let i = 0; i < node.props.length; i++) { for (let i = 0; i < node.props.length; i++) {
const p = node.props[i] const p = node.props[i]
if (p.type === NodeTypes.ATTRIBUTE) { if (p.type === NodeTypes.ATTRIBUTE) {
if (dynamicOnly) continue if (dynamicOnly) continue
if (p.name === name && p.value) { if (p.name === name && (p.value || allowEmpty)) {
return p return p
} }
} else if (p.name === 'bind' && p.exp && isBindKey(p.arg, name)) { } else if (p.name === 'bind' && p.exp && isBindKey(p.arg, name)) {

View File

@ -9,8 +9,33 @@ describe('ssr compile: portal', () => {
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_ssrRenderPortal(_push, (_push) => { _ssrRenderPortal(_push, (_push) => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}, _ctx.target, _parent) }, _ctx.target, false, _parent)
}" }"
`) `)
}) })
test('disabled prop handling', () => {
expect(compile(`<portal :target="target" disabled><div/></portal>`).code)
.toMatchInlineSnapshot(`
"const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_ssrRenderPortal(_push, (_push) => {
_push(\`<div></div>\`)
}, _ctx.target, true, _parent)
}"
`)
expect(
compile(`<portal :target="target" :disabled="foo"><div/></portal>`).code
).toMatchInlineSnapshot(`
"const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
_ssrRenderPortal(_push, (_push) => {
_push(\`<div></div>\`)
}, _ctx.target, _ctx.foo, _parent)
}"
`)
})
}) })

View File

@ -1,11 +1,11 @@
import { import {
ComponentNode, ComponentNode,
findProp, findProp,
JSChildNode,
NodeTypes, NodeTypes,
createSimpleExpression, createSimpleExpression,
createFunctionExpression, createFunctionExpression,
createCallExpression createCallExpression,
ExpressionNode
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { import {
SSRTransformContext, SSRTransformContext,
@ -27,12 +27,14 @@ export function ssrProcessPortal(
return return
} }
let target: JSChildNode let target: ExpressionNode | undefined
if (targetProp.type === NodeTypes.ATTRIBUTE && targetProp.value) { if (targetProp.type === NodeTypes.ATTRIBUTE) {
target = createSimpleExpression(targetProp.value.content, true) target =
} else if (targetProp.type === NodeTypes.DIRECTIVE && targetProp.exp) { targetProp.value && createSimpleExpression(targetProp.value.content, true)
target = targetProp.exp
} else { } else {
target = targetProp.exp
}
if (!target) {
context.onError( context.onError(
createSSRCompilerError( createSSRCompilerError(
SSRErrorCodes.X_SSR_NO_PORTAL_TARGET, SSRErrorCodes.X_SSR_NO_PORTAL_TARGET,
@ -42,6 +44,13 @@ export function ssrProcessPortal(
return 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( const contentRenderFn = createFunctionExpression(
[`_push`], [`_push`],
undefined, // Body is added later undefined, // Body is added later
@ -55,6 +64,7 @@ export function ssrProcessPortal(
`_push`, `_push`,
contentRenderFn, contentRenderFn,
target, target,
disabled,
`_parent` `_parent`
]) ])
) )

View File

@ -23,6 +23,9 @@ export const enum PortalMoveTypes {
REORDER // moved in the main view REORDER // moved in the main view
} }
const isDisabled = (props: VNode['props']): boolean =>
props && (props.disabled || props.disabled === '')
const movePortal = ( const movePortal = (
vnode: VNode, vnode: VNode,
container: RendererElement, container: RendererElement,
@ -43,7 +46,7 @@ const movePortal = (
// if this is a re-order and portal is enabled (content is in target) // 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 // do not move children. So the opposite is: only move children if this
// is not a reorder, or the portal is disabled // 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. // Portal has either Array children or no children.
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
for (let i = 0; i < (children as VNode[]).length; i++) { for (let i = 0; i < (children as VNode[]).length; i++) {
@ -83,7 +86,7 @@ export const PortalImpl = {
} = internals } = internals
const targetSelector = n2.props && n2.props.target const targetSelector = n2.props && n2.props.target
const disabled = n2.props && n2.props.disabled const disabled = isDisabled(n2.props)
const { shapeFlag, children } = n2 const { shapeFlag, children } = n2
if (n1 == null) { if (n1 == null) {
if (__DEV__ && isString(targetSelector) && !querySelector) { if (__DEV__ && isString(targetSelector) && !querySelector) {
@ -140,7 +143,7 @@ export const PortalImpl = {
const mainAnchor = (n2.anchor = n1.anchor)! const mainAnchor = (n2.anchor = n1.anchor)!
const target = (n2.target = n1.target)! const target = (n2.target = n1.target)!
const targetAnchor = (n2.targetAnchor = n1.targetAnchor)! const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
const wasDisabled = n1.props && n1.props.disabled const wasDisabled = isDisabled(n1.props)
const currentContainer = wasDisabled ? container : target const currentContainer = wasDisabled ? container : target
const currentAnchor = wasDisabled ? mainAnchor : targetAnchor const currentAnchor = wasDisabled ? mainAnchor : targetAnchor

View File

@ -17,16 +17,42 @@ describe('ssrRenderPortal', () => {
_push(`<div>content</div>`) _push(`<div>content</div>`)
}, },
'#target', '#target',
false,
_parent _parent
) )
} }
}), }),
ctx ctx
) )
expect(html).toBe('<!--portal-->') expect(html).toBe('<!--portal start--><!--portal end-->')
expect(ctx.portals!['#target']).toBe(`<div>content</div><!---->`) expect(ctx.portals!['#target']).toBe(`<div>content</div><!---->`)
}) })
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(`<div>content</div>`)
},
'#target',
true,
_parent
)
}
}),
ctx
)
expect(html).toBe('<!--portal start--><div>content</div><!--portal end-->')
expect(ctx.portals!['#target']).toBe(`<!---->`)
})
test('portal rendering (vnode)', async () => { test('portal rendering (vnode)', async () => {
const ctx: SSRContext = {} const ctx: SSRContext = {}
const html = await renderToString( const html = await renderToString(
@ -39,10 +65,27 @@ describe('ssrRenderPortal', () => {
), ),
ctx ctx
) )
expect(html).toBe('<!--portal-->') expect(html).toBe('<!--portal start--><!--portal end-->')
expect(ctx.portals!['#target']).toBe('<span>hello</span><!---->') expect(ctx.portals!['#target']).toBe('<span>hello</span><!---->')
}) })
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('<!--portal start--><span>hello</span><!--portal end-->')
expect(ctx.portals!['#target']).toBe(`<!---->`)
})
test('multiple portals with same target', async () => { test('multiple portals with same target', async () => {
const ctx: SSRContext = {} const ctx: SSRContext = {}
const html = await renderToString( const html = await renderToString(
@ -58,7 +101,9 @@ describe('ssrRenderPortal', () => {
]), ]),
ctx ctx
) )
expect(html).toBe('<div><!--portal--><!--portal--></div>') expect(html).toBe(
'<div><!--portal start--><!--portal end--><!--portal start--><!--portal end--></div>'
)
expect(ctx.portals!['#target']).toBe( expect(ctx.portals!['#target']).toBe(
'<span>hello</span><!---->world<!---->' '<span>hello</span><!---->world<!---->'
) )

View File

@ -1,16 +1,31 @@
import { ComponentInternalInstance, ssrContextKey } from 'vue' import { ComponentInternalInstance, ssrContextKey } from 'vue'
import { SSRContext, createBuffer, PushFn } from '../renderToString' import {
SSRContext,
createBuffer,
PushFn,
SSRBufferItem
} from '../renderToString'
export function ssrRenderPortal( export function ssrRenderPortal(
parentPush: PushFn, parentPush: PushFn,
contentRenderFn: (push: PushFn) => void, contentRenderFn: (push: PushFn) => void,
target: string, target: string,
disabled: boolean,
parentComponent: ComponentInternalInstance parentComponent: ComponentInternalInstance
) { ) {
parentPush('<!--portal-->') parentPush('<!--portal start-->')
const { getBuffer, push } = createBuffer()
contentRenderFn(push) let portalContent: SSRBufferItem
push(`<!---->`) // portal end anchor
if (disabled) {
contentRenderFn(parentPush)
portalContent = `<!---->`
} else {
const { getBuffer, push } = createBuffer()
contentRenderFn(push)
push(`<!---->`) // portal end anchor
portalContent = getBuffer()
}
const context = parentComponent.appContext.provides[ const context = parentComponent.appContext.provides[
ssrContextKey as any ssrContextKey as any
@ -18,8 +33,10 @@ export function ssrRenderPortal(
const portalBuffers = const portalBuffers =
context.__portalBuffers || (context.__portalBuffers = {}) context.__portalBuffers || (context.__portalBuffers = {})
if (portalBuffers[target]) { if (portalBuffers[target]) {
portalBuffers[target].push(getBuffer()) portalBuffers[target].push(portalContent)
} else { } else {
portalBuffers[target] = [getBuffer()] portalBuffers[target] = [portalContent]
} }
parentPush('<!--portal end-->')
} }

View File

@ -366,6 +366,7 @@ function renderPortalVNode(
parentComponent: ComponentInternalInstance parentComponent: ComponentInternalInstance
) { ) {
const target = vnode.props && vnode.props.target const target = vnode.props && vnode.props.target
const disabled = vnode.props && vnode.props.disabled
if (!target) { if (!target) {
warn(`[@vue/server-renderer] Portal is missing target prop.`) warn(`[@vue/server-renderer] Portal is missing target prop.`)
return [] return []
@ -386,6 +387,7 @@ function renderPortalVNode(
) )
}, },
target, target,
disabled || disabled === '',
parentComponent parentComponent
) )
} }