Skip to content

feat(ssr): transition appear work with SSR #8859

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/compiler-ssr/__tests__/ssrTransition.spec.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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