mirror of https://github.com/vuejs/core.git
fix: preserve empty slot anchor in vapor components in ssr
This commit is contained in:
parent
e6d037d8fe
commit
44867b14eb
|
@ -18,7 +18,9 @@ describe('insertion anchors', () => {
|
|||
expect(
|
||||
getCompiledString(
|
||||
`<component :is="'div'">
|
||||
<div><Comp/><Comp/><span/></div>
|
||||
<div>
|
||||
<Comp/><Comp/><span/>
|
||||
</div>
|
||||
</component>`,
|
||||
{ vapor: true },
|
||||
),
|
||||
|
@ -70,7 +72,11 @@ describe('insertion anchors', () => {
|
|||
expect(
|
||||
getCompiledString(
|
||||
`<component :is="'div'">
|
||||
<div><slot name="foo"/><slot/><span/></div>
|
||||
<div>
|
||||
<slot name="foo"/>
|
||||
<slot/>
|
||||
<span/>
|
||||
</div>
|
||||
</component>`,
|
||||
{ vapor: true },
|
||||
),
|
||||
|
@ -363,7 +369,9 @@ describe('insertion anchors', () => {
|
|||
expect(
|
||||
getCompiledString(
|
||||
`<component :is="'div'">
|
||||
<div><span v-for="item in items"/><span/></div>
|
||||
<div>
|
||||
<span v-for="item in items"/><span/>
|
||||
</div>
|
||||
</component>`,
|
||||
{ vapor: true },
|
||||
),
|
||||
|
@ -444,7 +452,13 @@ describe('insertion anchors', () => {
|
|||
test('mixed anchors in ssr slot vnode fallback', () => {
|
||||
expect(
|
||||
getCompiledString(
|
||||
`<component :is="'div'"><Comp/><span/><Comp/><span/><Comp/></component>`,
|
||||
`<component :is="'div'">
|
||||
<div>
|
||||
<Comp/><span/>
|
||||
<Comp/><span/>
|
||||
<Comp/>
|
||||
</div>
|
||||
</component>`,
|
||||
{
|
||||
vapor: true,
|
||||
},
|
||||
|
@ -454,13 +468,16 @@ describe('insertion anchors', () => {
|
|||
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent('div'), null, {
|
||||
default: _withCtx((_, _push, _parent, _scopeId) => {
|
||||
if (_push) {
|
||||
_push(\`<div\${_scopeId}><!--[p-->\`)
|
||||
_push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
|
||||
_push(\`<span\${_scopeId}></span>\`)
|
||||
_push(\`<!--p]--><span\${_scopeId}></span><!--[i-->\`)
|
||||
_push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
|
||||
_push(\`<span\${_scopeId}></span>\`)
|
||||
_push(\`<!--i]--><span\${_scopeId}></span><!--[a-->\`)
|
||||
_push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
|
||||
_push(\`<!--a]--></div>\`)
|
||||
} else {
|
||||
return [
|
||||
_createVNode("div", null, [
|
||||
_createCommentVNode("[p"),
|
||||
_createVNode(_component_Comp),
|
||||
_createCommentVNode("p]"),
|
||||
|
@ -472,6 +489,7 @@ describe('insertion anchors', () => {
|
|||
_createCommentVNode("[a"),
|
||||
_createVNode(_component_Comp),
|
||||
_createCommentVNode("a]")
|
||||
])
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
Namespaces,
|
||||
type NodeTransform,
|
||||
NodeTypes,
|
||||
type PlainElementNode,
|
||||
RESOLVE_DYNAMIC_COMPONENT,
|
||||
type ReturnStatement,
|
||||
type RootNode,
|
||||
|
@ -139,7 +140,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
|
|||
if (clonedNode.children.length) {
|
||||
buildSlots(clonedNode, context, (props, vFor, children) => {
|
||||
vnodeBranches.push(
|
||||
createVNodeSlotBranch(props, vFor, children, context),
|
||||
createVNodeSlotBranch(props, vFor, children, context, clonedNode),
|
||||
)
|
||||
return createFunctionExpression(undefined)
|
||||
})
|
||||
|
@ -302,6 +303,7 @@ function createVNodeSlotBranch(
|
|||
vFor: DirectiveNode | undefined,
|
||||
children: TemplateChildNode[],
|
||||
parentContext: TransformContext,
|
||||
parent: TemplateChildNode,
|
||||
): ReturnStatement {
|
||||
// apply a sub-transform using vnode-based transforms.
|
||||
const rawOptions = rawOptionsMap.get(parentContext.root)!
|
||||
|
@ -338,7 +340,7 @@ function createVNodeSlotBranch(
|
|||
}
|
||||
|
||||
if (parentContext.vapor) {
|
||||
children = injectVaporInsertionAnchors(children)
|
||||
children = injectVaporInsertionAnchors(children, parent)
|
||||
}
|
||||
|
||||
const wrapperNode: TemplateNode = {
|
||||
|
@ -395,8 +397,12 @@ function subTransform(
|
|||
|
||||
function injectVaporInsertionAnchors(
|
||||
children: TemplateChildNode[],
|
||||
parent: TemplateChildNode,
|
||||
): TemplateChildNode[] {
|
||||
if (isElementWithChildren(parent)) {
|
||||
processBlockNodeAnchor(children)
|
||||
}
|
||||
|
||||
const newChildren: TemplateChildNode[] = new Array(children.length * 3)
|
||||
let newIndex = 0
|
||||
|
||||
|
@ -439,12 +445,10 @@ function injectVaporInsertionAnchors(
|
|||
|
||||
// copy branch nodes
|
||||
for (let j = i; j <= lastBranchIndex; j++) {
|
||||
const node = children[j]
|
||||
const node = children[j] as PlainElementNode
|
||||
newChildren[newIndex++] = node
|
||||
|
||||
if (isElementWithChildren(node)) {
|
||||
node.children = injectVaporInsertionAnchors(node.children)
|
||||
}
|
||||
node.children = injectVaporInsertionAnchors(node.children, node)
|
||||
}
|
||||
|
||||
// inject anchor after branch nodes
|
||||
|
@ -464,9 +468,7 @@ function injectVaporInsertionAnchors(
|
|||
newChildren[newIndex++] = child
|
||||
if (anchor) newChildren[newIndex++] = createAnchorComment(`${anchor}]`)
|
||||
|
||||
if (isElementWithChildren(child)) {
|
||||
child.children = injectVaporInsertionAnchors(child.children)
|
||||
}
|
||||
child.children = injectVaporInsertionAnchors(child.children, child)
|
||||
}
|
||||
|
||||
newChildren.length = newIndex
|
||||
|
|
|
@ -3027,6 +3027,43 @@ describe('Vapor Mode hydration', () => {
|
|||
)
|
||||
})
|
||||
|
||||
test('forwarded slot with fallback', async () => {
|
||||
const data = reactive({
|
||||
foo: 'foo',
|
||||
})
|
||||
const { container } = await testHydration(
|
||||
`<template>
|
||||
<components.Parent/>
|
||||
</template>`,
|
||||
{
|
||||
Parent: `<template><components.Child><slot/></components.Child></template>`,
|
||||
Child: `<template><div><slot>{{data.foo}}</slot></div></template>`,
|
||||
},
|
||||
data,
|
||||
)
|
||||
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
|
||||
`
|
||||
"<div>
|
||||
<!--[a-->
|
||||
<!--[--><!--slot-->foo<!--]-->
|
||||
<!--slot--><!--a]-->
|
||||
</div>"
|
||||
`,
|
||||
)
|
||||
|
||||
data.foo = 'foo1'
|
||||
await nextTick()
|
||||
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
|
||||
`
|
||||
"<div>
|
||||
<!--[a-->
|
||||
<!--[--><!--slot-->foo1<!--]-->
|
||||
<!--slot--><!--a]-->
|
||||
</div>"
|
||||
`,
|
||||
)
|
||||
})
|
||||
|
||||
test('forwarded slot with empty content', async () => {
|
||||
const data = reactive({
|
||||
foo: 'foo',
|
||||
|
@ -3071,8 +3108,8 @@ describe('Vapor Mode hydration', () => {
|
|||
`
|
||||
"<div>
|
||||
<!--[p-->
|
||||
<!--[--><!--]-->
|
||||
<!--slot--><!--slot--><!--slot--><!--slot--><!--p]-->
|
||||
<!--[--><!--slot--><!--slot--><!--slot--><!--]-->
|
||||
<!--slot--><!--p]-->
|
||||
<div>foo</div></div>"
|
||||
`,
|
||||
)
|
||||
|
@ -3083,8 +3120,8 @@ describe('Vapor Mode hydration', () => {
|
|||
`
|
||||
"<div>
|
||||
<!--[p-->
|
||||
<!--[--><!--]-->
|
||||
<!--slot--><!--slot--><!--slot--><!--slot--><!--p]-->
|
||||
<!--[--><!--slot--><!--slot--><!--slot--><!--]-->
|
||||
<!--slot--><!--p]-->
|
||||
<div>bar</div></div>"
|
||||
`,
|
||||
)
|
||||
|
|
|
@ -145,6 +145,9 @@ export class DynamicFragment extends VaporFragment {
|
|||
}
|
||||
|
||||
hydrate = (label: string, isEmpty: boolean = false): void => {
|
||||
// avoid repeated hydration during rendering fallback
|
||||
if (this.anchor) return
|
||||
|
||||
const createAnchor = () => {
|
||||
const { parentNode, nextSibling } = findLastChild(this.nodes)!
|
||||
parentNode!.insertBefore(
|
||||
|
@ -156,14 +159,8 @@ export class DynamicFragment extends VaporFragment {
|
|||
|
||||
// manually create anchors for:
|
||||
// 1. else-if branch
|
||||
// 2. empty forwarded slot
|
||||
// (not present in SSR output)
|
||||
if (
|
||||
label === ELSE_IF_ANCHOR_LABEL ||
|
||||
(this.nodes instanceof DynamicFragment &&
|
||||
this.nodes.forwarded &&
|
||||
!isValidBlock(this.nodes))
|
||||
) {
|
||||
if (label === ELSE_IF_ANCHOR_LABEL) {
|
||||
createAnchor()
|
||||
} else {
|
||||
// for `v-if="false"`, the node will be an empty comment, use it as the anchor.
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
type SSRBufferItem,
|
||||
renderVNodeChildren,
|
||||
} from '../render'
|
||||
import { isArray } from '@vue/shared'
|
||||
import { isArray, isString } from '@vue/shared'
|
||||
|
||||
const { ensureValidVNode } = ssrUtils
|
||||
|
||||
|
@ -83,7 +83,20 @@ export function ssrRenderSlotInner(
|
|||
isEmptySlot = false
|
||||
} else {
|
||||
for (let i = 0; i < slotBuffer.length; i++) {
|
||||
if (!isComment(slotBuffer[i])) {
|
||||
const buffer = slotBuffer[i]
|
||||
|
||||
// preserve empty slot anchor in vapor components
|
||||
// DynamicFragment requires this anchor
|
||||
if (
|
||||
parentComponent.type.__vapor &&
|
||||
isString(buffer) &&
|
||||
buffer === '<!--slot-->'
|
||||
) {
|
||||
push(buffer)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!isComment(buffer)) {
|
||||
isEmptySlot = false
|
||||
break
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue