Skip to content
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

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

Merged
merged 22 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from 8 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>\`)
}"
`)
})
})
10 changes: 8 additions & 2 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 @@ -103,6 +107,9 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
if (component === TRANSITION_GROUP) {
return ssrTransformTransitionGroup(node, context)
}
if (component === TRANSITION) {
return ssrTransformTransition(node, context)
}
return // other built-in components: fallthrough
}

Expand Down Expand Up @@ -216,9 +223,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
37 changes: 37 additions & 0 deletions packages/compiler-ssr/src/transforms/ssrTransformTransition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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)
if (appear) {
wipMap.set(node, true)
}
}
}

export function ssrProcessTransition(
node: ComponentNode,
context: SSRTransformContext
) {
node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT)

const hasAppear = wipMap.get(node)
if (hasAppear) {
context.pushStringPart(`<template>`)
processChildren(node, context, false, true)
context.pushStringPart(`</template>`)
} else {
processChildren(node, context, false, true)
}
}
74 changes: 73 additions & 1 deletion packages/runtime-core/__tests__/hydration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import {
createVNode,
withDirectives,
vModelCheckbox,
renderSlot
renderSlot,
Transition,
createCommentVNode,
vShow,
vShowOldKey
} from '@vue/runtime-dom'
import { renderToString, SSRContext } from '@vue/server-renderer'
import { PatchFlags } from '../../shared/src'
Expand Down Expand Up @@ -994,6 +998,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
96 changes: 81 additions & 15 deletions packages/runtime-core/src/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ export function createHydrationFunctions(
parentNode,
remove,
insert,
createComment
createComment,
replace
}
} = rendererInternals

Expand Down Expand Up @@ -146,7 +147,16 @@ 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 child
node = replaceNode(content, node, parentComponent)
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,6 +228,14 @@ export function createHydrationFunctions(
// on its sub-tree.
vnode.slotScopeIds = slotScopeIds
const container = parentNode(node)!

// component may be async, so in the case of fragments we cannot rely
// on component's rendered output to determine the end of the fragment
// instead, we do a lookahead to find the end anchor node.
nextNode = isFragmentStart
? locateClosingAsyncAnchor(node)
: nextSibling(node)

mountComponent(
vnode,
container,
Expand All @@ -227,13 +246,6 @@ export function createHydrationFunctions(
optimized
)

// component may be async, so in the case of fragments we cannot rely
// on component's rendered output to determine the end of the fragment
// instead, we do a lookahead to find the end anchor node.
nextNode = isFragmentStart
? locateClosingAsyncAnchor(node)
: nextSibling(node)

// #4293 teleport as component root
if (
nextNode &&
Expand Down Expand Up @@ -309,7 +321,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 @@ -361,12 +373,37 @@ export function createHydrationFunctions(
if ((vnodeHooks = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHooks, parentComponent, vnode)
}
if (dirs) {

let needCallTransitionHooks = false
if (isTemplateNode(el)) {
needCallTransitionHooks =
(!parentSuspense ||
(parentSuspense && !parentSuspense.pendingBranch)) &&
transition &&
!transition.persisted &&
parentComponent?.vnode.props?.appear

const content = (el as HTMLTemplateElement).content
.firstChild as Element
needCallTransitionHooks && transition!.beforeEnter(content)

vnode.el = content
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')

// replace <template> node with inner child
el = replaceNode(content, el, parentComponent) as Element
} else 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 @@ -579,5 +616,34 @@ export function createHydrationFunctions(
return node
}

const replaceNode = (
newNode: Node,
oldNode: Node,
parentComponent: ComponentInternalInstance | null
): Node => {
// replace node
replace(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
}

oldNode = newNode
return oldNode
}

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

return [hydrate, hydrateNode] as const
}
1 change: 1 addition & 0 deletions packages/runtime-core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export interface RendererOptions<
): void
insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void
remove(el: HostNode): void
replace(newChild: HostNode, oldChild: HostNode): void
createElement(
type: string,
isSVG?: boolean,
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export {
vModelDynamic
} from './directives/vModel'
export { withModifiers, withKeys } from './directives/vOn'
export { vShow } from './directives/vShow'
export { vShow, vShowOldKey } from './directives/vShow'

import { initVModelForSSR } from './directives/vModel'
import { initVShowForSSR } from './directives/vShow'
Expand Down
7 changes: 7 additions & 0 deletions packages/runtime-dom/src/nodeOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
}
},

replace: (newChild, oldChild) => {
const parent = oldChild.parentNode
if (parent) {
parent.replaceChild(newChild, oldChild)
}
},

createElement: (tag, isSVG, is, props): Element => {
const el = isSVG
? doc.createElementNS(svgNS, tag)
Expand Down
Loading