Skip to content

Commit

Permalink
fix(transition/ssr): make transition appear work with SSR (#8859)
Browse files Browse the repository at this point in the history
close #6951
  • Loading branch information
edison1105 committed Oct 24, 2023
1 parent 16ecb44 commit 5ea8a8a
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 26 deletions.
25 changes: 25 additions & 0 deletions packages/compiler-ssr/__tests__/ssrTransition.spec.ts
@@ -0,0 +1,25 @@
import { compile } from '../src'

describe('transition', () => {
test('basic', () => {
expect(compile(`<transition><div>foo</div></transition>`).code)
.toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}>foo</div>\`)
}"
`)
})

test('with appear', () => {
expect(compile(`<transition appear><div>foo</div></transition>`).code)
.toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<template><div\${_ssrRenderAttrs(_attrs)}>foo</div></template>\`)
}"
`)
})
})
12 changes: 8 additions & 4 deletions packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
Expand Up @@ -56,6 +56,10 @@ import {
} from './ssrTransformTransitionGroup'
import { isSymbol, isObject, isArray } from '@vue/shared'
import { buildSSRProps } from './ssrTransformElement'
import {
ssrProcessTransition,
ssrTransformTransition
} from './ssrTransformTransition'

// We need to construct the slot functions in the 1st pass to ensure proper
// scope tracking, but the children of each slot cannot be processed until
Expand Down Expand Up @@ -99,9 +103,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
if (isSymbol(component)) {
if (component === SUSPENSE) {
return ssrTransformSuspense(node, context)
}
if (component === TRANSITION_GROUP) {
} else if (component === TRANSITION_GROUP) {
return ssrTransformTransitionGroup(node, context)
} else if (component === TRANSITION) {
return ssrTransformTransition(node, context)
}
return // other built-in components: fallthrough
}
Expand Down Expand Up @@ -216,9 +221,8 @@ export function ssrProcessComponent(
if ((parent as WIPSlotEntry).type === WIP_SLOT) {
context.pushStringPart(``)
}
// #5351: filter out comment children inside transition
if (component === TRANSITION) {
node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT)
return ssrProcessTransition(node, context)
}
processChildren(node, context)
}
Expand Down
36 changes: 36 additions & 0 deletions packages/compiler-ssr/src/transforms/ssrTransformTransition.ts
@@ -0,0 +1,36 @@
import {
ComponentNode,
findProp,
NodeTypes,
TransformContext
} from '@vue/compiler-dom'
import { processChildren, SSRTransformContext } from '../ssrCodegenTransform'

const wipMap = new WeakMap<ComponentNode, Boolean>()

export function ssrTransformTransition(
node: ComponentNode,
context: TransformContext
) {
return () => {
const appear = findProp(node, 'appear', false, true)
wipMap.set(node, !!appear)
}
}

export function ssrProcessTransition(
node: ComponentNode,
context: SSRTransformContext
) {
// #5351: filter out comment children inside transition
node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT)

const appear = wipMap.get(node)
if (appear) {
context.pushStringPart(`<template>`)
processChildren(node, context, false, true)
context.pushStringPart(`</template>`)
} else {
processChildren(node, context, false, true)
}
}
76 changes: 74 additions & 2 deletions packages/runtime-core/__tests__/hydration.spec.ts
Expand Up @@ -18,10 +18,14 @@ import {
createVNode,
withDirectives,
vModelCheckbox,
renderSlot
renderSlot,
Transition,
createCommentVNode,
vShow
} from '@vue/runtime-dom'
import { renderToString, SSRContext } from '@vue/server-renderer'
import { PatchFlags } from '../../shared/src'
import { PatchFlags } from '@vue/shared'
import { vShowOldKey } from '../../runtime-dom/src/directives/vShow'

function mountWithHydration(html: string, render: () => any) {
const container = document.createElement('div')
Expand Down Expand Up @@ -1016,6 +1020,74 @@ describe('SSR hydration', () => {
expect(`mismatch`).not.toHaveBeenWarned()
})

test('transition appear', () => {
const { vnode, container } = mountWithHydration(
`<template><div>foo</div></template>`,
() =>
h(
Transition,
{ appear: true },
{
default: () => h('div', 'foo')
}
)
)
expect(container.firstChild).toMatchInlineSnapshot(`
<div
class="v-enter-from v-enter-active"
>
foo
</div>
`)
expect(vnode.el).toBe(container.firstChild)
expect(`mismatch`).not.toHaveBeenWarned()
})

test('transition appear with v-if', () => {
const show = false
const { vnode, container } = mountWithHydration(
`<template><!----></template>`,
() =>
h(
Transition,
{ appear: true },
{
default: () => (show ? h('div', 'foo') : createCommentVNode(''))
}
)
)
expect(container.firstChild).toMatchInlineSnapshot('<!---->')
expect(vnode.el).toBe(container.firstChild)
expect(`mismatch`).not.toHaveBeenWarned()
})

test('transition appear with v-show', () => {
const show = false
const { vnode, container } = mountWithHydration(
`<template><div style="display: none;">foo</div></template>`,
() =>
h(
Transition,
{ appear: true },
{
default: () =>
withDirectives(createVNode('div', null, 'foo'), [[vShow, show]])
}
)
)
expect(container.firstChild).toMatchInlineSnapshot(`
<div
class="v-enter-from v-enter-active"
style="display: none;"
>
foo
</div>
`)
expect((container.firstChild as any)[vShowOldKey]).toBe('')
expect(vnode.el).toBe(container.firstChild)
expect(`mismatch`).not.toHaveBeenWarned()
})

describe('mismatch handling', () => {
test('text node', () => {
const { container } = mountWithHydration(`foo`, () => 'bar')
Expand Down
101 changes: 85 additions & 16 deletions packages/runtime-core/src/hydration.ts
Expand Up @@ -15,7 +15,7 @@ import { ComponentInternalInstance } from './component'
import { invokeDirectiveHook } from './directives'
import { warn } from './warning'
import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared'
import { RendererInternals } from './renderer'
import { needTransition, RendererInternals } from './renderer'
import { setRef } from './rendererTemplateRef'
import {
SuspenseImpl,
Expand Down Expand Up @@ -146,7 +146,17 @@ export function createHydrationFunctions(
break
case Comment:
if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) {
nextNode = onMismatch()
if ((node as Element).tagName.toLowerCase() === 'template') {
const content = (vnode.el! as HTMLTemplateElement).content
.firstChild!

// replace <template> node with inner children
replaceNode(content, node, parentComponent)
vnode.el = node = content
nextNode = nextSibling(node)
} else {
nextNode = onMismatch()
}
} else {
nextNode = nextSibling(node)
}
Expand Down Expand Up @@ -196,9 +206,10 @@ export function createHydrationFunctions(
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
if (
domType !== DOMNodeTypes.ELEMENT ||
(vnode.type as string).toLowerCase() !==
(node as Element).tagName.toLowerCase()
(domType !== DOMNodeTypes.ELEMENT ||
(vnode.type as string).toLowerCase() !==
(node as Element).tagName.toLowerCase()) &&
!isTemplateNode(node as Element)
) {
nextNode = onMismatch()
} else {
Expand All @@ -217,15 +228,6 @@ export function createHydrationFunctions(
// on its sub-tree.
vnode.slotScopeIds = slotScopeIds
const container = parentNode(node)!
mountComponent(
vnode,
container,
null,
parentComponent,
parentSuspense,
isSVGContainer(container),
optimized
)

// Locate the next node.
if (isFragmentStart) {
Expand All @@ -241,6 +243,16 @@ export function createHydrationFunctions(
nextNode = nextSibling(node)
}

mountComponent(
vnode,
container,
null,
parentComponent,
parentSuspense,
isSVGContainer(container),
optimized
)

// #3787
// if component is async, it may get moved / unmounted before its
// inner component is loaded, so we need to give it a placeholder
Expand Down Expand Up @@ -307,7 +319,7 @@ export function createHydrationFunctions(
optimized: boolean
) => {
optimized = optimized || !!vnode.dynamicChildren
const { type, props, patchFlag, shapeFlag, dirs } = vnode
const { type, props, patchFlag, shapeFlag, dirs, transition } = vnode
// #4006 for form elements with non-string v-model value bindings
// e.g. <option :value="obj">, <input type="checkbox" :true-value="1">
const forcePatchValue = (type === 'input' && dirs) || type === 'option'
Expand Down Expand Up @@ -359,12 +371,40 @@ export function createHydrationFunctions(
if ((vnodeHooks = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHooks, parentComponent, vnode)
}

// handle appear transition
let needCallTransitionHooks = false
if (isTemplateNode(el)) {
needCallTransitionHooks =
needTransition(parentSuspense, transition) &&
parentComponent &&
parentComponent.vnode.props &&
parentComponent.vnode.props.appear

const content = (el as HTMLTemplateElement).content
.firstChild as Element

if (needCallTransitionHooks) {
transition!.beforeEnter(content)
}

// replace <template> node with inner children
replaceNode(content, el, parentComponent)
vnode.el = el = content
}

if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
}
if ((vnodeHooks = props && props.onVnodeMounted) || dirs) {

if (
(vnodeHooks = props && props.onVnodeMounted) ||
dirs ||
needCallTransitionHooks
) {
queueEffectWithSuspense(() => {
vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode)
needCallTransitionHooks && transition!.enter(el)
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
}, parentSuspense)
}
Expand Down Expand Up @@ -582,5 +622,34 @@ export function createHydrationFunctions(
return node
}

const replaceNode = (
newNode: Node,
oldNode: Node,
parentComponent: ComponentInternalInstance | null
): void => {
// replace node
const parentNode = oldNode.parentNode
if (parentNode) {
parentNode.replaceChild(newNode, oldNode)
}

// update vnode
let parent = parentComponent
while (parent) {
if (parent.vnode.el === oldNode) {
parent.vnode.el = newNode
parent.subTree.el = newNode
}
parent = parent.parent
}
}

const isTemplateNode = (node: Element): boolean => {
return (
node.nodeType === DOMNodeTypes.ELEMENT &&
node.tagName.toLowerCase() === 'template'
)
}

return [hydrate, hydrateNode] as const
}
17 changes: 13 additions & 4 deletions packages/runtime-core/src/renderer.ts
Expand Up @@ -72,6 +72,7 @@ import { initFeatureFlags } from './featureFlags'
import { isAsyncWrapper } from './apiAsyncComponent'
import { isCompatEnabled } from './compat/compatConfig'
import { DeprecationTypes } from './compat/compatConfig'
import { TransitionHooks } from './components/BaseTransition'

export interface Renderer<HostElement = RendererElement> {
render: RootRenderFunction<HostElement>
Expand Down Expand Up @@ -701,10 +702,7 @@ function baseCreateRenderer(
}
// #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved
// #1689 For inside suspense + suspense resolved case, just call it
const needCallTransitionHooks =
(!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
transition &&
!transition.persisted
const needCallTransitionHooks = needTransition(parentSuspense, transition)
if (needCallTransitionHooks) {
transition!.beforeEnter(el)
}
Expand Down Expand Up @@ -2365,6 +2363,17 @@ function toggleRecurse(
effect.allowRecurse = update.allowRecurse = allowed
}

export function needTransition(
parentSuspense: SuspenseBoundary | null,
transition: TransitionHooks | null
) {
return (
(!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
transition &&
!transition.persisted
)
}

/**
* #1156
* When a component is HMR-enabled, we need to make sure that all static nodes
Expand Down

0 comments on commit 5ea8a8a

Please sign in to comment.