diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap index caac138dcef..f53323247dc 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap @@ -111,6 +111,33 @@ export function render(_ctx) { }" `; +exports[`compiler: transform outlets > slot outlet with scopeId and slotted=false should generate noSlotted 1`] = ` +"import { createSlot as _createSlot } from 'vue'; + +export function render(_ctx) { + const n0 = _createSlot("default", null, null, undefined, true) + return n0 +}" +`; + +exports[`compiler: transform outlets > slot outlet with scopeId and slotted=true should not generate noSlotted 1`] = ` +"import { createSlot as _createSlot } from 'vue'; + +export function render(_ctx) { + const n0 = _createSlot("default", null) + return n0 +}" +`; + +exports[`compiler: transform outlets > slot outlet without scopeId should not generate noSlotted 1`] = ` +"import { createSlot as _createSlot } from 'vue'; + +export function render(_ctx) { + const n0 = _createSlot("default", null) + return n0 +}" +`; + exports[`compiler: transform outlets > statically named slot outlet 1`] = ` "import { createSlot as _createSlot } from 'vue'; diff --git a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts index 389c665a12f..39b2bdcc8c3 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts @@ -277,4 +277,64 @@ describe('compiler: transform outlets', () => { }, }) }) + + test('slot outlet with scopeId and slotted=false should generate noSlotted', () => { + const { ir, code } = compileWithSlotsOutlet(``, { + scopeId: 'test-scope', + slotted: false, + }) + expect(code).toMatchSnapshot() + expect(code).toContain('true') + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'default', + isStatic: true, + }, + props: [], + fallback: undefined, + noSlotted: true, + }) + }) + + test('slot outlet with scopeId and slotted=true should not generate noSlotted', () => { + const { ir, code } = compileWithSlotsOutlet(``, { + scopeId: 'test-scope', + slotted: true, + }) + expect(code).toMatchSnapshot() + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'default', + isStatic: true, + }, + props: [], + fallback: undefined, + noSlotted: false, + }) + }) + + test('slot outlet without scopeId should not generate noSlotted', () => { + const { ir, code } = compileWithSlotsOutlet(``, { + slotted: false, + }) + expect(code).toMatchSnapshot() + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'default', + isStatic: true, + }, + props: [], + fallback: undefined, + noSlotted: false, + }) + }) }) diff --git a/packages/compiler-vapor/src/generators/slotOutlet.ts b/packages/compiler-vapor/src/generators/slotOutlet.ts index dc992ae2334..17ed92d946a 100644 --- a/packages/compiler-vapor/src/generators/slotOutlet.ts +++ b/packages/compiler-vapor/src/generators/slotOutlet.ts @@ -12,7 +12,7 @@ export function genSlotOutlet( context: CodegenContext, ): CodeFragment[] { const { helper } = context - const { id, name, fallback, forwarded } = oper + const { id, name, fallback, forwarded, noSlotted } = oper const [frag, push] = buildCodeFragment() const nameExpr = name.isStatic @@ -32,6 +32,8 @@ export function genSlotOutlet( nameExpr, genRawProps(oper.props, context) || 'null', fallbackArg, + noSlotted && 'undefined', // instance + noSlotted && 'true', // noSlotted ), ) diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index 76ef7c53c49..6e6b11aa35f 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -221,6 +221,7 @@ export interface SlotOutletIRNode extends BaseIRNode { props: IRProps[] fallback?: BlockIRNode forwarded?: boolean + noSlotted?: boolean parent?: number anchor?: number append?: boolean diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index 75d0c26f4af..2b1b27c6313 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -108,6 +108,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { props: irProps, fallback, forwarded: context.inSlot, + noSlotted: !!(context.options.scopeId && !context.options.slotted), } } } diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index b15fe1e6960..9444c2efddf 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -516,7 +516,11 @@ export { type VaporInteropInterface } from './apiCreateApp' /** * @internal */ -export { type RendererInternals, MoveType, invalidateMount } from './renderer' +export { + type RendererInternals, + MoveType, + getInheritedScopeIds, +} from './renderer' /** * @internal */ diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 0cf7c351d0e..9cdc571921c 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -777,30 +777,9 @@ function baseCreateRenderer( hostSetScopeId(el, slotScopeIds[i]) } } - let subTree = parentComponent && parentComponent.subTree - if (subTree) { - if ( - __DEV__ && - subTree.patchFlag > 0 && - subTree.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT - ) { - subTree = - filterSingleRoot(subTree.children as VNodeArrayChildren) || subTree - } - if ( - vnode === subTree || - (isSuspense(subTree.type) && - (subTree.ssContent === vnode || subTree.ssFallback === vnode)) - ) { - const parentVNode = parentComponent!.vnode! - setScopeId( - el, - parentVNode, - parentVNode.scopeId, - parentVNode.slotScopeIds, - parentComponent!.parent, - ) - } + const inheritedScopeIds = getInheritedScopeIds(vnode, parentComponent) + for (let i = 0; i < inheritedScopeIds.length; i++) { + hostSetScopeId(el, inheritedScopeIds[i]) } } @@ -2792,3 +2771,54 @@ export function getVaporInterface( } return res! } + +/** + * shared between vdom and vapor + */ +export function getInheritedScopeIds( + vnode: VNode, + parentComponent: GenericComponentInstance | null, +): string[] { + const inheritedScopeIds: string[] = [] + + let currentParent = parentComponent + let currentVNode = vnode + + while (currentParent) { + let subTree = currentParent.subTree + if (!subTree) break + + if ( + __DEV__ && + subTree.patchFlag > 0 && + subTree.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT + ) { + subTree = + filterSingleRoot(subTree.children as VNodeArrayChildren) || subTree + } + + if ( + currentVNode === subTree || + (isSuspense(subTree.type) && + (subTree.ssContent === currentVNode || + subTree.ssFallback === currentVNode)) + ) { + const parentVNode = currentParent.vnode! + + if (parentVNode.scopeId) { + inheritedScopeIds.push(parentVNode.scopeId) + } + + if (parentVNode.slotScopeIds) { + inheritedScopeIds.push(...parentVNode.slotScopeIds) + } + + currentVNode = parentVNode + currentParent = currentParent.parent + } else { + break + } + } + + return inheritedScopeIds +} diff --git a/packages/runtime-vapor/__tests__/scopeId.spec.ts b/packages/runtime-vapor/__tests__/scopeId.spec.ts new file mode 100644 index 00000000000..10bad40af68 --- /dev/null +++ b/packages/runtime-vapor/__tests__/scopeId.spec.ts @@ -0,0 +1,617 @@ +import { createApp, h } from '@vue/runtime-dom' +import { + createComponent, + createDynamicComponent, + createSlot, + defineVaporComponent, + forwardedSlotCreator, + setInsertionState, + template, + vaporInteropPlugin, + withVaporCtx, +} from '../src' +import { makeRender } from './_utils' + +const define = makeRender() + +describe('scopeId', () => { + test('should attach scopeId to child component', () => { + const Child = defineVaporComponent({ + __scopeId: 'child', + setup() { + return template('
', true)() + }, + }) + + const { html } = define({ + __scopeId: 'parent', + setup() { + return createComponent(Child) + }, + }).render() + expect(html()).toBe(`
`) + }) + + test('should attach scopeId to child component with insertion state', () => { + const Child = defineVaporComponent({ + __scopeId: 'child', + setup() { + return template('
', true)() + }, + }) + + const { html } = define({ + __scopeId: 'parent', + setup() { + const t0 = template('
', true) + const n1 = t0() as any + setInsertionState(n1) + createComponent(Child) + return n1 + }, + }).render() + expect(html()).toBe(`
`) + }) + + test('should attach scopeId to nested child component', () => { + const Child = defineVaporComponent({ + __scopeId: 'child', + setup() { + return template('
', true)() + }, + }) + + const Parent = defineVaporComponent({ + __scopeId: 'parent', + setup() { + return createComponent(Child) + }, + }) + + const { html } = define({ + __scopeId: 'app', + setup() { + return createComponent(Parent) + }, + }).render() + expect(html()).toBe(`
`) + }) + + test('should not attach scopeId to nested multiple root components', () => { + const Child = defineVaporComponent({ + __scopeId: 'child', + setup() { + return template('
', true)() + }, + }) + + const Parent = defineVaporComponent({ + __scopeId: 'parent', + setup() { + const n0 = template('
')() + const n1 = createComponent(Child) + return [n0, n1] + }, + }) + + const { html } = define({ + __scopeId: 'app', + setup() { + return createComponent(Parent) + }, + }).render() + expect(html()).toBe(`
`) + }) + + test('should attach scopeId to nested child component with insertion state', () => { + const Child = defineVaporComponent({ + __scopeId: 'child', + setup() { + return template('
', true)() + }, + }) + + const Parent = defineVaporComponent({ + __scopeId: 'parent', + setup() { + return createComponent(Child) + }, + }) + + const { html } = define({ + __scopeId: 'app', + setup() { + const t0 = template('
', true) + const n1 = t0() as any + setInsertionState(n1) + createComponent(Parent) + return n1 + }, + }).render() + expect(html()).toBe( + `
`, + ) + }) + + test('should attach scopeId to dynamic component', () => { + const { html } = define({ + __scopeId: 'parent', + setup() { + return createDynamicComponent(() => 'button') + }, + }).render() + expect(html()).toBe(``) + }) + + test('should attach scopeId to dynamic component with insertion state', () => { + const { html } = define({ + __scopeId: 'parent', + setup() { + const t0 = template('
', true) + const n1 = t0() as any + setInsertionState(n1) + createDynamicComponent(() => 'button') + return n1 + }, + }).render() + expect(html()).toBe( + `
`, + ) + }) + + test('should attach scopeId to nested dynamic component', () => { + const Comp = defineVaporComponent({ + __scopeId: 'child', + setup() { + return createDynamicComponent(() => 'button', null, null, true) + }, + }) + const { html } = define({ + __scopeId: 'parent', + setup() { + return createComponent(Comp, null, null, true) + }, + }).render() + expect(html()).toBe( + ``, + ) + }) + + test('should attach scopeId to nested dynamic component with insertion state', () => { + const Comp = defineVaporComponent({ + __scopeId: 'child', + setup() { + return createDynamicComponent(() => 'button', null, null, true) + }, + }) + const { html } = define({ + __scopeId: 'parent', + setup() { + const t0 = template('
', true) + const n1 = t0() as any + setInsertionState(n1) + createComponent(Comp, null, null, true) + return n1 + }, + }).render() + expect(html()).toBe( + `
`, + ) + }) + + test.todo('should attach scopeId to suspense content', async () => {}) + + // :slotted basic + test('should work on slots', () => { + const Child = defineVaporComponent({ + __scopeId: 'child', + setup() { + const n1 = template('
', true)() as any + setInsertionState(n1) + createSlot('default', null) + return n1 + }, + }) + + const Child2 = defineVaporComponent({ + __scopeId: 'child2', + setup() { + return template('', true)() + }, + }) + + const { html } = define({ + __scopeId: 'parent', + setup() { + const n2 = createComponent( + Child, + null, + { + default: withVaporCtx(() => { + const n0 = template('
')() + const n1 = createComponent(Child2) + return [n0, n1] + }) as any, + }, + true, + ) + return n2 + }, + }).render() + + // slot content should have: + // - scopeId from parent + // - slotted scopeId (with `-s` postfix) from child (the tree owner) + expect(html()).toBe( + `
` + + `
` + + // component inside slot should have: + // - scopeId from template context + // - slotted scopeId from slot owner + // - its own scopeId + `` + + `` + + `
`, + ) + }) + + test(':slotted on forwarded slots', async () => { + const Wrapper = defineVaporComponent({ + __scopeId: 'wrapper', + setup() { + //
+ const n1 = template('
', true)() as any + setInsertionState(n1) + createSlot('default', null, undefined, undefined, true /* noSlotted */) + return n1 + }, + }) + + const Slotted = defineVaporComponent({ + __scopeId: 'slotted', + setup() { + // + const _createForwardedSlot = forwardedSlotCreator() + const n1 = createComponent( + Wrapper, + null, + { + default: withVaporCtx(() => { + const n0 = _createForwardedSlot('default', null) + return n0 + }) as any, + }, + true, + ) + return n1 + }, + }) + + const { html } = define({ + __scopeId: 'root', + setup() { + //
+ const n2 = createComponent( + Slotted, + null, + { + default: withVaporCtx(() => { + return template('
')() + }) as any, + }, + true, + ) + return n2 + }, + }).render() + + expect(html()).toBe( + `
` + + `
` + + `` + + `
`, + ) + }) +}) + +describe('vdom interop', () => { + test('vdom parent > vapor child', () => { + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return template('', true)() + }, + }) + + const VdomParent = { + __scopeId: 'vdom-parent', + setup() { + return () => h(VaporChild as any) + }, + } + + const App = { + setup() { + return () => h(VdomParent) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vdom parent > vapor child > vdom child', () => { + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h('button') + }, + } + + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return createComponent(VdomChild as any, null, null, true) + }, + }) + + const VdomParent = { + __scopeId: 'vdom-parent', + setup() { + return () => h(VaporChild as any) + }, + } + + const App = { + setup() { + return () => h(VdomParent) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vdom parent > vapor child > vapor child > vdom child', () => { + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h('button') + }, + } + + const NestedVaporChild = defineVaporComponent({ + __scopeId: 'nested-vapor-child', + setup() { + return createComponent(VdomChild as any, null, null, true) + }, + }) + + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return createComponent(NestedVaporChild as any, null, null, true) + }, + }) + + const VdomParent = { + __scopeId: 'vdom-parent', + setup() { + return () => h(VaporChild as any) + }, + } + + const App = { + setup() { + return () => h(VdomParent) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vdom parent > vapor dynamic child', () => { + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return createDynamicComponent(() => 'button', null, null, true) + }, + }) + + const VdomParent = { + __scopeId: 'vdom-parent', + setup() { + return () => h(VaporChild as any) + }, + } + + const App = { + setup() { + return () => h(VdomParent) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vapor parent > vdom child', () => { + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h('button') + }, + } + + const VaporParent = defineVaporComponent({ + __scopeId: 'vapor-parent', + setup() { + return createComponent(VdomChild as any, null, null, true) + }, + }) + + const App = { + setup() { + return () => h(VaporParent as any) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vapor parent > vdom child > vapor child', () => { + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return template('', true)() + }, + }) + + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h(VaporChild as any) + }, + } + + const VaporParent = defineVaporComponent({ + __scopeId: 'vapor-parent', + setup() { + return createComponent(VdomChild as any, null, null, true) + }, + }) + + const App = { + setup() { + return () => h(VaporParent as any) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vapor parent > vdom child > vdom child > vapor child', () => { + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return template('', true)() + }, + }) + + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h(VaporChild as any) + }, + } + + const VdomParent = { + __scopeId: 'vdom-parent', + setup() { + return () => h(VdomChild as any) + }, + } + + const VaporParent = defineVaporComponent({ + __scopeId: 'vapor-parent', + setup() { + return createComponent(VdomParent as any, null, null, true) + }, + }) + + const App = { + setup() { + return () => h(VaporParent as any) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vapor parent > vapor slot > vdom child', () => { + const VaporSlot = defineVaporComponent({ + __scopeId: 'vapor-slot', + setup() { + const n1 = template('
', true)() as any + setInsertionState(n1) + createSlot('default', null) + return n1 + }, + }) + + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h('span') + }, + } + + const VaporParent = defineVaporComponent({ + __scopeId: 'vapor-parent', + setup() { + const n2 = createComponent( + VaporSlot, + null, + { + default: withVaporCtx(() => { + const n0 = template('
')() + const n1 = createComponent(VdomChild) + return [n0, n1] + }) as any, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => h(VaporParent as any) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + `
` + + `
` + + `` + + `` + + `
`, + ) + }) +}) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index c4c2f0e188a..729ebee16fc 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -11,6 +11,7 @@ import { type TransitionHooks, type TransitionProps, type TransitionState, + getInheritedScopeIds, performTransitionEnter, performTransitionLeave, } from '@vue/runtime-dom' @@ -220,3 +221,48 @@ export function isFragmentBlock(block: Block): boolean { } return false } + +export function setScopeId(block: Block, scopeIds: string[]): void { + if (block instanceof Element) { + for (const id of scopeIds) { + block.setAttribute(id, '') + } + } else if (isVaporComponent(block)) { + setScopeId(block.block, scopeIds) + } else if (isArray(block)) { + for (const b of block) { + setScopeId(b, scopeIds) + } + } else if (isFragment(block)) { + setScopeId(block.nodes, scopeIds) + } +} + +export function setComponentScopeId(instance: VaporComponentInstance): void { + const parent = instance.parent + if (!parent) return + // prevent setting scopeId on multi-root fragments + if (isArray(instance.block) && instance.block.length > 1) return + + const scopeIds: string[] = [] + + const scopeId = parent.type.__scopeId + if (scopeId) { + scopeIds.push(scopeId) + } + + // inherit scopeId from vdom parent + if ( + parent.subTree && + (parent.subTree.component as any) === instance && + parent.vnode!.scopeId + ) { + scopeIds.push(parent.vnode!.scopeId) + const inheritedScopeIds = getInheritedScopeIds(parent.vnode!, parent.parent) + scopeIds.push(...inheritedScopeIds) + } + + if (scopeIds.length > 0) { + setScopeId(instance.block, scopeIds) + } +} diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 1952b31048d..e9e288031f4 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -28,7 +28,14 @@ import { unregisterHMR, warn, } from '@vue/runtime-dom' -import { type Block, insert, isBlock, remove } from './block' +import { + type Block, + insert, + isBlock, + remove, + setComponentScopeId, + setScopeId, +} from './block' import { type ShallowRef, markRaw, @@ -382,8 +389,6 @@ export function setupComponent( } } - // TODO: scopeid - setActiveSub(prevSub) setCurrentInstance(...prevInstance) @@ -640,6 +645,11 @@ export function createComponentWithFallback( // mark single root ;(el as any).$root = isSingleRoot + if (!isHydrating) { + const scopeId = currentInstance!.type.__scopeId + if (scopeId) setScopeId(el, [scopeId]) + } + if (rawProps) { const setFn = () => setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)]) @@ -690,6 +700,7 @@ export function mountComponent( if (instance.bm) invokeArrayFns(instance.bm) if (!isHydrating) { insert(instance.block, parent, anchor) + setComponentScopeId(instance) } if (instance.m) queuePostFlushCb(instance.m!) if ( diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 346c272b6d5..6b29ad1ae0d 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -1,5 +1,5 @@ import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared' -import { type Block, type BlockFn, insert } from './block' +import { type Block, type BlockFn, insert, setScopeId } from './block' import { rawPropsProxyHandlers } from './componentProps' import { currentInstance, isRef, setCurrentInstance } from '@vue/runtime-dom' import type { LooseRawProps, VaporComponentInstance } from './component' @@ -17,6 +17,23 @@ import { } from './dom/hydration' import { DynamicFragment, type VaporFragment } from './fragment' +/** + * Current slot scopeIds for vdom interop + * @internal + */ +export let currentSlotScopeIds: string[] | null = null + +/** + * @internal + */ +export function setCurrentSlotScopeIds( + scopeIds: string[] | null, +): string[] | null { + const prev = currentSlotScopeIds + currentSlotScopeIds = scopeIds + return prev +} + export type RawSlots = Record & { $?: DynamicSlotSource[] } @@ -116,7 +133,7 @@ export function forwardedSlotCreator(): ( ) => Block { const instance = currentInstance as VaporComponentInstance return (name, rawProps, fallback) => - createSlot(name, rawProps, fallback, instance) + createSlot(name, rawProps, fallback, instance, false /* noSlotted */) } export function createSlot( @@ -124,6 +141,7 @@ export function createSlot( rawProps?: LooseRawProps | null, fallback?: VaporSlot, i?: VaporComponentInstance, + noSlotted?: boolean, ): Block { const _insertionParent = insertionParent const _insertionAnchor = insertionAnchor @@ -152,13 +170,36 @@ export function createSlot( ? new DynamicFragment('slot') : new DynamicFragment() const isDynamicName = isFunction(name) + + // Calculate slotScopeIds once (for vdom interop) + const slotScopeIds: string[] = [] + if (!noSlotted) { + const scopeId = instance!.type.__scopeId + if (scopeId) { + slotScopeIds.push(`${scopeId}-s`) + } + } + const renderSlot = () => { const slot = getSlot(rawSlots, isFunction(name) ? name() : name) if (slot) { fragment.fallback = fallback - // create and cache bound version of the slot to make it stable + // Create and cache bound version of the slot to make it stable // so that we avoid unnecessary updates if it resolves to the same slot - fragment.update(slot._bound || (slot._bound = () => slot(slotProps))) + + fragment.update( + slot._bound || + (slot._bound = () => { + const prevSlotScopeIds = setCurrentSlotScopeIds( + slotScopeIds.length > 0 ? slotScopeIds : null, + ) + try { + return slot(slotProps) + } finally { + setCurrentSlotScopeIds(prevSlotScopeIds) + } + }), + ) } else { fragment.update(fallback) } @@ -173,6 +214,13 @@ export function createSlot( } if (!isHydrating) { + if (!noSlotted) { + const scopeId = instance.type.__scopeId + if (scopeId) { + setScopeId(fragment, [`${scopeId}-s`]) + } + } + if (_insertionParent) insert(fragment, _insertionParent, _insertionAnchor) } else { if (fragment.insert) { diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index d3cb8d243a8..a44c078e6aa 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -59,6 +59,7 @@ import { } from '@vue/shared' import { type RawProps, rawPropsProxyHandlers } from './componentProps' import type { RawSlots, VaporSlot } from './componentSlots' +import { currentSlotScopeIds } from './componentSlots' import { renderEffect } from './renderEffect' import { _next, createTextNode } from './dom/node' import { optimizePropertyLookup } from './dom/prop' @@ -331,6 +332,9 @@ function createVDOMComponent( frag.nodes = vnode.el as any } + vnode.scopeId = parentInstance && parentInstance.type.__scopeId! + vnode.slotScopeIds = currentSlotScopeIds + frag.insert = (parentNode, anchor, transition) => { if (isHydrating) return if (vnode.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { @@ -485,6 +489,9 @@ function renderVDOMSlot( parentNode!, anchor, parentComponent as any, + null, // parentSuspense + undefined, // namespace + vnode!.slotScopeIds, // pass slotScopeIds for :slotted styles ) oldVNode = vnode! frag.nodes = vnode!.el as any