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