feat(portal): SSR support for portal disabled prop
This commit is contained in:
parent
8ce3da0104
commit
9ed9bf3687
@ -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)) {
|
||||||
|
@ -9,7 +9,32 @@ 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)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
@ -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`
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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<!---->'
|
||||||
)
|
)
|
||||||
|
@ -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-->')
|
||||||
|
|
||||||
|
let portalContent: SSRBufferItem
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
contentRenderFn(parentPush)
|
||||||
|
portalContent = `<!---->`
|
||||||
|
} else {
|
||||||
const { getBuffer, push } = createBuffer()
|
const { getBuffer, push } = createBuffer()
|
||||||
contentRenderFn(push)
|
contentRenderFn(push)
|
||||||
push(`<!---->`) // portal end anchor
|
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-->')
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user