wip(srr): slot outlet
This commit is contained in:
parent
7a63103a11
commit
9b3b6962df
@ -147,11 +147,13 @@ export interface ComponentNode extends BaseElementNode {
|
|||||||
| ComponentCodegenNode
|
| ComponentCodegenNode
|
||||||
| CacheExpression // when cached by v-once
|
| CacheExpression // when cached by v-once
|
||||||
| undefined
|
| undefined
|
||||||
|
ssrCodegenNode?: CallExpression
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SlotOutletNode extends BaseElementNode {
|
export interface SlotOutletNode extends BaseElementNode {
|
||||||
tagType: ElementTypes.SLOT
|
tagType: ElementTypes.SLOT
|
||||||
codegenNode: SlotOutletCodegenNode | undefined | CacheExpression // when cached by v-once
|
codegenNode: SlotOutletCodegenNode | undefined | CacheExpression // when cached by v-once
|
||||||
|
ssrCodegenNode?: CallExpression
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TemplateNode extends BaseElementNode {
|
export interface TemplateNode extends BaseElementNode {
|
||||||
|
@ -35,14 +35,15 @@ export { transformBind } from './transforms/vBind'
|
|||||||
|
|
||||||
// exported for compiler-ssr
|
// exported for compiler-ssr
|
||||||
export { MERGE_PROPS } from './runtimeHelpers'
|
export { MERGE_PROPS } from './runtimeHelpers'
|
||||||
export { processIfBranches } from './transforms/vIf'
|
export { processIf } from './transforms/vIf'
|
||||||
export { processForNode, createForLoopParams } from './transforms/vFor'
|
export { processFor, createForLoopParams } from './transforms/vFor'
|
||||||
export {
|
export {
|
||||||
transformExpression,
|
transformExpression,
|
||||||
processExpression
|
processExpression
|
||||||
} from './transforms/transformExpression'
|
} from './transforms/transformExpression'
|
||||||
export { trackVForSlotScopes, trackSlotScopes } from './transforms/vSlot'
|
export { trackVForSlotScopes, trackSlotScopes } from './transforms/vSlot'
|
||||||
export { buildProps } from './transforms/transformElement'
|
export { buildProps } from './transforms/transformElement'
|
||||||
|
export { processSlotOutlet } from './transforms/transformSlotOutlet'
|
||||||
|
|
||||||
// utility, but need to rewrite typing to avoid dts relying on @vue/shared
|
// utility, but need to rewrite typing to avoid dts relying on @vue/shared
|
||||||
import { generateCodeFrame as _genCodeFrame } from '@vue/shared'
|
import { generateCodeFrame as _genCodeFrame } from '@vue/shared'
|
||||||
|
@ -1,78 +1,32 @@
|
|||||||
import { NodeTransform } from '../transform'
|
import { NodeTransform, TransformContext } from '../transform'
|
||||||
import {
|
import {
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
CallExpression,
|
CallExpression,
|
||||||
createCallExpression,
|
createCallExpression,
|
||||||
ExpressionNode
|
ExpressionNode,
|
||||||
|
SlotOutletNode
|
||||||
} from '../ast'
|
} from '../ast'
|
||||||
import { isSlotOutlet } from '../utils'
|
import { isSlotOutlet, findProp } from '../utils'
|
||||||
import { buildProps } from './transformElement'
|
import { buildProps, PropsExpression } from './transformElement'
|
||||||
import { createCompilerError, ErrorCodes } from '../errors'
|
import { createCompilerError, ErrorCodes } from '../errors'
|
||||||
import { RENDER_SLOT } from '../runtimeHelpers'
|
import { RENDER_SLOT } from '../runtimeHelpers'
|
||||||
|
|
||||||
export const transformSlotOutlet: NodeTransform = (node, context) => {
|
export const transformSlotOutlet: NodeTransform = (node, context) => {
|
||||||
if (isSlotOutlet(node)) {
|
if (isSlotOutlet(node)) {
|
||||||
const { props, children, loc } = node
|
const { children, loc } = node
|
||||||
const $slots = context.prefixIdentifiers ? `_ctx.$slots` : `$slots`
|
const { slotName, slotProps } = processSlotOutlet(node, context)
|
||||||
let slotName: string | ExpressionNode = `"default"`
|
|
||||||
|
|
||||||
// check for <slot name="xxx" OR :name="xxx" />
|
const slotArgs: CallExpression['arguments'] = [
|
||||||
let nameIndex: number = -1
|
context.prefixIdentifiers ? `_ctx.$slots` : `$slots`,
|
||||||
for (let i = 0; i < props.length; i++) {
|
slotName
|
||||||
const prop = props[i]
|
]
|
||||||
if (prop.type === NodeTypes.ATTRIBUTE) {
|
|
||||||
if (prop.name === `name` && prop.value) {
|
|
||||||
// static name="xxx"
|
|
||||||
slotName = JSON.stringify(prop.value.content)
|
|
||||||
nameIndex = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else if (prop.name === `bind`) {
|
|
||||||
const { arg, exp } = prop
|
|
||||||
if (
|
|
||||||
arg &&
|
|
||||||
exp &&
|
|
||||||
arg.type === NodeTypes.SIMPLE_EXPRESSION &&
|
|
||||||
arg.isStatic &&
|
|
||||||
arg.content === `name`
|
|
||||||
) {
|
|
||||||
// dynamic :name="xxx"
|
|
||||||
slotName = exp
|
|
||||||
nameIndex = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const slotArgs: CallExpression['arguments'] = [$slots, slotName]
|
if (slotProps) {
|
||||||
const propsWithoutName =
|
slotArgs.push(slotProps)
|
||||||
nameIndex > -1
|
|
||||||
? props.slice(0, nameIndex).concat(props.slice(nameIndex + 1))
|
|
||||||
: props
|
|
||||||
let hasProps = propsWithoutName.length > 0
|
|
||||||
if (hasProps) {
|
|
||||||
const { props: propsExpression, directives } = buildProps(
|
|
||||||
node,
|
|
||||||
context,
|
|
||||||
propsWithoutName
|
|
||||||
)
|
|
||||||
if (directives.length) {
|
|
||||||
context.onError(
|
|
||||||
createCompilerError(
|
|
||||||
ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
|
|
||||||
directives[0].loc
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (propsExpression) {
|
|
||||||
slotArgs.push(propsExpression)
|
|
||||||
} else {
|
|
||||||
hasProps = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (children.length) {
|
if (children.length) {
|
||||||
if (!hasProps) {
|
if (!slotProps) {
|
||||||
slotArgs.push(`{}`)
|
slotArgs.push(`{}`)
|
||||||
}
|
}
|
||||||
slotArgs.push(children)
|
slotArgs.push(children)
|
||||||
@ -85,3 +39,49 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SlotOutletProcessResult {
|
||||||
|
slotName: string | ExpressionNode
|
||||||
|
slotProps: PropsExpression | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processSlotOutlet(
|
||||||
|
node: SlotOutletNode,
|
||||||
|
context: TransformContext
|
||||||
|
): SlotOutletProcessResult {
|
||||||
|
let slotName: string | ExpressionNode = `"default"`
|
||||||
|
let slotProps: PropsExpression | undefined = undefined
|
||||||
|
|
||||||
|
// check for <slot name="xxx" OR :name="xxx" />
|
||||||
|
const name = findProp(node, 'name')
|
||||||
|
if (name) {
|
||||||
|
if (name.type === NodeTypes.ATTRIBUTE && name.value) {
|
||||||
|
// static name
|
||||||
|
slotName = JSON.stringify(name.value.content)
|
||||||
|
} else if (name.type === NodeTypes.DIRECTIVE && name.exp) {
|
||||||
|
// dynamic name
|
||||||
|
slotName = name.exp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsWithoutName = name
|
||||||
|
? node.props.filter(p => p !== name)
|
||||||
|
: node.props
|
||||||
|
if (propsWithoutName.length > 0) {
|
||||||
|
const { props, directives } = buildProps(node, context, propsWithoutName)
|
||||||
|
slotProps = props
|
||||||
|
if (directives.length) {
|
||||||
|
context.onError(
|
||||||
|
createCompilerError(
|
||||||
|
ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
|
||||||
|
directives[0].loc
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
slotName,
|
||||||
|
slotProps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -46,7 +46,7 @@ export const transformFor = createStructuralDirectiveTransform(
|
|||||||
'for',
|
'for',
|
||||||
(node, dir, context) => {
|
(node, dir, context) => {
|
||||||
const { helper } = context
|
const { helper } = context
|
||||||
return processForNode(node, dir, context, forNode => {
|
return processFor(node, dir, context, forNode => {
|
||||||
// create the loop render function expression now, and add the
|
// create the loop render function expression now, and add the
|
||||||
// iterator on exit after all children have been traversed
|
// iterator on exit after all children have been traversed
|
||||||
const renderExp = createCallExpression(helper(RENDER_LIST), [
|
const renderExp = createCallExpression(helper(RENDER_LIST), [
|
||||||
@ -138,7 +138,7 @@ export const transformFor = createStructuralDirectiveTransform(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// target-agnostic transform used for both Client and SSR
|
// target-agnostic transform used for both Client and SSR
|
||||||
export function processForNode(
|
export function processFor(
|
||||||
node: ElementNode,
|
node: ElementNode,
|
||||||
dir: DirectiveNode,
|
dir: DirectiveNode,
|
||||||
context: TransformContext,
|
context: TransformContext,
|
||||||
|
@ -41,7 +41,7 @@ import { injectProp } from '../utils'
|
|||||||
export const transformIf = createStructuralDirectiveTransform(
|
export const transformIf = createStructuralDirectiveTransform(
|
||||||
/^(if|else|else-if)$/,
|
/^(if|else|else-if)$/,
|
||||||
(node, dir, context) => {
|
(node, dir, context) => {
|
||||||
return processIfBranches(node, dir, context, (ifNode, branch, isRoot) => {
|
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
|
||||||
// Exit callback. Complete the codegenNode when all children have been
|
// Exit callback. Complete the codegenNode when all children have been
|
||||||
// transformed.
|
// transformed.
|
||||||
return () => {
|
return () => {
|
||||||
@ -72,7 +72,7 @@ export const transformIf = createStructuralDirectiveTransform(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// target-agnostic transform used for both Client and SSR
|
// target-agnostic transform used for both Client and SSR
|
||||||
export function processIfBranches(
|
export function processIf(
|
||||||
node: ElementNode,
|
node: ElementNode,
|
||||||
dir: DirectiveNode,
|
dir: DirectiveNode,
|
||||||
context: TransformContext,
|
context: TransformContext,
|
||||||
|
60
packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts
Normal file
60
packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { compile } from '../src'
|
||||||
|
|
||||||
|
describe('ssr: <slot>', () => {
|
||||||
|
test('basic', () => {
|
||||||
|
expect(compile(`<slot/>`).code).toMatchInlineSnapshot(`
|
||||||
|
"const { _renderSlot } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
|
_renderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent)
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('with name', () => {
|
||||||
|
expect(compile(`<slot name="foo" />`).code).toMatchInlineSnapshot(`
|
||||||
|
"const { _renderSlot } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
|
_renderSlot(_ctx.$slots, \\"foo\\", {}, null, _push, _parent)
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('with dynamic name', () => {
|
||||||
|
expect(compile(`<slot :name="bar.baz" />`).code).toMatchInlineSnapshot(`
|
||||||
|
"const { _renderSlot } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
|
_renderSlot(_ctx.$slots, _ctx.bar.baz, {}, null, _push, _parent)
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('with props', () => {
|
||||||
|
expect(compile(`<slot name="foo" :p="1" bar="2" />`).code)
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"const { _renderSlot } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
|
_renderSlot(_ctx.$slots, \\"foo\\", {
|
||||||
|
p: 1,
|
||||||
|
bar: \\"2\\"
|
||||||
|
}, null, _push, _parent)
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('with fallback', () => {
|
||||||
|
expect(compile(`<slot>some {{ fallback }} content</slot>`).code)
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"const { _renderSlot, _interpolate } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
|
_renderSlot(_ctx.$slots, \\"default\\", {}, () => {
|
||||||
|
_push(\`some \${_interpolate(_ctx.fallback)} content\`)
|
||||||
|
}, _push, _parent)
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
@ -15,8 +15,9 @@ import {
|
|||||||
} from '@vue/compiler-dom'
|
} from '@vue/compiler-dom'
|
||||||
import { isString, escapeHtml, NO } from '@vue/shared'
|
import { isString, escapeHtml, NO } from '@vue/shared'
|
||||||
import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
|
import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
|
||||||
import { processIf } from './transforms/ssrVIf'
|
import { ssrProcessIf } from './transforms/ssrVIf'
|
||||||
import { processFor } from './transforms/ssrVFor'
|
import { ssrProcessFor } from './transforms/ssrVFor'
|
||||||
|
import { ssrProcessSlotOutlet } from './transforms/ssrTransformSlotOutlet'
|
||||||
|
|
||||||
// Because SSR codegen output is completely different from client-side output
|
// Because SSR codegen output is completely different from client-side output
|
||||||
// (e.g. multiple elements can be concatenated into a single template literal
|
// (e.g. multiple elements can be concatenated into a single template literal
|
||||||
@ -119,7 +120,7 @@ export function processChildren(
|
|||||||
} else if (child.tagType === ElementTypes.COMPONENT) {
|
} else if (child.tagType === ElementTypes.COMPONENT) {
|
||||||
// TODO
|
// TODO
|
||||||
} else if (child.tagType === ElementTypes.SLOT) {
|
} else if (child.tagType === ElementTypes.SLOT) {
|
||||||
// TODO
|
ssrProcessSlotOutlet(child, context)
|
||||||
}
|
}
|
||||||
} else if (child.type === NodeTypes.TEXT) {
|
} else if (child.type === NodeTypes.TEXT) {
|
||||||
context.pushStringPart(escapeHtml(child.content))
|
context.pushStringPart(escapeHtml(child.content))
|
||||||
@ -128,9 +129,9 @@ export function processChildren(
|
|||||||
createCallExpression(context.helper(SSR_INTERPOLATE), [child.content])
|
createCallExpression(context.helper(SSR_INTERPOLATE), [child.content])
|
||||||
)
|
)
|
||||||
} else if (child.type === NodeTypes.IF) {
|
} else if (child.type === NodeTypes.IF) {
|
||||||
processIf(child, context)
|
ssrProcessIf(child, context)
|
||||||
} else if (child.type === NodeTypes.FOR) {
|
} else if (child.type === NodeTypes.FOR) {
|
||||||
processFor(child, context)
|
ssrProcessFor(child, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,49 @@
|
|||||||
import { NodeTransform } from '@vue/compiler-dom'
|
import {
|
||||||
|
NodeTransform,
|
||||||
|
isSlotOutlet,
|
||||||
|
processSlotOutlet,
|
||||||
|
createCallExpression,
|
||||||
|
SlotOutletNode,
|
||||||
|
createFunctionExpression,
|
||||||
|
createBlockStatement
|
||||||
|
} from '@vue/compiler-dom'
|
||||||
|
import { SSR_RENDER_SLOT } from '../runtimeHelpers'
|
||||||
|
import {
|
||||||
|
SSRTransformContext,
|
||||||
|
createChildContext,
|
||||||
|
processChildren
|
||||||
|
} from '../ssrCodegenTransform'
|
||||||
|
|
||||||
export const ssrTransformSlotOutlet: NodeTransform = () => {}
|
export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
|
||||||
|
if (isSlotOutlet(node)) {
|
||||||
|
const { slotName, slotProps } = processSlotOutlet(node, context)
|
||||||
|
node.ssrCodegenNode = createCallExpression(
|
||||||
|
context.helper(SSR_RENDER_SLOT),
|
||||||
|
[
|
||||||
|
`_ctx.$slots`,
|
||||||
|
slotName,
|
||||||
|
slotProps || `{}`,
|
||||||
|
`null`, // fallback content placeholder.
|
||||||
|
`_push`,
|
||||||
|
`_parent`
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ssrProcessSlotOutlet(
|
||||||
|
node: SlotOutletNode,
|
||||||
|
context: SSRTransformContext
|
||||||
|
) {
|
||||||
|
const renderCall = node.ssrCodegenNode!
|
||||||
|
// has fallback content
|
||||||
|
if (node.children.length) {
|
||||||
|
const childContext = createChildContext(context)
|
||||||
|
processChildren(node.children, childContext)
|
||||||
|
const fallbackRenderFn = createFunctionExpression([])
|
||||||
|
fallbackRenderFn.body = createBlockStatement(childContext.body)
|
||||||
|
// _renderSlot(slots, name, props, fallback, ...)
|
||||||
|
renderCall.arguments[3] = fallbackRenderFn
|
||||||
|
}
|
||||||
|
context.pushStatement(node.ssrCodegenNode!)
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
createStructuralDirectiveTransform,
|
createStructuralDirectiveTransform,
|
||||||
ForNode,
|
ForNode,
|
||||||
processForNode,
|
processFor,
|
||||||
createCallExpression,
|
createCallExpression,
|
||||||
createFunctionExpression,
|
createFunctionExpression,
|
||||||
createForLoopParams,
|
createForLoopParams,
|
||||||
@ -18,12 +18,12 @@ import { SSR_RENDER_LIST } from '../runtimeHelpers'
|
|||||||
// Plugin for the first transform pass, which simply constructs the AST node
|
// Plugin for the first transform pass, which simply constructs the AST node
|
||||||
export const ssrTransformFor = createStructuralDirectiveTransform(
|
export const ssrTransformFor = createStructuralDirectiveTransform(
|
||||||
'for',
|
'for',
|
||||||
processForNode
|
processFor
|
||||||
)
|
)
|
||||||
|
|
||||||
// This is called during the 2nd transform pass to construct the SSR-sepcific
|
// This is called during the 2nd transform pass to construct the SSR-sepcific
|
||||||
// codegen nodes.
|
// codegen nodes.
|
||||||
export function processFor(node: ForNode, context: SSRTransformContext) {
|
export function ssrProcessFor(node: ForNode, context: SSRTransformContext) {
|
||||||
const childContext = createChildContext(context)
|
const childContext = createChildContext(context)
|
||||||
const needFragmentWrapper =
|
const needFragmentWrapper =
|
||||||
node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT
|
node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
createStructuralDirectiveTransform,
|
createStructuralDirectiveTransform,
|
||||||
processIfBranches,
|
processIf,
|
||||||
IfNode,
|
IfNode,
|
||||||
createIfStatement,
|
createIfStatement,
|
||||||
createBlockStatement,
|
createBlockStatement,
|
||||||
@ -18,12 +18,12 @@ import {
|
|||||||
// Plugin for the first transform pass, which simply constructs the AST node
|
// Plugin for the first transform pass, which simply constructs the AST node
|
||||||
export const ssrTransformIf = createStructuralDirectiveTransform(
|
export const ssrTransformIf = createStructuralDirectiveTransform(
|
||||||
/^(if|else|else-if)$/,
|
/^(if|else|else-if)$/,
|
||||||
processIfBranches
|
processIf
|
||||||
)
|
)
|
||||||
|
|
||||||
// This is called during the 2nd transform pass to construct the SSR-sepcific
|
// This is called during the 2nd transform pass to construct the SSR-sepcific
|
||||||
// codegen nodes.
|
// codegen nodes.
|
||||||
export function processIf(node: IfNode, context: SSRTransformContext) {
|
export function ssrProcessIf(node: IfNode, context: SSRTransformContext) {
|
||||||
const [rootBranch] = node.branches
|
const [rootBranch] = node.branches
|
||||||
const ifStatement = createIfStatement(
|
const ifStatement = createIfStatement(
|
||||||
rootBranch.condition!,
|
rootBranch.condition!,
|
||||||
|
@ -7,11 +7,8 @@ import {
|
|||||||
ComponentOptions
|
ComponentOptions
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { escapeHtml } from '@vue/shared'
|
import { escapeHtml } from '@vue/shared'
|
||||||
import {
|
import { renderToString, renderComponent } from '../src/renderToString'
|
||||||
renderToString,
|
import { renderSlot } from '../src/helpers/renderSlot'
|
||||||
renderComponent,
|
|
||||||
renderSlot
|
|
||||||
} from '../src/renderToString'
|
|
||||||
|
|
||||||
describe('ssr: renderToString', () => {
|
describe('ssr: renderToString', () => {
|
||||||
test('should apply app context', async () => {
|
test('should apply app context', async () => {
|
||||||
@ -135,7 +132,16 @@ describe('ssr: renderToString', () => {
|
|||||||
props: ['msg'],
|
props: ['msg'],
|
||||||
ssrRender(ctx: any, push: any, parent: any) {
|
ssrRender(ctx: any, push: any, parent: any) {
|
||||||
push(`<div class="child">`)
|
push(`<div class="child">`)
|
||||||
renderSlot(ctx.$slots.default, { msg: 'from slot' }, push, parent)
|
renderSlot(
|
||||||
|
ctx.$slots,
|
||||||
|
'default',
|
||||||
|
{ msg: 'from slot' },
|
||||||
|
() => {
|
||||||
|
push(`fallback`)
|
||||||
|
},
|
||||||
|
push,
|
||||||
|
parent
|
||||||
|
)
|
||||||
push(`</div>`)
|
push(`</div>`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -169,6 +175,19 @@ describe('ssr: renderToString', () => {
|
|||||||
`<!----><span>from slot</span><!---->` +
|
`<!----><span>from slot</span><!---->` +
|
||||||
`</div></div>`
|
`</div></div>`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// test fallback
|
||||||
|
expect(
|
||||||
|
await renderToString(
|
||||||
|
createApp({
|
||||||
|
ssrRender(_ctx, push, parent) {
|
||||||
|
push(`<div>parent`)
|
||||||
|
push(renderComponent(Child, { msg: 'hello' }, null, parent))
|
||||||
|
push(`</div>`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).toBe(`<div>parent<div class="child"><!---->fallback<!----></div></div>`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('nested components with vnode slots', async () => {
|
test('nested components with vnode slots', async () => {
|
||||||
@ -176,7 +195,14 @@ describe('ssr: renderToString', () => {
|
|||||||
props: ['msg'],
|
props: ['msg'],
|
||||||
ssrRender(ctx: any, push: any, parent: any) {
|
ssrRender(ctx: any, push: any, parent: any) {
|
||||||
push(`<div class="child">`)
|
push(`<div class="child">`)
|
||||||
renderSlot(ctx.$slots.default, { msg: 'from slot' }, push, parent)
|
renderSlot(
|
||||||
|
ctx.$slots,
|
||||||
|
'default',
|
||||||
|
{ msg: 'from slot' },
|
||||||
|
null,
|
||||||
|
push,
|
||||||
|
parent
|
||||||
|
)
|
||||||
push(`</div>`)
|
push(`</div>`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
35
packages/server-renderer/src/helpers/renderSlot.ts
Normal file
35
packages/server-renderer/src/helpers/renderSlot.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Props, PushFn, renderVNodeChildren } from '../renderToString'
|
||||||
|
import { ComponentInternalInstance, Slot, Slots } from 'vue'
|
||||||
|
|
||||||
|
export type SSRSlots = Record<string, SSRSlot>
|
||||||
|
|
||||||
|
export type SSRSlot = (
|
||||||
|
props: Props,
|
||||||
|
push: PushFn,
|
||||||
|
parentComponent: ComponentInternalInstance | null
|
||||||
|
) => void
|
||||||
|
|
||||||
|
export function renderSlot(
|
||||||
|
slots: Slots | SSRSlots,
|
||||||
|
slotName: string,
|
||||||
|
slotProps: Props,
|
||||||
|
fallbackRenderFn: (() => void) | null,
|
||||||
|
push: PushFn,
|
||||||
|
parentComponent: ComponentInternalInstance | null = null
|
||||||
|
) {
|
||||||
|
const slotFn = slots[slotName]
|
||||||
|
// template-compiled slots are always rendered as fragments
|
||||||
|
push(`<!---->`)
|
||||||
|
if (slotFn) {
|
||||||
|
if (slotFn.length > 1) {
|
||||||
|
// only ssr-optimized slot fns accept more than 1 arguments
|
||||||
|
slotFn(slotProps, push, parentComponent)
|
||||||
|
} else {
|
||||||
|
// normal slot
|
||||||
|
renderVNodeChildren(push, (slotFn as Slot)(slotProps), parentComponent)
|
||||||
|
}
|
||||||
|
} else if (fallbackRenderFn) {
|
||||||
|
fallbackRenderFn()
|
||||||
|
}
|
||||||
|
push(`<!---->`)
|
||||||
|
}
|
@ -2,10 +2,8 @@
|
|||||||
export { renderToString } from './renderToString'
|
export { renderToString } from './renderToString'
|
||||||
|
|
||||||
// internal runtime helpers
|
// internal runtime helpers
|
||||||
export {
|
export { renderComponent as _renderComponent } from './renderToString'
|
||||||
renderComponent as _renderComponent,
|
export { renderSlot as _renderSlot } from './helpers/renderSlot'
|
||||||
renderSlot as _renderSlot
|
|
||||||
} from './renderToString'
|
|
||||||
export {
|
export {
|
||||||
renderClass as _renderClass,
|
renderClass as _renderClass,
|
||||||
renderStyle as _renderStyle,
|
renderStyle as _renderStyle,
|
||||||
|
@ -11,7 +11,6 @@ import {
|
|||||||
Portal,
|
Portal,
|
||||||
ShapeFlags,
|
ShapeFlags,
|
||||||
ssrUtils,
|
ssrUtils,
|
||||||
Slot,
|
|
||||||
Slots
|
Slots
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import {
|
import {
|
||||||
@ -23,6 +22,7 @@ import {
|
|||||||
escapeHtml
|
escapeHtml
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import { renderAttrs } from './helpers/renderAttrs'
|
import { renderAttrs } from './helpers/renderAttrs'
|
||||||
|
import { SSRSlots } from './helpers/renderSlot'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isVNode,
|
isVNode,
|
||||||
@ -41,8 +41,8 @@ const {
|
|||||||
type SSRBuffer = SSRBufferItem[]
|
type SSRBuffer = SSRBufferItem[]
|
||||||
type SSRBufferItem = string | ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
|
type SSRBufferItem = string | ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
|
||||||
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
|
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
|
||||||
type PushFn = (item: SSRBufferItem) => void
|
export type PushFn = (item: SSRBufferItem) => void
|
||||||
type Props = Record<string, unknown>
|
export type Props = Record<string, unknown>
|
||||||
|
|
||||||
function createBuffer() {
|
function createBuffer() {
|
||||||
let appendable = false
|
let appendable = false
|
||||||
@ -191,7 +191,7 @@ function renderVNode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderVNodeChildren(
|
export function renderVNodeChildren(
|
||||||
push: PushFn,
|
push: PushFn,
|
||||||
children: VNodeArrayChildren,
|
children: VNodeArrayChildren,
|
||||||
parentComponent: ComponentInternalInstance | null = null
|
parentComponent: ComponentInternalInstance | null = null
|
||||||
@ -255,29 +255,3 @@ function renderElement(
|
|||||||
push(`</${tag}>`)
|
push(`</${tag}>`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SSRSlots = Record<string, SSRSlot>
|
|
||||||
|
|
||||||
export type SSRSlot = (
|
|
||||||
props: Props,
|
|
||||||
push: PushFn,
|
|
||||||
parentComponent: ComponentInternalInstance | null
|
|
||||||
) => void
|
|
||||||
|
|
||||||
export function renderSlot(
|
|
||||||
slotFn: Slot | SSRSlot,
|
|
||||||
slotProps: Props,
|
|
||||||
push: PushFn,
|
|
||||||
parentComponent: ComponentInternalInstance | null = null
|
|
||||||
) {
|
|
||||||
// template-compiled slots are always rendered as fragments
|
|
||||||
push(`<!---->`)
|
|
||||||
if (slotFn.length > 1) {
|
|
||||||
// only ssr-optimized slot fns accept more than 1 arguments
|
|
||||||
slotFn(slotProps, push, parentComponent)
|
|
||||||
} else {
|
|
||||||
// normal slot
|
|
||||||
renderVNodeChildren(push, (slotFn as Slot)(slotProps), parentComponent)
|
|
||||||
}
|
|
||||||
push(`<!---->`)
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user