Skip to content

Commit efda719

Browse files
committed
fix(transition): align interop transition vnode identity with vdom
1 parent 26e4a81 commit efda719

4 files changed

Lines changed: 129 additions & 25 deletions

File tree

packages/runtime-vapor/__tests__/vdomInterop.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,55 @@ describe('vdomInterop', () => {
7373
expect(componentBlock.$key).toBe('bar')
7474
expect(componentBlock.vnode.key).toBe('bar')
7575
})
76+
77+
test('preserves single slot vnode key on interop fragments', async () => {
78+
const key = ref('foo')
79+
80+
const CompA = defineComponent({
81+
setup() {
82+
return () => h('div', 'A')
83+
},
84+
})
85+
const CompB = defineComponent({
86+
setup() {
87+
return () => h('div', 'B')
88+
},
89+
})
90+
const current = shallowRef<any>(CompA)
91+
92+
const VaporChild = defineVaporComponent({
93+
setup() {
94+
return createSlot('default') as any
95+
},
96+
})
97+
98+
const Parent = defineComponent({
99+
setup() {
100+
return () =>
101+
h(VaporChild as any, null, {
102+
default: () => [h(current.value, { key: key.value })],
103+
})
104+
},
105+
})
106+
107+
const app = createApp(Parent)
108+
app.use(vaporInteropPlugin)
109+
const vapor = (app._context as any).vapor
110+
const originalVdomSlot = vapor.vdomSlot
111+
let frag: any
112+
vapor.vdomSlot = (...args: any[]) => (frag = originalVdomSlot(...args))
113+
114+
const host = document.createElement('div')
115+
app.mount(host)
116+
117+
expect(frag.$key).toBe('_defaultfoo')
118+
119+
key.value = 'bar'
120+
current.value = CompB
121+
await nextTick()
122+
123+
expect(frag.$key).toBe('_defaultbar')
124+
})
76125
})
77126

78127
describe('fragment nodes', () => {

packages/runtime-vapor/src/components/Transition.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import {
77
type TransitionProps,
88
TransitionPropsValidators,
99
type TransitionState,
10+
type VNode,
1011
baseResolveTransitionHooks,
1112
checkTransitionMode,
1213
currentInstance,
1314
getComponentName,
15+
getTransitionRawChildren,
1416
isAsyncWrapper,
1517
isTemplateNode,
1618
leaveCbKey,
@@ -44,6 +46,7 @@ import {
4446
isHydrating,
4547
setCurrentHydrationNode,
4648
} from '../dom/hydration'
49+
import { isInteropEnabled } from '../vdomInteropState'
4750

4851
const displayName = 'VaporTransition'
4952
export type ResolvedTransitionBlock = (
@@ -139,7 +142,10 @@ function getTransitionType(block: ResolvedTransitionBlock): any {
139142
const type = transitionTypeMap.get(block)
140143
if (type !== undefined) return type
141144
if (block instanceof Element) return block.localName
142-
if (isFragment(block) && block.vnode) return block.vnode.type
145+
if (isFragment(block) && block.vnode) {
146+
const children = getTransitionRawChildren([block.vnode])
147+
if (children.length === 1) return children[0].type
148+
}
143149
return block
144150
}
145151

@@ -163,6 +169,10 @@ function getLeaveElement(
163169
if (block instanceof Element) {
164170
return block as TransitionElement
165171
}
172+
if (isInteropEnabled && isFragment(block) && block.vnode) {
173+
const el = getTransitionElementFromVNode(block.vnode)
174+
if (el) return el as TransitionElement
175+
}
166176
if (
167177
isFragment(block) &&
168178
!isArray(block.nodes) &&
@@ -407,9 +417,12 @@ export function resolveTransitionBlock(
407417
if (!__DEV__) break
408418
}
409419
} else if (isFragment(block)) {
410-
if (block.insert) {
420+
if (isInteropEnabled && block.vnode) {
411421
child = block
412-
if (block.vnode) transitionTypeMap.set(child, block.vnode.type)
422+
const children = getTransitionRawChildren([block.vnode])
423+
if (children.length === 1) {
424+
transitionTypeMap.set(child, children[0].type)
425+
}
413426
} else {
414427
// collect fragments for setting transition hooks
415428
if (onFragment) onFragment(block)
@@ -430,3 +443,27 @@ export function setTransitionHooks(
430443
}
431444
block.$transition = hooks
432445
}
446+
447+
export function getVNodeKey(
448+
vnode: VNode | undefined,
449+
): VNode['key'] | undefined {
450+
if (!vnode) return
451+
const children = getTransitionRawChildren([vnode])
452+
return children.length === 1 ? children[0].key : undefined
453+
}
454+
455+
export function getTransitionElementFromVNode(
456+
vnode: VNode | undefined,
457+
): Element | undefined {
458+
if (!vnode) return
459+
if (vnode.component) {
460+
return getTransitionElementFromVNode(vnode.component.subTree)
461+
}
462+
if (vnode.el instanceof Element) {
463+
return vnode.el
464+
}
465+
const children = getTransitionRawChildren([vnode])
466+
if (children.length === 1 && children[0] !== vnode) {
467+
return getTransitionElementFromVNode(children[0])
468+
}
469+
}

packages/runtime-vapor/src/components/TransitionGroup.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { renderEffect } from '../renderEffect'
2222
import {
2323
type ResolvedTransitionBlock,
2424
ensureTransitionHooksRegistered,
25+
getTransitionElementFromVNode,
2526
resolveTransitionHooks,
2627
setTransitionHooks,
2728
} from './Transition'
@@ -257,14 +258,8 @@ function getTransitionElement(
257258
if (block instanceof Element) return block
258259

259260
// vdom interop
260-
if (
261-
isInteropEnabled &&
262-
isFragment(block) &&
263-
block.vnode &&
264-
!isArray(block.nodes) &&
265-
(block.nodes instanceof Element || isFragment(block.nodes))
266-
) {
267-
return getTransitionElement(block.nodes)
261+
if (isInteropEnabled && isFragment(block) && block.vnode) {
262+
return getTransitionElementFromVNode(block.vnode)
268263
}
269264
}
270265

packages/runtime-vapor/src/vdomInterop.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@ import {
101101
renderSlotFallback,
102102
} from './fragment'
103103
import type { NodeRef } from './apiTemplateRef'
104-
import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition'
104+
import {
105+
getVNodeKey,
106+
setTransitionHooks as setVaporTransitionHooks,
107+
} from './components/Transition'
105108
import { setInteropEnabled } from './vdomInteropState'
106109
import {
107110
type KeepAliveInstance,
@@ -559,19 +562,22 @@ function resolveVNodeNodes(vnode: VNode): Block {
559562
return vnode.el as Block
560563
}
561564

565+
function appendVnodeUpdatedHook(vnode: VNode, hook: () => void): void {
566+
const props = (vnode.props ||= {})
567+
const existing = props.onVnodeUpdated
568+
props.onVnodeUpdated = existing
569+
? isArray(existing)
570+
? [...existing, hook]
571+
: [existing, hook]
572+
: hook
573+
}
574+
562575
function trackFragmentVNodeUpdates(frag: VaporFragment, vnode: VNode): void {
563576
const refresh = () => {
564577
frag.nodes = resolveVNodeNodes(vnode)
565578
if (frag.onUpdated) frag.onUpdated.forEach(m => m())
566579
}
567-
568-
const props = (vnode.props ||= {})
569-
const existing = props.onVnodeUpdated
570-
props.onVnodeUpdated = existing
571-
? isArray(existing)
572-
? [...existing, refresh]
573-
: [existing, refresh]
574-
: refresh
580+
appendVnodeUpdatedHook(vnode, refresh)
575581
}
576582

577583
/**
@@ -903,12 +909,21 @@ function ensureRendererBridge(
903909
}
904910

905911
function trackSlotVNodeUpdates(frag: VaporFragment, vnode: VNode): void {
906-
trackFragmentVNodeUpdates(frag, vnode)
907-
if (vnode.type === Fragment && isArray(vnode.children)) {
908-
vnode.children.forEach(child => {
909-
if (isVNode(child)) trackSlotVNodeUpdates(frag, child)
910-
})
912+
const refresh = () => {
913+
frag.nodes = resolveVNodeNodes(vnode)
914+
if (frag.onUpdated) frag.onUpdated.forEach(m => m())
911915
}
916+
917+
const track = (node: VNode) => {
918+
appendVnodeUpdatedHook(node, refresh)
919+
if (node.type === Fragment && isArray(node.children)) {
920+
node.children.forEach(child => {
921+
if (isVNode(child)) track(child)
922+
})
923+
}
924+
}
925+
926+
track(vnode)
912927
}
913928

914929
/**
@@ -1021,6 +1036,8 @@ function renderVDOMSlot(
10211036

10221037
if (isHydrating) {
10231038
if (isVNode(resolved)) {
1039+
frag.vnode = resolved
1040+
frag.$key = getVNodeKey(resolved)
10241041
trackSlotVNodeUpdates(frag, resolved)
10251042
hydrateVNode(resolved, parentComponent as any)
10261043
currentVNode = resolved
@@ -1039,6 +1056,8 @@ function renderVDOMSlot(
10391056
}
10401057

10411058
if (isVNode(resolved)) {
1059+
frag.vnode = resolved
1060+
frag.$key = getVNodeKey(resolved)
10421061
trackSlotVNodeUpdates(frag, resolved)
10431062
if (currentBlock) {
10441063
remove(currentBlock, parentNode)
@@ -1060,6 +1079,8 @@ function renderVDOMSlot(
10601079
}
10611080

10621081
if (resolved) {
1082+
frag.vnode = null
1083+
frag.$key = undefined
10631084
if (currentVNode) {
10641085
internals.um(currentVNode, parentComponent as any, null, true)
10651086
currentVNode = null
@@ -1083,6 +1104,8 @@ function renderVDOMSlot(
10831104
}
10841105

10851106
// mark as empty
1107+
frag.vnode = null
1108+
frag.$key = undefined
10861109
frag.nodes = []
10871110
} finally {
10881111
setCurrentSlotOwner(prevSlotOwner)

0 commit comments

Comments
 (0)