Merge branch 'edison/feat/fowardedSlots' into edison/feat/setScopeId

This commit is contained in:
daiwei 2025-06-08 15:28:08 +08:00
commit 1169db8c81
13 changed files with 290 additions and 9 deletions

View File

@ -103,6 +103,97 @@ export function render(_ctx) {
}" }"
`; `;
exports[`compiler: transform slot > forwarded slots > <slot w/ nested component> 1`] = `
"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
export function render(_ctx) {
const _createForwardedSlot = _forwardedSlotCreator()
const _component_Comp = _resolveComponent("Comp")
const n2 = _createComponentWithFallback(_component_Comp, null, {
"default": () => {
const n1 = _createComponentWithFallback(_component_Comp, null, {
"default": () => {
const n0 = _createForwardedSlot("default", null)
return n0
}
})
return n1
}
}, true)
return n2
}"
`;
exports[`compiler: transform slot > forwarded slots > <slot> tag only 1`] = `
"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
export function render(_ctx) {
const _createForwardedSlot = _forwardedSlotCreator()
const _component_Comp = _resolveComponent("Comp")
const n1 = _createComponentWithFallback(_component_Comp, null, {
"default": () => {
const n0 = _createForwardedSlot("default", null)
return n0
}
}, true)
return n1
}"
`;
exports[`compiler: transform slot > forwarded slots > <slot> tag w/ template 1`] = `
"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
export function render(_ctx) {
const _createForwardedSlot = _forwardedSlotCreator()
const _component_Comp = _resolveComponent("Comp")
const n2 = _createComponentWithFallback(_component_Comp, null, {
"default": () => {
const n0 = _createForwardedSlot("default", null)
return n0
}
}, true)
return n2
}"
`;
exports[`compiler: transform slot > forwarded slots > <slot> tag w/ v-for 1`] = `
"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createFor as _createFor, createComponentWithFallback as _createComponentWithFallback } from 'vue';
export function render(_ctx) {
const _createForwardedSlot = _forwardedSlotCreator()
const _component_Comp = _resolveComponent("Comp")
const n3 = _createComponentWithFallback(_component_Comp, null, {
"default": () => {
const n0 = _createFor(() => (_ctx.b), (_for_item0) => {
const n2 = _createForwardedSlot("default", null)
return n2
})
return n0
}
}, true)
return n3
}"
`;
exports[`compiler: transform slot > forwarded slots > <slot> tag w/ v-if 1`] = `
"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createIf as _createIf, createComponentWithFallback as _createComponentWithFallback } from 'vue';
export function render(_ctx) {
const _createForwardedSlot = _forwardedSlotCreator()
const _component_Comp = _resolveComponent("Comp")
const n3 = _createComponentWithFallback(_component_Comp, null, {
"default": () => {
const n0 = _createIf(() => (_ctx.ok), () => {
const n2 = _createForwardedSlot("default", null)
return n2
})
return n0
}
}, true)
return n3
}"
`;
exports[`compiler: transform slot > implicit default slot 1`] = ` exports[`compiler: transform slot > implicit default slot 1`] = `
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; "import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
const t0 = _template("<div></div>") const t0 = _template("<div></div>")

View File

@ -409,6 +409,35 @@ describe('compiler: transform slot', () => {
}) })
}) })
describe('forwarded slots', () => {
test('<slot> tag only', () => {
const { code } = compileWithSlots(`<Comp><slot/></Comp>`)
expect(code).toMatchSnapshot()
})
test('<slot> tag w/ v-if', () => {
const { code } = compileWithSlots(`<Comp><slot v-if="ok"/></Comp>`)
expect(code).toMatchSnapshot()
})
test('<slot> tag w/ v-for', () => {
const { code } = compileWithSlots(`<Comp><slot v-for="a in b"/></Comp>`)
expect(code).toMatchSnapshot()
})
test('<slot> tag w/ template', () => {
const { code } = compileWithSlots(
`<Comp><template #default><slot/></template></Comp>`,
)
expect(code).toMatchSnapshot()
})
test('<slot w/ nested component>', () => {
const { code } = compileWithSlots(`<Comp><Comp><slot/></Comp></Comp>`)
expect(code).toMatchSnapshot()
})
})
describe('errors', () => { describe('errors', () => {
test('error on extraneous children w/ named default slot', () => { test('error on extraneous children w/ named default slot', () => {
const onError = vi.fn() const onError = vi.fn()

View File

@ -18,6 +18,7 @@ import {
genCall, genCall,
} from './generators/utils' } from './generators/utils'
import { setTemplateRefIdent } from './generators/templateRef' import { setTemplateRefIdent } from './generators/templateRef'
import { createForwardedSlotIdent } from './generators/slotOutlet'
export type CodegenOptions = Omit<BaseCodegenOptions, 'optimizeImports'> export type CodegenOptions = Omit<BaseCodegenOptions, 'optimizeImports'>
@ -129,6 +130,12 @@ export function generate(
`const ${setTemplateRefIdent} = ${context.helper('createTemplateRefSetter')}()`, `const ${setTemplateRefIdent} = ${context.helper('createTemplateRefSetter')}()`,
) )
} }
if (ir.hasForwardedSlot) {
push(
NEWLINE,
`const ${createForwardedSlotIdent} = ${context.helper('forwardedSlotCreator')}()`,
)
}
push(...genBlockContent(ir.block, context, true)) push(...genBlockContent(ir.block, context, true))
push(INDENT_END, NEWLINE) push(INDENT_END, NEWLINE)

View File

@ -5,12 +5,14 @@ import { genExpression } from './expression'
import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils' import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
import { genRawProps } from './component' import { genRawProps } from './component'
export const createForwardedSlotIdent = `_createForwardedSlot`
export function genSlotOutlet( export function genSlotOutlet(
oper: SlotOutletIRNode, oper: SlotOutletIRNode,
context: CodegenContext, context: CodegenContext,
): CodeFragment[] { ): CodeFragment[] {
const { helper } = context const { helper } = context
const { id, name, fallback } = oper const { id, name, fallback, forwarded } = oper
const [frag, push] = buildCodeFragment() const [frag, push] = buildCodeFragment()
const nameExpr = name.isStatic const nameExpr = name.isStatic
@ -26,7 +28,7 @@ export function genSlotOutlet(
NEWLINE, NEWLINE,
`const n${id} = `, `const n${id} = `,
...genCall( ...genCall(
helper('createSlot'), forwarded ? createForwardedSlotIdent : helper('createSlot'),
nameExpr, nameExpr,
genRawProps(oper.props, context) || 'null', genRawProps(oper.props, context) || 'null',
fallbackArg, fallbackArg,

View File

@ -66,6 +66,7 @@ export interface RootIRNode {
directive: Set<string> directive: Set<string>
block: BlockIRNode block: BlockIRNode
hasTemplateRef: boolean hasTemplateRef: boolean
hasForwardedSlot: boolean
} }
export interface IfIRNode extends BaseIRNode { export interface IfIRNode extends BaseIRNode {
@ -209,6 +210,7 @@ export interface SlotOutletIRNode extends BaseIRNode {
name: SimpleExpressionNode name: SimpleExpressionNode
props: IRProps[] props: IRProps[]
fallback?: BlockIRNode fallback?: BlockIRNode
forwarded?: boolean
parent?: number parent?: number
anchor?: number anchor?: number
} }

View File

@ -76,6 +76,7 @@ export class TransformContext<T extends AllNode = AllNode> {
inVOnce: boolean = false inVOnce: boolean = false
inVFor: number = 0 inVFor: number = 0
inSlot: boolean = false
comment: CommentNode[] = [] comment: CommentNode[] = []
component: Set<string> = this.ir.component component: Set<string> = this.ir.component
@ -230,6 +231,7 @@ export function transform(
directive: new Set(), directive: new Set(),
block: newBlock(node), block: newBlock(node),
hasTemplateRef: false, hasTemplateRef: false,
hasForwardedSlot: false,
} }
const context = new TransformContext(ir, node, options) const context = new TransformContext(ir, node, options)

View File

@ -99,6 +99,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
} }
return () => { return () => {
if (context.inSlot) context.ir.hasForwardedSlot = true
exitBlock && exitBlock() exitBlock && exitBlock()
context.dynamic.operation = { context.dynamic.operation = {
type: IRNodeTypes.SLOT_OUTLET_NODE, type: IRNodeTypes.SLOT_OUTLET_NODE,
@ -106,6 +107,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
name: slotName, name: slotName,
props: irProps, props: irProps,
fallback, fallback,
forwarded: context.inSlot,
} }
} }
} }

View File

@ -237,7 +237,14 @@ function createSlotBlock(
const block: SlotBlockIRNode = newBlock(slotNode) const block: SlotBlockIRNode = newBlock(slotNode)
block.props = dir && dir.exp block.props = dir && dir.exp
const exitBlock = context.enterBlock(block) const exitBlock = context.enterBlock(block)
return [block, exitBlock] context.inSlot = true
return [
block,
() => {
context.inSlot = false
exitBlock()
},
]
} }
function isNonWhitespaceContent(node: TemplateChildNode): boolean { function isNonWhitespaceContent(node: TemplateChildNode): boolean {

View File

@ -7,6 +7,7 @@ import {
createSlot, createSlot,
createVaporApp, createVaporApp,
defineVaporComponent, defineVaporComponent,
forwardedSlotCreator,
insert, insert,
prepend, prepend,
renderEffect, renderEffect,
@ -15,7 +16,7 @@ import {
import { currentInstance, nextTick, ref } from '@vue/runtime-dom' import { currentInstance, nextTick, ref } from '@vue/runtime-dom'
import { makeRender } from './_utils' import { makeRender } from './_utils'
import type { DynamicSlot } from '../src/componentSlots' import type { DynamicSlot } from '../src/componentSlots'
import { setElementText } from '../src/dom/prop' import { setElementText, setText } from '../src/dom/prop'
const define = makeRender<any>() const define = makeRender<any>()
@ -503,4 +504,106 @@ describe('component: slots', () => {
expect(host.innerHTML).toBe('<div><h1></h1><!--slot--></div>') expect(host.innerHTML).toBe('<div><h1></h1><!--slot--></div>')
}) })
}) })
describe('forwarded slot', () => {
test('should work', async () => {
const Child = defineVaporComponent({
setup() {
return createSlot('foo', null)
},
})
const Parent = defineVaporComponent({
setup() {
const createForwardedSlot = forwardedSlotCreator()
const n2 = createComponent(
Child,
null,
{
foo: () => {
return createForwardedSlot('foo', null)
},
},
true,
)
return n2
},
})
const foo = ref('foo')
const { host } = define({
setup() {
const n2 = createComponent(
Parent,
null,
{
foo: () => {
const n0 = template(' ')() as any
renderEffect(() => setText(n0, foo.value))
return n0
},
},
true,
)
return n2
},
}).render()
expect(host.innerHTML).toBe('foo<!--slot--><!--slot-->')
foo.value = 'bar'
await nextTick()
expect(host.innerHTML).toBe('bar<!--slot--><!--slot-->')
})
test('mixed with non-forwarded slot', async () => {
const Child = defineVaporComponent({
setup() {
return [createSlot('foo', null)]
},
})
const Parent = defineVaporComponent({
setup() {
const createForwardedSlot = forwardedSlotCreator()
const n2 = createComponent(Child, null, {
foo: () => {
const n0 = createForwardedSlot('foo', null)
return n0
},
})
const n3 = createSlot('default', null)
return [n2, n3]
},
})
const foo = ref('foo')
const { host } = define({
setup() {
const n2 = createComponent(
Parent,
null,
{
foo: () => {
const n0 = template(' ')() as any
renderEffect(() => setText(n0, foo.value))
return n0
},
default: () => {
const n3 = template(' ')() as any
renderEffect(() => setText(n3, foo.value))
return n3
},
},
true,
)
return n2
},
}).render()
expect(host.innerHTML).toBe('foo<!--slot--><!--slot-->foo<!--slot-->')
foo.value = 'bar'
await nextTick()
expect(host.innerHTML).toBe('bar<!--slot--><!--slot-->bar<!--slot-->')
})
})
}) })

View File

@ -210,7 +210,8 @@ export function hasAttrFromRawProps(rawProps: RawProps, key: string): boolean {
if (dynamicSources) { if (dynamicSources) {
let i = dynamicSources.length let i = dynamicSources.length
while (i--) { while (i--) {
if (hasOwn(resolveSource(dynamicSources[i]), key)) { const source = resolveSource(dynamicSources[i])
if (source && hasOwn(source, key)) {
return true return true
} }
} }

View File

@ -87,10 +87,21 @@ export function getSlot(
} }
} }
export function forwardedSlotCreator(): (
name: string | (() => string),
rawProps?: LooseRawProps | null,
fallback?: VaporSlot,
) => Block {
const instance = currentInstance as VaporComponentInstance
return (name, rawProps, fallback) =>
createSlot(name, rawProps, fallback, instance)
}
export function createSlot( export function createSlot(
name: string | (() => string), name: string | (() => string),
rawProps?: LooseRawProps | null, rawProps?: LooseRawProps | null,
fallback?: VaporSlot, fallback?: VaporSlot,
i?: VaporComponentInstance,
): Block { ): Block {
const _insertionParent = insertionParent const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor const _insertionAnchor = insertionAnchor
@ -98,7 +109,7 @@ export function createSlot(
locateHydrationNode() locateHydrationNode()
} }
const instance = currentInstance as VaporComponentInstance const instance = i || (currentInstance as VaporComponentInstance)
const rawSlots = instance.rawSlots const rawSlots = instance.rawSlots
const slotProps = rawProps const slotProps = rawProps
? new Proxy(rawProps, rawPropsProxyHandlers) ? new Proxy(rawProps, rawPropsProxyHandlers)

View File

@ -9,7 +9,7 @@ export { insert, prepend, remove, isFragment, VaporFragment } from './block'
export { setInsertionState } from './insertionState' export { setInsertionState } from './insertionState'
export { createComponent, createComponentWithFallback } from './component' export { createComponent, createComponentWithFallback } from './component'
export { renderEffect } from './renderEffect' export { renderEffect } from './renderEffect'
export { createSlot } from './componentSlots' export { createSlot, forwardedSlotCreator } from './componentSlots'
export { template } from './dom/template' export { template } from './dom/template'
export { createTextNode, child, nthChild, next } from './dom/node' export { createTextNode, child, nthChild, next } from './dom/node'
export { export {

View File

@ -26,7 +26,14 @@ import {
mountComponent, mountComponent,
unmountComponent, unmountComponent,
} from './component' } from './component'
import { type Block, VaporFragment, insert, remove } from './block' import {
type Block,
VaporFragment,
insert,
isFragment,
isValidBlock,
remove,
} from './block'
import { EMPTY_OBJ, extend, isFunction } from '@vue/shared' import { EMPTY_OBJ, extend, isFunction } from '@vue/shared'
import { type RawProps, rawPropsProxyHandlers } from './componentProps' import { type RawProps, rawPropsProxyHandlers } from './componentProps'
import type { RawSlots, VaporSlot } from './componentSlots' import type { RawSlots, VaporSlot } from './componentSlots'
@ -236,7 +243,24 @@ function renderVDOMSlot(
isFunction(name) ? name() : name, isFunction(name) ? name() : name,
props, props,
) )
if ((vnode.children as any[]).length) { let isValidSlotContent
let children = vnode.children as any[]
// TODO add tests
// handle forwarded vapor slot
let vaporSlot
if (children.length === 1 && (vaporSlot = children[0].vs)) {
const block = vaporSlot.slot(props)
isValidSlotContent =
isValidBlock(block) ||
// if block is a vapor fragment with insert, it indicates a forwarded VDOM slot
(isFragment(block) && block.insert)
}
// vnode children
else {
isValidSlotContent = children.length > 0
}
if (isValidSlotContent) {
if (fallbackNodes) { if (fallbackNodes) {
remove(fallbackNodes, parentNode) remove(fallbackNodes, parentNode)
fallbackNodes = undefined fallbackNodes = undefined