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] return [hydrate, hydrateNode]
} }
export const isTemplateNode = (node: Node): node is HTMLTemplateElement => {
return (
node.nodeType === DOMNodeTypes.ELEMENT &&
(node as Element).tagName === 'TEMPLATE'
)
}
/** /**
* Dev only * Dev only
*/ */

View File

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

View File

@ -1568,8 +1568,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( 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( 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> <!--[--><div>
<!--[--><span>a</span><span>b</span><span>c</span><!--]--> <!--[--><span>a</span><span>b</span><span>c</span><!--for--></div><div>3</div><!--]-->
</div><div>3</div><!--]-->
" "
`, `,
) )
@ -1613,8 +1610,7 @@ describe('Vapor Mode hydration', () => {
` `
" "
<!--[--><div> <!--[--><div>
<!--[--><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--></div><div>4</div><!--]-->
</div><div>4</div><!--]-->
" "
`, `,
) )
@ -1635,8 +1631,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
` `
"<div><span></span> "<div><span></span>
<!--[--><span>a</span><span>b</span><span>c</span><!--]--> <!--[--><span>a</span><span>b</span><span>c</span><!--for--><span></span></div>"
<span></span></div>"
`, `,
) )
@ -1645,8 +1640,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
` `
"<div><span></span> "<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><!--for--><span></span></div>"
<span></span></div>"
`, `,
) )
@ -1655,8 +1649,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
` `
"<div><span></span> "<div><span></span>
<!--[--><span>b</span><span>c</span><span>d</span><!--]--> <!--[--><span>b</span><span>c</span><span>d</span><!--for--><span></span></div>"
<span></span></div>"
`, `,
) )
}) })
@ -1677,9 +1670,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
` `
"<div><span></span> "<div><span></span>
<!--[--><span>a</span><span>b</span><span>c</span><!--]--> <!--[--><span>a</span><span>b</span><span>c</span><!--for-->
<!--[--><span>a</span><span>b</span><span>c</span><!--]--> <!--[--><span>a</span><span>b</span><span>c</span><!--for--><span></span></div>"
<span></span></div>"
`, `,
) )
@ -1688,9 +1680,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
` `
"<div><span></span> "<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><!--for-->
<!--[--><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--><span></span></div>"
<span></span></div>"
`, `,
) )
@ -1699,9 +1690,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
` `
"<div><span></span> "<div><span></span>
<!--[--><span>c</span><span>d</span><!--]--> <!--[--><span>c</span><span>d</span><!--for-->
<!--[--><span>c</span><span>d</span><!--]--> <!--[--><span>c</span><span>d</span><!--for--><span></span></div>"
<span></span></div>"
`, `,
) )
}) })
@ -1722,8 +1712,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
` `
"<div> "<div>
<!--[--><div>comp</div><div>comp</div><div>comp</div><!--]--> <!--[--><div>comp</div><div>comp</div><div>comp</div><!--for--></div>"
</div>"
`, `,
) )
@ -1732,8 +1721,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
` `
"<div> "<div>
<!--[--><div>comp</div><div>comp</div><div>comp</div><div>comp</div><!--]--> <!--[--><div>comp</div><div>comp</div><div>comp</div><div>comp</div><!--for--></div>"
</div>"
`, `,
) )
}) })
@ -1758,8 +1746,7 @@ describe('Vapor Mode hydration', () => {
<!--[--> <!--[-->
<!--[--><span>a</span><!--]--> <!--[--><span>a</span><!--]-->
<!--[--><span>b</span><!--]--> <!--[--><span>b</span><!--]-->
<!--[--><span>c</span><!--]--> <!--[--><span>c</span><!--for--><!--]-->
<!--]-->
</div>" </div>"
`, `,
) )
@ -1772,8 +1759,7 @@ describe('Vapor Mode hydration', () => {
<!--[--> <!--[-->
<!--[--><span>a</span><!--]--> <!--[--><span>a</span><!--]-->
<!--[--><span>b</span><!--]--> <!--[--><span>b</span><!--]-->
<!--[--><span>c</span><span>d</span><!--slot--><!--]--> <!--[--><span>c</span><span>d</span><!--slot--><!--for--><!--]-->
<!--]-->
</div>" </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-<!--]-->
<!--[--><div>foo</div>-bar-<!--]--> <!--[--><div>foo</div>-bar-<!--for--><!--]-->
<!--]-->
</div>" </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-<!--]--> <!--[--><div>foo</div>-bar-<div>foo</div>-bar-<!--for--><!--]-->
<!--]-->
</div>" </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><!--]--> <!--[--><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><!--]--> <!--[--><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 () => { describe('transition', async () => {
test('transition appear', 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 () => { describe.todo('async component', async () => {

View File

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

View File

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

View File

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

View File

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