This commit is contained in:
edison 2025-06-26 10:08:59 +02:00 committed by GitHub
commit d544cd2362
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 1340 additions and 29 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`] = `
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
const t0 = _template("<div></div>")

View File

@ -420,6 +420,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', () => {
test('error on extraneous children w/ named default slot', () => {
const onError = vi.fn()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -81,6 +81,10 @@ export function renderSlot(
}
openBlock()
const validSlotContent = slot && ensureValidVNode(slot(props))
// handle forwarded vapor slot fallback
ensureVaporSlotFallback(validSlotContent, fallback)
const slotKey =
props.key ||
// slot content array of a dynamic conditional slot may have a branch
@ -124,3 +128,20 @@ export function ensureValidVNode(
? vnodes
: null
}
export function ensureVaporSlotFallback(
vnodes: VNodeArrayChildren | null | undefined,
fallback?: () => VNodeArrayChildren,
): void {
let vaporSlot: any
if (
vnodes &&
vnodes.length === 1 &&
isVNode(vnodes[0]) &&
(vaporSlot = vnodes[0].vs)
) {
if (!vaporSlot.fallback && fallback) {
vaporSlot.fallback = fallback
}
}
}

View File

@ -557,6 +557,10 @@ export { startMeasure, endMeasure } from './profiling'
* @internal
*/
export { initFeatureFlags } from './featureFlags'
/**
* @internal
*/
export { ensureVaporSlotFallback } from './helpers/renderSlot'
/**
* @internal
*/

View File

@ -2648,7 +2648,7 @@ export function traverseStaticChildren(
function locateNonHydratedAsyncRoot(
instance: ComponentInternalInstance,
): ComponentInternalInstance | undefined {
const subComponent = instance.subTree.component
const subComponent = instance.vapor ? null : instance.subTree.component
if (subComponent) {
if (subComponent.asyncDep && !subComponent.asyncResolved) {
return subComponent

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,7 @@ export class VaporFragment {
anchor?: Node
insert?: (parent: ParentNode, anchor: Node | null) => void
remove?: (parent?: ParentNode) => void
fallback?: BlockFn
constructor(nodes: Block) {
this.nodes = nodes
@ -33,7 +34,6 @@ export class DynamicFragment extends VaporFragment {
anchor: Node
scope: EffectScope | undefined
current?: BlockFn
fallback?: BlockFn
constructor(anchorLabel?: string) {
super([])
@ -124,6 +124,10 @@ export function insert(
insert(b, parent, anchor)
}
} else {
if (block.anchor) {
insert(block.anchor, parent, anchor)
anchor = block.anchor
}
// fragment
if (block.insert) {
// TODO handle hydration for vdom interop
@ -131,7 +135,6 @@ export function insert(
} else {
insert(block.nodes, parent, anchor)
}
if (block.anchor) insert(block.anchor, parent, anchor)
}
}

View File

@ -1,5 +1,12 @@
import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
import { type Block, type BlockFn, DynamicFragment, insert } from './block'
import {
type Block,
type BlockFn,
DynamicFragment,
type VaporFragment,
insert,
isFragment,
} from './block'
import { rawPropsProxyHandlers } from './componentProps'
import { currentInstance, isRef } from '@vue/runtime-dom'
import type { LooseRawProps, VaporComponentInstance } from './component'
@ -91,10 +98,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(
name: string | (() => string),
rawProps?: LooseRawProps | null,
fallback?: VaporSlot,
i?: VaporComponentInstance,
): Block {
const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor
@ -104,7 +122,7 @@ export function createSlot(
resetInsertionState()
}
const instance = currentInstance as VaporComponentInstance
const instance = i || (currentInstance as VaporComponentInstance)
const rawSlots = instance.rawSlots
const slotProps = rawProps
? new Proxy(rawProps, rawPropsProxyHandlers)
@ -133,8 +151,27 @@ export function createSlot(
(slot._bound = () => {
const slotContent = slot(slotProps)
if (slotContent instanceof DynamicFragment) {
slotContent.fallback = fallback
let nodes = slotContent.nodes
if (
(slotContent.fallback = fallback) &&
isArray(nodes) &&
nodes.length === 0
) {
// use fallback if the slot content is invalid
slotContent.update(fallback)
} else {
while (isFragment(nodes)) {
ensureVaporSlotFallback(nodes, fallback)
nodes = nodes.nodes
}
}
}
// forwarded vdom slot, if there is no fallback provide, try use the fallback
// provided by the slot outlet.
else if (isFragment(slotContent)) {
ensureVaporSlotFallback(slotContent, fallback)
}
return slotContent
}),
)
@ -157,3 +194,12 @@ export function createSlot(
return fragment
}
function ensureVaporSlotFallback(
block: VaporFragment,
fallback?: VaporSlot,
): void {
if (block.insert && !block.fallback && fallback) {
block.fallback = fallback
}
}

View File

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

View File

@ -4,7 +4,9 @@ import {
type ConcreteComponent,
MoveType,
type Plugin,
type RendererElement,
type RendererInternals,
type RendererNode,
type ShallowRef,
type Slots,
type VNode,
@ -13,7 +15,9 @@ import {
createVNode,
currentInstance,
ensureRenderer,
ensureVaporSlotFallback,
isEmitListener,
isVNode,
onScopeDispose,
renderSlot,
shallowReactive,
@ -29,8 +33,15 @@ import {
mountComponent,
unmountComponent,
} from './component'
import { type Block, VaporFragment, insert, remove } from './block'
import { EMPTY_OBJ, extend, isFunction } from '@vue/shared'
import {
type Block,
DynamicFragment,
VaporFragment,
insert,
isFragment,
remove,
} from './block'
import { EMPTY_OBJ, extend, isArray, isFunction } from '@vue/shared'
import { type RawProps, rawPropsProxyHandlers } from './componentProps'
import type { RawSlots, VaporSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
@ -94,15 +105,28 @@ const vaporInteropImpl: Omit<
slot(n1: VNode, n2: VNode, container, anchor) {
if (!n1) {
// mount
const selfAnchor = (n2.el = n2.anchor = createTextNode())
insert(selfAnchor, container, anchor)
let selfAnchor: Node | undefined
const { slot, fallback } = n2.vs!
const propsRef = (n2.vs!.ref = shallowRef(n2.props))
const slotBlock = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler))
// TODO fallback for slot with v-if content
// fallback is a vnode slot function here, and slotBlock, if a DynamicFragment,
// expects a Vapor BlockFn as fallback
fallback
// forwarded vdom slot without its own fallback, use the fallback provided by
// the slot outlet
if (slotBlock instanceof DynamicFragment) {
// vapor slot's nodes is a forwarded vdom slot
let nodes = slotBlock.nodes
while (isFragment(nodes)) {
ensureVDOMSlotFallback(nodes, fallback)
nodes = nodes.nodes
}
// use fragment's anchor when possible
selfAnchor = slotBlock.anchor
} else if (isFragment(slotBlock)) {
ensureVDOMSlotFallback(slotBlock, fallback)
selfAnchor = slotBlock.anchor!
}
if (!selfAnchor) selfAnchor = createTextNode()
insert((n2.el = n2.anchor = selfAnchor), container, anchor)
insert((n2.vb = slotBlock), container, selfAnchor)
} else {
// update
@ -233,34 +257,56 @@ function renderVDOMSlot(
let fallbackNodes: Block | undefined
let oldVNode: VNode | null = null
frag.fallback = fallback
frag.insert = (parentNode, anchor) => {
if (!isMounted) {
renderEffect(() => {
const vnode = renderSlot(
slotsRef.value,
isFunction(name) ? name() : name,
props,
)
if ((vnode.children as any[]).length) {
let vnode: VNode | undefined
let isValidSlot = false
// only render slot if rawSlots is defined and slot nodes are not empty
// otherwise, render fallback
if (slotsRef.value) {
vnode = renderSlot(
slotsRef.value,
isFunction(name) ? name() : name,
props,
)
let children = vnode.children as any[]
// handle forwarded vapor slot without its own fallback
// use the fallback provided by the slot outlet
ensureVaporSlotFallback(children, fallback as any)
isValidSlot = children.length > 0
}
if (isValidSlot) {
if (fallbackNodes) {
remove(fallbackNodes, parentNode)
fallbackNodes = undefined
}
internals.p(
oldVNode,
vnode,
vnode!,
parentNode,
anchor,
parentComponent as any,
)
oldVNode = vnode
oldVNode = vnode!
} else {
// for forwarded slot without its own fallback, use the fallback
// provided by the slot outlet.
// re-fetch `frag.fallback` as it may have been updated at `createSlot`
fallback = frag.fallback
if (fallback && !fallbackNodes) {
// mount fallback
if (oldVNode) {
internals.um(oldVNode, parentComponent as any, null, true)
}
insert((fallbackNodes = fallback(props)), parentNode, anchor)
insert(
(fallbackNodes = fallback(internals, parentComponent)),
parentNode,
anchor,
)
}
oldVNode = null
}
@ -302,3 +348,37 @@ export const vaporInteropPlugin: Plugin = app => {
return mount(...args)
}) satisfies App['mount']
}
function ensureVDOMSlotFallback(block: VaporFragment, fallback?: () => any) {
if (block.insert && !block.fallback && fallback) {
block.fallback = createFallback(fallback)
}
}
const createFallback =
(fallback: () => any) =>
(
internals: RendererInternals<RendererNode, RendererElement>,
parentComponent: ComponentInternalInstance | null,
) => {
const fallbackNodes = fallback()
// vnode slot, wrap it as a VaporFragment
if (isArray(fallbackNodes) && fallbackNodes.every(isVNode)) {
const frag = new VaporFragment([])
frag.insert = (parentNode, anchor) => {
fallbackNodes.forEach(vnode => {
internals.p(null, vnode, parentNode, anchor, parentComponent)
})
}
frag.remove = parentNode => {
fallbackNodes.forEach(vnode => {
internals.um(vnode, parentComponent, null, true)
})
}
return frag
}
// vapor slot
return fallbackNodes as Block
}