Skip to content

Commit 9ed9bf3

Browse files
committed
feat(portal): SSR support for portal disabled prop
1 parent 8ce3da0 commit 9ed9bf3

File tree

7 files changed

+126
-23
lines changed

7 files changed

+126
-23
lines changed

packages/compiler-core/src/utils.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,14 @@ export function findDir(
184184
export function findProp(
185185
node: ElementNode,
186186
name: string,
187-
dynamicOnly: boolean = false
187+
dynamicOnly: boolean = false,
188+
allowEmpty: boolean = false
188189
): ElementNode['props'][0] | undefined {
189190
for (let i = 0; i < node.props.length; i++) {
190191
const p = node.props[i]
191192
if (p.type === NodeTypes.ATTRIBUTE) {
192193
if (dynamicOnly) continue
193-
if (p.name === name && p.value) {
194+
if (p.name === name && (p.value || allowEmpty)) {
194195
return p
195196
}
196197
} else if (p.name === 'bind' && p.exp && isBindKey(p.arg, name)) {

packages/compiler-ssr/__tests__/ssrPortal.spec.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,33 @@ describe('ssr compile: portal', () => {
99
return function ssrRender(_ctx, _push, _parent) {
1010
_ssrRenderPortal(_push, (_push) => {
1111
_push(\`<div></div>\`)
12-
}, _ctx.target, _parent)
12+
}, _ctx.target, false, _parent)
1313
}"
1414
`)
1515
})
16+
17+
test('disabled prop handling', () => {
18+
expect(compile(`<portal :target="target" disabled><div/></portal>`).code)
19+
.toMatchInlineSnapshot(`
20+
"const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\")
21+
22+
return function ssrRender(_ctx, _push, _parent) {
23+
_ssrRenderPortal(_push, (_push) => {
24+
_push(\`<div></div>\`)
25+
}, _ctx.target, true, _parent)
26+
}"
27+
`)
28+
29+
expect(
30+
compile(`<portal :target="target" :disabled="foo"><div/></portal>`).code
31+
).toMatchInlineSnapshot(`
32+
"const { ssrRenderPortal: _ssrRenderPortal } = require(\\"@vue/server-renderer\\")
33+
34+
return function ssrRender(_ctx, _push, _parent) {
35+
_ssrRenderPortal(_push, (_push) => {
36+
_push(\`<div></div>\`)
37+
}, _ctx.target, _ctx.foo, _parent)
38+
}"
39+
`)
40+
})
1641
})

packages/compiler-ssr/src/transforms/ssrTransformPortal.ts

+17-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {
22
ComponentNode,
33
findProp,
4-
JSChildNode,
54
NodeTypes,
65
createSimpleExpression,
76
createFunctionExpression,
8-
createCallExpression
7+
createCallExpression,
8+
ExpressionNode
99
} from '@vue/compiler-dom'
1010
import {
1111
SSRTransformContext,
@@ -27,12 +27,14 @@ export function ssrProcessPortal(
2727
return
2828
}
2929

30-
let target: JSChildNode
31-
if (targetProp.type === NodeTypes.ATTRIBUTE && targetProp.value) {
32-
target = createSimpleExpression(targetProp.value.content, true)
33-
} else if (targetProp.type === NodeTypes.DIRECTIVE && targetProp.exp) {
34-
target = targetProp.exp
30+
let target: ExpressionNode | undefined
31+
if (targetProp.type === NodeTypes.ATTRIBUTE) {
32+
target =
33+
targetProp.value && createSimpleExpression(targetProp.value.content, true)
3534
} else {
35+
target = targetProp.exp
36+
}
37+
if (!target) {
3638
context.onError(
3739
createSSRCompilerError(
3840
SSRErrorCodes.X_SSR_NO_PORTAL_TARGET,
@@ -42,6 +44,13 @@ export function ssrProcessPortal(
4244
return
4345
}
4446

47+
const disabledProp = findProp(node, 'disabled', false, true /* allow empty */)
48+
const disabled = disabledProp
49+
? disabledProp.type === NodeTypes.ATTRIBUTE
50+
? `true`
51+
: disabledProp.exp || `false`
52+
: `false`
53+
4554
const contentRenderFn = createFunctionExpression(
4655
[`_push`],
4756
undefined, // Body is added later
@@ -55,6 +64,7 @@ export function ssrProcessPortal(
5564
`_push`,
5665
contentRenderFn,
5766
target,
67+
disabled,
5868
`_parent`
5969
])
6070
)

packages/runtime-core/src/components/Portal.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export const enum PortalMoveTypes {
2323
REORDER // moved in the main view
2424
}
2525

26+
const isDisabled = (props: VNode['props']): boolean =>
27+
props && (props.disabled || props.disabled === '')
28+
2629
const movePortal = (
2730
vnode: VNode,
2831
container: RendererElement,
@@ -43,7 +46,7 @@ const movePortal = (
4346
// if this is a re-order and portal is enabled (content is in target)
4447
// do not move children. So the opposite is: only move children if this
4548
// is not a reorder, or the portal is disabled
46-
if (!isReorder || (props && props.disabled)) {
49+
if (!isReorder || isDisabled(props)) {
4750
// Portal has either Array children or no children.
4851
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
4952
for (let i = 0; i < (children as VNode[]).length; i++) {
@@ -83,7 +86,7 @@ export const PortalImpl = {
8386
} = internals
8487

8588
const targetSelector = n2.props && n2.props.target
86-
const disabled = n2.props && n2.props.disabled
89+
const disabled = isDisabled(n2.props)
8790
const { shapeFlag, children } = n2
8891
if (n1 == null) {
8992
if (__DEV__ && isString(targetSelector) && !querySelector) {
@@ -140,7 +143,7 @@ export const PortalImpl = {
140143
const mainAnchor = (n2.anchor = n1.anchor)!
141144
const target = (n2.target = n1.target)!
142145
const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
143-
const wasDisabled = n1.props && n1.props.disabled
146+
const wasDisabled = isDisabled(n1.props)
144147
const currentContainer = wasDisabled ? container : target
145148
const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
146149

packages/server-renderer/__tests__/ssrPortal.spec.ts

+48-3
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,42 @@ describe('ssrRenderPortal', () => {
1717
_push(`<div>content</div>`)
1818
},
1919
'#target',
20+
false,
2021
_parent
2122
)
2223
}
2324
}),
2425
ctx
2526
)
26-
expect(html).toBe('<!--portal-->')
27+
expect(html).toBe('<!--portal start--><!--portal end-->')
2728
expect(ctx.portals!['#target']).toBe(`<div>content</div><!---->`)
2829
})
2930

31+
test('portal rendering (compiled + disabled)', async () => {
32+
const ctx: SSRContext = {}
33+
const html = await renderToString(
34+
createApp({
35+
data() {
36+
return { msg: 'hello' }
37+
},
38+
ssrRender(_ctx, _push, _parent) {
39+
ssrRenderPortal(
40+
_push,
41+
_push => {
42+
_push(`<div>content</div>`)
43+
},
44+
'#target',
45+
true,
46+
_parent
47+
)
48+
}
49+
}),
50+
ctx
51+
)
52+
expect(html).toBe('<!--portal start--><div>content</div><!--portal end-->')
53+
expect(ctx.portals!['#target']).toBe(`<!---->`)
54+
})
55+
3056
test('portal rendering (vnode)', async () => {
3157
const ctx: SSRContext = {}
3258
const html = await renderToString(
@@ -39,10 +65,27 @@ describe('ssrRenderPortal', () => {
3965
),
4066
ctx
4167
)
42-
expect(html).toBe('<!--portal-->')
68+
expect(html).toBe('<!--portal start--><!--portal end-->')
4369
expect(ctx.portals!['#target']).toBe('<span>hello</span><!---->')
4470
})
4571

72+
test('portal rendering (vnode + disabled)', async () => {
73+
const ctx: SSRContext = {}
74+
const html = await renderToString(
75+
h(
76+
Portal,
77+
{
78+
target: `#target`,
79+
disabled: true
80+
},
81+
h('span', 'hello')
82+
),
83+
ctx
84+
)
85+
expect(html).toBe('<!--portal start--><span>hello</span><!--portal end-->')
86+
expect(ctx.portals!['#target']).toBe(`<!---->`)
87+
})
88+
4689
test('multiple portals with same target', async () => {
4790
const ctx: SSRContext = {}
4891
const html = await renderToString(
@@ -58,7 +101,9 @@ describe('ssrRenderPortal', () => {
58101
]),
59102
ctx
60103
)
61-
expect(html).toBe('<div><!--portal--><!--portal--></div>')
104+
expect(html).toBe(
105+
'<div><!--portal start--><!--portal end--><!--portal start--><!--portal end--></div>'
106+
)
62107
expect(ctx.portals!['#target']).toBe(
63108
'<span>hello</span><!---->world<!---->'
64109
)
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,42 @@
11
import { ComponentInternalInstance, ssrContextKey } from 'vue'
2-
import { SSRContext, createBuffer, PushFn } from '../renderToString'
2+
import {
3+
SSRContext,
4+
createBuffer,
5+
PushFn,
6+
SSRBufferItem
7+
} from '../renderToString'
38

49
export function ssrRenderPortal(
510
parentPush: PushFn,
611
contentRenderFn: (push: PushFn) => void,
712
target: string,
13+
disabled: boolean,
814
parentComponent: ComponentInternalInstance
915
) {
10-
parentPush('<!--portal-->')
11-
const { getBuffer, push } = createBuffer()
12-
contentRenderFn(push)
13-
push(`<!---->`) // portal end anchor
16+
parentPush('<!--portal start-->')
17+
18+
let portalContent: SSRBufferItem
19+
20+
if (disabled) {
21+
contentRenderFn(parentPush)
22+
portalContent = `<!---->`
23+
} else {
24+
const { getBuffer, push } = createBuffer()
25+
contentRenderFn(push)
26+
push(`<!---->`) // portal end anchor
27+
portalContent = getBuffer()
28+
}
1429

1530
const context = parentComponent.appContext.provides[
1631
ssrContextKey as any
1732
] as SSRContext
1833
const portalBuffers =
1934
context.__portalBuffers || (context.__portalBuffers = {})
2035
if (portalBuffers[target]) {
21-
portalBuffers[target].push(getBuffer())
36+
portalBuffers[target].push(portalContent)
2237
} else {
23-
portalBuffers[target] = [getBuffer()]
38+
portalBuffers[target] = [portalContent]
2439
}
40+
41+
parentPush('<!--portal end-->')
2542
}

packages/server-renderer/src/renderToString.ts

+2
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ function renderPortalVNode(
366366
parentComponent: ComponentInternalInstance
367367
) {
368368
const target = vnode.props && vnode.props.target
369+
const disabled = vnode.props && vnode.props.disabled
369370
if (!target) {
370371
warn(`[@vue/server-renderer] Portal is missing target prop.`)
371372
return []
@@ -386,6 +387,7 @@ function renderPortalVNode(
386387
)
387388
},
388389
target,
390+
disabled || disabled === '',
389391
parentComponent
390392
)
391393
}

0 commit comments

Comments
 (0)