diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts
index ae660abf1..e41a4fdc9 100644
--- a/packages/runtime-core/src/hydration.ts
+++ b/packages/runtime-core/src/hydration.ts
@@ -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
*/
diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index 533000ee8..1c7fa5d78 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -601,3 +601,7 @@ export { createInternalObject } from './internalObject'
* @internal
*/
export { createCanSetSetupRefChecker } from './rendererTemplateRef'
+/**
+ * @internal
+ */
+export { isTemplateNode } from './hydration'
diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts
index 72b7e0787..999caf97c 100644
--- a/packages/runtime-vapor/__tests__/hydration.spec.ts
+++ b/packages/runtime-vapor/__tests__/hydration.spec.ts
@@ -1568,8 +1568,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
- abc
- "
+ abc"
`,
)
@@ -1578,8 +1577,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
- abcd
- "
+ abcd"
`,
)
})
@@ -1601,8 +1599,7 @@ describe('Vapor Mode hydration', () => {
`
"
- abc
-
3
+ abc3
"
`,
)
@@ -1613,8 +1610,7 @@ describe('Vapor Mode hydration', () => {
`
"
- abcd
-
4
+ abcd4
"
`,
)
@@ -1635,8 +1631,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
- abc
-
"
+ abc"
`,
)
@@ -1645,8 +1640,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
- abcd
-
"
+ abcd"
`,
)
@@ -1655,8 +1649,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
- bcd
-
"
+ bcd"
`,
)
})
@@ -1677,9 +1670,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
- abc
- abc
-
"
+ abc
+ abc"
`,
)
@@ -1688,9 +1680,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
- abcd
- abcd
-
"
+ abcd
+ abcd"
`,
)
@@ -1699,9 +1690,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
- cd
- cd
-
"
+ cd
+ cd"
`,
)
})
@@ -1722,8 +1712,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
""
+ comp
comp
comp
"
`,
)
@@ -1732,8 +1721,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
""
+ comp
comp
comp
comp
"
`,
)
})
@@ -1758,8 +1746,7 @@ describe('Vapor Mode hydration', () => {
a
b
- c
-
+ c
"
`,
)
@@ -1772,8 +1759,7 @@ describe('Vapor Mode hydration', () => {
a
b
- cd
-
+ cd
"
`,
)
@@ -1797,8 +1783,7 @@ describe('Vapor Mode hydration', () => {
foo
-bar-
foo
-bar-
- foo
-bar-
-
+ foo
-bar-
"
`,
)
@@ -1811,8 +1796,7 @@ describe('Vapor Mode hydration', () => {
foo
-bar-
foo
-bar-
- foo
-bar-foo
-bar-
-
+ foo
-bar-foo
-bar-
"
`,
)
@@ -1950,8 +1934,7 @@ describe('Vapor Mode hydration', () => {
`
"
- abc
-
+ abc
"
`,
)
@@ -2383,10 +2366,9 @@ describe('Vapor Mode hydration', () => {
`
"
- a
b
c
+ a
b
c
foo
- a
b
c
-
+ a
b
c
"
`,
)
@@ -2397,10 +2379,9 @@ describe('Vapor Mode hydration', () => {
`
"
- a
b
c
d
+ a
b
c
d
foo
- a
b
c
d
-
+ a
b
c
d
"
`,
)
@@ -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(
+ `
+
+ foo
+
+ `,
+ )
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `"foo
"`,
+ )
+ expect(`mismatch`).not.toHaveBeenWarned()
+ })
- test('transition appear with v-if', async () => {})
+ test('transition appear work with pre-existing class', async () => {
+ const { container } = await testHydration(
+ `
+
+ foo
+
+ `,
+ )
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `"foo
"`,
+ )
+ 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(
+ `
+
+
+ foo
+
+ `,
+ undefined,
+ data,
+ )
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `""`,
+ )
+ expect(`mismatch`).not.toHaveBeenWarned()
- test('transition appear w/ event listener', async () => {})
+ data.value = false
+ await nextTick()
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `"foo"`,
+ )
+ })
+
+ test('transition appear with v-if', async () => {
+ const data = ref(false)
+ const { container } = await testHydration(
+ `
+
+ foo
+
+ `,
+ undefined,
+ data,
+ )
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `""`,
+ )
+ expect(`mismatch`).not.toHaveBeenWarned()
+ })
+
+ test('transition appear with v-show', async () => {
+ const data = ref(false)
+ const { container } = await testHydration(
+ `
+
+ foo
+
+ `,
+ undefined,
+ data,
+ )
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `"foo
"`,
+ )
+ expect(`mismatch`).not.toHaveBeenWarned()
+ })
+
+ test('transition appear w/ event listener', async () => {
+ const { container } = await testHydration(
+ `
+
+
+
+
+ `,
+ )
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `""`,
+ )
+
+ triggerEvent('click', container.querySelector('button')!)
+ await nextTick()
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `""`,
+ )
+ })
})
describe.todo('async component', async () => {
diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts
index 7954dcb05..4e88afe8f 100644
--- a/packages/runtime-vapor/src/apiCreateFor.ts
+++ b/packages/runtime-vapor/src/apiCreateFor.ts
@@ -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 {
diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts
index 560b277cb..b3deaf0b6 100644
--- a/packages/runtime-vapor/src/components/Transition.ts
+++ b/packages/runtime-vapor/src/components/Transition.ts
@@ -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
+ let resetDisplay: Function | undefined
+ if (
+ isHydrating &&
+ currentHydrationNode &&
+ isTemplateNode(currentHydrationNode)
+ ) {
+ // replace 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
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
},
)
diff --git a/packages/runtime-vapor/src/components/TransitionGroup.ts b/packages/runtime-vapor/src/components/TransitionGroup.ts
index e3b4fb4fb..81f384595 100644
--- a/packages/runtime-vapor/src/components/TransitionGroup.ts
+++ b/packages/runtime-vapor/src/components/TransitionGroup.ts
@@ -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
diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts
index da92f6b4e..f5b70b08b 100644
--- a/packages/runtime-vapor/src/fragment.ts
+++ b/packages/runtime-vapor/src/fragment.ts
@@ -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. ``
+ 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')