})
})
+ test('slot with slotted: true', async () => {
+ const ast = parseWithSlots(`<slot/>`, { slotted: true })
+ expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
+ type: NodeTypes.JS_CALL_EXPRESSION,
+ callee: RENDER_SLOT,
+ arguments: [`$slots`, `"default"`, `{}`, `undefined`, `true`]
+ })
+ })
+
test(`error on unexpected custom directive on <slot>`, () => {
const onError = jest.fn()
const source = `<slot v-foo />`
* SFC scoped styles ID
*/
scopeId?: string | null
+ /**
+ * Indicates this SFC template has used :slotted in its styles
+ * Defaults to `true` for backwards compatibility - SFC tooling should set it
+ * to `false` if no `:slotted` usage is detected in `<style>`
+ */
+ slotted?: boolean
/**
* SFC `<style vars>` injection string
* Should already be an object expression, e.g. `{ 'xxxx-color': color }`
isCustomElement = NOOP,
expressionPlugins = [],
scopeId = null,
+ slotted = true,
ssr = false,
ssrCssVars = ``,
bindingMetadata = EMPTY_OBJ,
isCustomElement,
expressionPlugins,
scopeId,
+ slotted,
ssr,
ssrCssVars,
bindingMetadata,
slotArgs.push(createFunctionExpression([], children, false, false, loc))
}
+ if (context.slotted) {
+ if (!slotProps) {
+ slotArgs.push(`{}`)
+ }
+ if (!children.length) {
+ slotArgs.push(`undefined`)
+ }
+ slotArgs.push(`true`)
+ }
+
node.codegenNode = createCallExpression(
context.helper(RENDER_SLOT),
slotArgs,
expect(errors.length).toBe(0)
})
+ test('slotted detection', async () => {
+ expect(parse(`<template>hi</template>`).descriptor.slotted).toBe(false)
+ expect(
+ parse(`<template>hi</template><style>h1{color:red;}</style>`).descriptor
+ .slotted
+ ).toBe(false)
+ expect(
+ parse(`<template>hi</template><style>:slotted(h1){color:red;}</style>`)
+ .descriptor.slotted
+ ).toBe(true)
+ expect(
+ parse(`<template>hi</template><style>::v-slotted(h1){color:red;}</style>`)
+ .descriptor.slotted
+ ).toBe(true)
+ })
+
test('error tolerance', () => {
const { errors } = parse(`<template>`)
expect(errors.length).toBe(1)
filename: string
id: string
scoped?: boolean
+ slotted?: boolean
isProd?: boolean
ssr?: boolean
ssrCssVars?: string[]
filename,
id,
scoped,
+ slotted,
inMap,
source,
ssr = false,
? genCssVarsFromList(ssrCssVars, shortId, isProd)
: '',
scopeId: scoped ? longId : undefined,
+ slotted,
...compilerOptions,
nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
filename,
styles: SFCStyleBlock[]
customBlocks: SFCBlock[]
cssVars: string[]
+ // whether the SFC uses :slotted() modifier.
+ // this is used as a compiler optimization hint.
+ slotted: boolean
}
export interface SFCParseResult {
scriptSetup: null,
styles: [],
customBlocks: [],
- cssVars: []
+ cssVars: [],
+ slotted: false
}
const errors: (CompilerError | SyntaxError)[] = []
warnExperimental(`v-bind() CSS variable injection`, 231)
}
+ // check if the SFC uses :slotted
+ const slottedRE = /(?:::v-|:)slotted\(/
+ descriptor.slotted = descriptor.styles.some(s => slottedRE.test(s.content))
+
const result = {
descriptor,
errors
"const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
- _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent, null)
+ _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent)
}"
`)
})
"const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
- _ssrRenderSlot(_ctx.$slots, \\"foo\\", {}, null, _push, _parent, null)
+ _ssrRenderSlot(_ctx.$slots, \\"foo\\", {}, null, _push, _parent)
}"
`)
})
"const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
- _ssrRenderSlot(_ctx.$slots, _ctx.bar.baz, {}, null, _push, _parent, null)
+ _ssrRenderSlot(_ctx.$slots, _ctx.bar.baz, {}, null, _push, _parent)
}"
`)
})
_ssrRenderSlot(_ctx.$slots, \\"foo\\", {
p: 1,
bar: \\"2\\"
- }, null, _push, _parent, null)
+ }, null, _push, _parent)
}"
`)
})
return function ssrRender(_ctx, _push, _parent, _attrs) {
_ssrRenderSlot(_ctx.$slots, \\"default\\", {}, () => {
_push(\`some \${_ssrInterpolate(_ctx.fallback)} content\`)
- }, _push, _parent, null)
+ }, _push, _parent)
}"
`)
})
`)
})
+ test('with scopeId + slotted:false', async () => {
+ expect(
+ compile(`<slot/>`, {
+ scopeId: 'hello',
+ slotted: false
+ }).code
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent)
+ }"
+ `)
+ })
+
test('with forwarded scopeId', async () => {
expect(
compile(`<Comp><slot/></Comp>`, {
_ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent, \\"hello-s\\" + _scopeId)
} else {
return [
- _renderSlot(_ctx.$slots, \\"default\\")
+ _renderSlot(_ctx.$slots, \\"default\\", {}, undefined, true)
]
}
}),
export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
if (isSlotOutlet(node)) {
const { slotName, slotProps } = processSlotOutlet(node, context)
+
+ const args = [
+ `_ctx.$slots`,
+ slotName,
+ slotProps || `{}`,
+ // fallback content placeholder. will be replaced in the process phase
+ `null`,
+ `_push`,
+ `_parent`
+ ]
+
+ // inject slot scope id if current template uses :slotted
+ if (context.scopeId && context.slotted !== false) {
+ args.push(`"${context.scopeId}-s"`)
+ }
+
node.ssrCodegenNode = createCallExpression(
context.helper(SSR_RENDER_SLOT),
- [
- `_ctx.$slots`,
- slotName,
- slotProps || `{}`,
- // fallback content placeholder. will be replaced in the process phase
- `null`,
- `_push`,
- `_parent`,
- context.scopeId ? `"${context.scopeId}-s"` : `null`
- ]
+ args
)
}
}
renderCall.arguments[3] = fallbackRenderFn
}
- // Forwarded <slot/>. Add slot scope id
+ // Forwarded <slot/>. Merge slot scope ids
if (context.withSlotScopeId) {
- const scopeId = renderCall.arguments[6] as string
- renderCall.arguments[6] =
- scopeId === `null` ? `_scopeId` : `${scopeId} + _scopeId`
+ const slotScopeId = renderCall.arguments[6]
+ renderCall.arguments[6] = slotScopeId
+ ? `${slotScopeId as string} + _scopeId`
+ : `_scopeId`
}
context.pushStatement(node.ssrCodegenNode!)
const Child = {
__scopeId: 'child',
render(this: any) {
- return h('div', renderSlot(this.$slots, 'default'))
+ return h('div', renderSlot(this.$slots, 'default', {}, undefined, true))
}
}
const Child2 = {
render(this: any) {
// <Wrapper><slot/></Wrapper>
return h(Wrapper, null, {
- default: withCtx(() => [renderSlot(this.$slots, 'default')])
+ default: withCtx(() => [
+ renderSlot(this.$slots, 'default', {}, undefined, true)
+ ])
})
}
}
render(h(Root), root)
expect(serializeInner(root)).toBe(
`<div class="wrapper" wrapper slotted root>` +
- `<div root wrapper-s slotted-s>hoisted</div>` +
- `<div root wrapper-s slotted-s>dynamic</div>` +
+ `<div root slotted-s>hoisted</div>` +
+ `<div root slotted-s>dynamic</div>` +
`</div>`
)
render(h(Root2), root2)
expect(serializeInner(root2)).toBe(
`<div class="wrapper" wrapper slotted root>` +
- `<div class="wrapper" wrapper root wrapper-s slotted-s>` +
- `<div root wrapper-s>hoisted</div>` +
- `<div root wrapper-s>dynamic</div>` +
+ `<div class="wrapper" wrapper root slotted-s>` +
+ `<div root>hoisted</div>` +
+ `<div root>dynamic</div>` +
`</div>` +
`</div>`
)
props: Data = {},
// this is not a user-facing function, so the fallback is always generated by
// the compiler and guaranteed to be a function returning an array
- fallback?: () => VNodeArrayChildren
+ fallback?: () => VNodeArrayChildren,
+ hasSlotted?: boolean
): VNode {
let slot = slots[name]
? PatchFlags.STABLE_FRAGMENT
: PatchFlags.BAIL
)
- // TODO (optimization) only add slot scope id if :slotted is used
- if (rendered.scopeId) {
+ if (hasSlotted && rendered.scopeId) {
rendered.slotScopeIds = [rendered.scopeId + '-s']
}
isRenderingCompiledSlot--