feat: hydrate VaporTransition with appear (#13863)

This commit is contained in:
edison 2025-09-10 11:11:51 +08:00 committed by GitHub
parent 8978ef759f
commit 1ee1777232
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 207 additions and 63 deletions

View File

@ -805,16 +805,16 @@ export function createHydrationFunctions(
}
}
const isTemplateNode = (node: Node): node is HTMLTemplateElement => {
return (
node.nodeType === DOMNodeTypes.ELEMENT &&
(node as Element).tagName === 'TEMPLATE'
)
}
return [hydrate, hydrateNode]
}
export const isTemplateNode = (node: Node): node is HTMLTemplateElement => {
return (
node.nodeType === DOMNodeTypes.ELEMENT &&
(node as Element).tagName === 'TEMPLATE'
)
}
/**
* Dev only
*/

View File

@ -601,3 +601,7 @@ export { createInternalObject } from './internalObject'
* @internal
*/
export { createCanSetSetupRefChecker } from './rendererTemplateRef'
/**
* @internal
*/
export { isTemplateNode } from './hydration'

View File

@ -1568,8 +1568,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
<!--[--><span>a</span><span>b</span><span>c</span><!--]-->
"
<!--[--><span>a</span><span>b</span><span>c</span><!--for-->"
`,
)
@ -1578,8 +1577,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
"
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--for-->"
`,
)
})
@ -1601,8 +1599,7 @@ describe('Vapor Mode hydration', () => {
`
"
<!--[--><div>
<!--[--><span>a</span><span>b</span><span>c</span><!--]-->
</div><div>3</div><!--]-->
<!--[--><span>a</span><span>b</span><span>c</span><!--for--></div><div>3</div><!--]-->
"
`,
)
@ -1613,8 +1610,7 @@ describe('Vapor Mode hydration', () => {
`
"
<!--[--><div>
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
</div><div>4</div><!--]-->
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--for--></div><div>4</div><!--]-->
"
`,
)
@ -1635,8 +1631,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
<!--[--><span>a</span><span>b</span><span>c</span><!--]-->
<span></span></div>"
<!--[--><span>a</span><span>b</span><span>c</span><!--for--><span></span></div>"
`,
)
@ -1645,8 +1640,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
<span></span></div>"
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--for--><span></span></div>"
`,
)
@ -1655,8 +1649,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
<!--[--><span>b</span><span>c</span><span>d</span><!--]-->
<span></span></div>"
<!--[--><span>b</span><span>c</span><span>d</span><!--for--><span></span></div>"
`,
)
})
@ -1677,9 +1670,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
<!--[--><span>a</span><span>b</span><span>c</span><!--]-->
<!--[--><span>a</span><span>b</span><span>c</span><!--]-->
<span></span></div>"
<!--[--><span>a</span><span>b</span><span>c</span><!--for-->
<!--[--><span>a</span><span>b</span><span>c</span><!--for--><span></span></div>"
`,
)
@ -1688,9 +1680,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
<span></span></div>"
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--for-->
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--for--><span></span></div>"
`,
)
@ -1699,9 +1690,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
<!--[--><span>c</span><span>d</span><!--]-->
<!--[--><span>c</span><span>d</span><!--]-->
<span></span></div>"
<!--[--><span>c</span><span>d</span><!--for-->
<!--[--><span>c</span><span>d</span><!--for--><span></span></div>"
`,
)
})
@ -1722,8 +1712,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div>
<!--[--><div>comp</div><div>comp</div><div>comp</div><!--]-->
</div>"
<!--[--><div>comp</div><div>comp</div><div>comp</div><!--for--></div>"
`,
)
@ -1732,8 +1721,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div>
<!--[--><div>comp</div><div>comp</div><div>comp</div><div>comp</div><!--]-->
</div>"
<!--[--><div>comp</div><div>comp</div><div>comp</div><div>comp</div><!--for--></div>"
`,
)
})
@ -1758,8 +1746,7 @@ describe('Vapor Mode hydration', () => {
<!--[-->
<!--[--><span>a</span><!--]-->
<!--[--><span>b</span><!--]-->
<!--[--><span>c</span><!--]-->
<!--]-->
<!--[--><span>c</span><!--for--><!--]-->
</div>"
`,
)
@ -1772,8 +1759,7 @@ describe('Vapor Mode hydration', () => {
<!--[-->
<!--[--><span>a</span><!--]-->
<!--[--><span>b</span><!--]-->
<!--[--><span>c</span><span>d</span><!--slot--><!--]-->
<!--]-->
<!--[--><span>c</span><span>d</span><!--slot--><!--for--><!--]-->
</div>"
`,
)
@ -1797,8 +1783,7 @@ describe('Vapor Mode hydration', () => {
<!--[-->
<!--[--><div>foo</div>-bar-<!--]-->
<!--[--><div>foo</div>-bar-<!--]-->
<!--[--><div>foo</div>-bar-<!--]-->
<!--]-->
<!--[--><div>foo</div>-bar-<!--for--><!--]-->
</div>"
`,
)
@ -1811,8 +1796,7 @@ describe('Vapor Mode hydration', () => {
<!--[-->
<!--[--><div>foo</div>-bar-<!--]-->
<!--[--><div>foo</div>-bar-<!--]-->
<!--[--><div>foo</div>-bar-<div>foo</div>-bar-<!--]-->
<!--]-->
<!--[--><div>foo</div>-bar-<div>foo</div>-bar-<!--for--><!--]-->
</div>"
`,
)
@ -1950,8 +1934,7 @@ describe('Vapor Mode hydration', () => {
`
"
<!--[-->
<!--[--><span>a</span><span>b</span><span>c</span><!--]-->
<!--]-->
<!--[--><span>a</span><span>b</span><span>c</span><!--for--><!--]-->
"
`,
)
@ -2383,10 +2366,9 @@ describe('Vapor Mode hydration', () => {
`
"
<!--[-->
<!--[--><div>a</div><div>b</div><div>c</div><!--]-->
<!--[--><div>a</div><div>b</div><div>c</div><!--for-->
<!--[--><span>foo</span><!--]-->
<!--[--><div>a</div><div>b</div><div>c</div><!--]-->
<!--]-->
<!--[--><div>a</div><div>b</div><div>c</div><!--for--><!--]-->
"
`,
)
@ -2397,10 +2379,9 @@ describe('Vapor Mode hydration', () => {
`
"
<!--[-->
<!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--]-->
<!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--for-->
<!--[--><span>foo</span><!--]-->
<!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--]-->
<!--]-->
<!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--for--><!--]-->
"
`,
)
@ -2725,14 +2706,115 @@ describe('Vapor Mode hydration', () => {
})
})
describe.todo('transition', async () => {
test('transition appear', async () => {})
describe('transition', async () => {
test('transition appear', async () => {
const { container } = await testHydration(
`<template>
<transition appear>
<div>foo</div>
</transition>
</template>`,
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`"<div style="" class="v-enter-from v-enter-active">foo</div>"`,
)
expect(`mismatch`).not.toHaveBeenWarned()
})
test('transition appear with v-if', async () => {})
test('transition appear work with pre-existing class', async () => {
const { container } = await testHydration(
`<template>
<transition appear>
<div class="foo">foo</div>
</transition>
</template>`,
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`"<div class="foo v-enter-from v-enter-active" style="">foo</div>"`,
)
expect(`mismatch`).not.toHaveBeenWarned()
})
test('transition appear with v-show', async () => {})
test('transition appear work with empty content', async () => {
const data = ref(true)
const { container } = await testHydration(
`<template>
<transition appear>
<slot v-if="data"></slot>
<span v-else>foo</span>
</transition>
</template>`,
undefined,
data,
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`"<!--slot--><!--if-->"`,
)
expect(`mismatch`).not.toHaveBeenWarned()
test('transition appear w/ event listener', async () => {})
data.value = false
await nextTick()
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`"<span class="v-enter-from v-enter-active">foo</span><!--if-->"`,
)
})
test('transition appear with v-if', async () => {
const data = ref(false)
const { container } = await testHydration(
`<template>
<transition appear>
<div v-if="data">foo</div>
</transition>
</template>`,
undefined,
data,
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`"<!--if-->"`,
)
expect(`mismatch`).not.toHaveBeenWarned()
})
test('transition appear with v-show', async () => {
const data = ref(false)
const { container } = await testHydration(
`<template>
<transition appear>
<div v-show="data">foo</div>
</transition>
</template>`,
undefined,
data,
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`"<div style="display:none;" class="v-enter-from v-enter-active v-leave-from v-leave-active">foo</div>"`,
)
expect(`mismatch`).not.toHaveBeenWarned()
})
test('transition appear w/ event listener', async () => {
const { container } = await testHydration(
`<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<transition appear>
<button @click="count++">{{ count }}</button>
</transition>
</template>`,
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`"<button style="" class="v-enter-from v-enter-active">0</button>"`,
)
triggerEvent('click', container.querySelector('button')!)
await nextTick()
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`"<button style="" class="v-enter-from v-enter-active">1</button>"`,
)
})
})
describe.todo('async component', async () => {

View File

@ -135,9 +135,11 @@ export const createFor = (
if (isHydrating) {
parentAnchor = locateFragmentEndAnchor()!
// TODO: special handling vFor not render as a fragment. (inside Transition/TransitionGroup)
if (__DEV__ && !parentAnchor) {
throw new Error(`v-for fragment anchor node was not found.`)
if (__DEV__) {
if (!parentAnchor) {
throw new Error(`v-for fragment anchor node was not found.`)
}
;(parentAnchor as Comment).data = 'for'
}
}
} else {

View File

@ -1,4 +1,5 @@
import {
type BaseTransitionProps,
type GenericComponentInstance,
type TransitionElement,
type TransitionHooks,
@ -9,7 +10,9 @@ import {
baseResolveTransitionHooks,
checkTransitionMode,
currentInstance,
isTemplateNode,
leaveCbKey,
queuePostFlushCb,
resolveTransitionProps,
useTransitionState,
warn,
@ -24,6 +27,11 @@ import {
import { extend, isArray } from '@vue/shared'
import { renderEffect } from '../renderEffect'
import { isFragment } from '../fragment'
import {
currentHydrationNode,
isHydrating,
setCurrentHydrationNode,
} from '../dom/hydration'
const decorate = (t: typeof VaporTransition) => {
t.displayName = 'VaporTransition'
@ -34,6 +42,33 @@ const decorate = (t: typeof VaporTransition) => {
export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
(props, { slots, attrs }) => {
// wrapped <transition appear>
let resetDisplay: Function | undefined
if (
isHydrating &&
currentHydrationNode &&
isTemplateNode(currentHydrationNode)
) {
// replace <template> node with inner child
const {
content: { firstChild },
parentNode,
} = currentHydrationNode
if (firstChild) {
if (
firstChild instanceof HTMLElement ||
firstChild instanceof SVGElement
) {
const originalDisplay = firstChild.style.display
firstChild.style.display = 'none'
resetDisplay = () => (firstChild.style.display = originalDisplay)
}
parentNode!.replaceChild(firstChild, currentHydrationNode)
setCurrentHydrationNode(firstChild)
}
}
const children = (slots.default && slots.default()) as any as Block
if (!children) return
@ -41,7 +76,7 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
const { mode } = props
checkTransitionMode(mode)
let resolvedProps
let resolvedProps: BaseTransitionProps<Element>
let isMounted = false
renderEffect(() => {
resolvedProps = resolveTransitionProps(props)
@ -81,7 +116,7 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
})
}
applyTransitionHooks(
const hooks = applyTransitionHooks(
children,
{
state: useTransitionState(),
@ -91,6 +126,13 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
fallthroughAttrs,
)
if (resetDisplay && resolvedProps!.appear) {
const child = findTransitionBlock(children)!
hooks.beforeEnter(child)
resetDisplay()
queuePostFlushCb(() => hooks.enter(child))
}
return children
},
)

View File

@ -158,7 +158,7 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({
return container
} else {
const frag = __DEV__
? new DynamicFragment('transitionGroup')
? new DynamicFragment('transition-group')
: new DynamicFragment()
renderEffect(() => frag.update(() => slottedBlock))
return frag

View File

@ -12,6 +12,8 @@ import {
import type { TransitionHooks } from '@vue/runtime-dom'
import {
advanceHydrationNode,
currentHydrationNode,
isComment,
isHydrating,
locateFragmentEndAnchor,
locateHydrationNode,
@ -152,13 +154,25 @@ export class DynamicFragment extends VaporFragment {
if (!this.anchor) {
throw new Error('Failed to locate if anchor')
} else {
;(this.anchor as Comment).data = this.anchorLabel
if (__DEV__) {
;(this.anchor as Comment).data = this.anchorLabel
}
return
}
}
// reuse the vdom fragment end anchor for slots
if (this.anchorLabel === 'slot') {
// reuse the empty comment node for empty slot
// e.g. `<slot v-if="false"></slot>`
if (isEmpty && isComment(currentHydrationNode!, '')) {
this.anchor = currentHydrationNode!
if (__DEV__) {
;(this.anchor as Comment).data = this.anchorLabel!
}
return
}
// reuse the vdom fragment end anchor for slots
this.anchor = locateFragmentEndAnchor()!
if (!this.anchor) {
throw new Error('Failed to locate slot anchor')